.NET 9 GetIsNetworkAvailable returns true when offline

Summary

During a regression test on a Windows 11 LTSC environment, we discovered a critical discrepancy in how network availability is reported by the runtime. A test unit with no physical Ethernet connection and no active Wi-Fi connection yielded different results based solely on the target framework:

  • Built with .NET 8.0: NetworkInterface.GetIsNetworkAvailable() returned false (Expected behavior).
  • Built with .NET 9.0: NetworkInterface.GetIsNetworkAvailable() returned true (Unexpected behavior).

This inconsistency breaks logic in downstream services that rely on strict network state detection to trigger retry mechanisms or offline modes.

Root Cause

The issue stems from a change in the underlying implementation logic within the BCL (Base Class Library) between .NET versions.

  • The .NET 8.0 Implementation: Relied on older Win32 API calls or internal logic that strictly checked for an active, routable network interface with a valid IP configuration.
  • The .NET 9.0 Implementation: The definition of “available” was broadened. In newer versions, the runtime may report true if a network interface is enabled/up, even if it lacks an active connection to a gateway or a valid IP address.
  • OS Abstraction Layers: .NET abstracts Windows networking via the IP Helper API. Changes in how the runtime interprets the flags returned by these APIs (e.g., GetAdaptersAddresses) cause the mismatch in perceived “availability.”

Why This Happens in Real Systems

In large-scale production environments, this is a classic example of Leaky Abstractions.

  • Framework Evolution: Library maintainers often optimize or “correct” functions to align with newer OS definitions of a state. What was considered a “bug” in .NET 8 might be considered “correct” according to new networking standards in .NET 9.
  • Environmental Drift: A system may pass CI/CD tests on a developer machine (with internet) but fail in a hardened, air-gapped LTSC (Long-Term Servicing Channel) environment where hardware states are more rigid.
  • Implicit Assumptions: Code is often written assuming a function’s behavior is an invariant, whereas, in managed runtimes, behavior is often a version-dependent implementation detail.

Real-World Impact

  • Infinite Retry Loops: Services attempting to reach an API might enter a tight loop because they believe the network is up, consuming excessive CPU.
  • Data Corruption/Loss: An application might attempt to flush a local buffer to a remote database, failing repeatedly and causing memory pressure or disk bloat.
  • Broken Offline Modes: Critical UI/UX components might fail to enter “Offline Mode,” leaving users with broken buttons and misleading “Connecting…” spinners.

Example or Code

To regain the strict behavior of .NET 8.0 in a .NET 9.0 environment, we must move away from the high-level abstraction and inspect the IP interface properties directly to ensure a valid gateway or IP exists.

using System.Net.NetworkInformation;
using System.Linq;

public static class NetworkChecker
{
    public static bool IsNetworkStrictlyAvailable()
    {
        return NetworkInterface.GetIsNetworkAvailable() && 
               NetworkInterface.GetAllNetworkInterfaces()
                   .Any(ni => ni.OperationalStatus == OperationalStatus.Up && 
                              ni.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
                              ni.GetIPProperties().UnicastAddresses.Any(ua => ua.Address.AddressFamily == System.Net.System.Net.Sockets.AddressFamily.InterNetwork));
    }
}

How Senior Engineers Fix It

Senior engineers do not rely on “magic” boolean functions for critical logic. They implement defensive, multi-layered checks.

  • Avoid High-Level Heuristics: Instead of GetIsNetworkAvailable(), we implement checks that verify reachability (e.g., attempting to resolve a known DNS entry or pinging a gateway).
  • Dependency Inversion: We wrap network detection in an INetworkService interface. This allows us to mock the network state during testing and swap implementations if a runtime upgrade breaks the logic.
  • Version-Agnostic Logic: We write code that validates the actual capability (can I send a packet?) rather than the reported state (is the wire plugged in?).

Why Juniors Miss It

  • Trusting the API Documentation: Juniors often assume that if a method name is GetIsNetworkAvailable, it will always behave the same way across all versions. They miss the “Implementation Note” sections in documentation.
  • Testing in “Happy Path” Environments: Most development happens on machines with stable, always-on internet. The edge cases of LTSC/Air-gapped environments are rarely encountered until production.
  • Focus on Syntax over Semantics: A junior focuses on making the code compile and run; a senior focuses on what the code actually means in the context of the underlying operating system.

Leave a Comment