Repository: dotnet/interactive
Pull request #3910: Adds `--http-local-only` option to bypass networking probing
Author: login: Luiz-Ossinho
Created: 2025-04-10T05:44:29Z | Merged: 2025-04-15T16:53:10Z

# Add fallback to `NetworkInterface.GetAllNetworkInterfaces()` [Fixes #3903]

- All commits should be squashed before merge.

### Content
I’ve applied the same logic used in other "server startup" related options.  
Please let me know if you spot any issues or inconsistencies in the current implementation.

### Styling
I believe I’m following the [specified coding guidelines](https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/coding-style.md).  
Let me know if you notice anything that needs improvement.


Linked issue #3903: Add fallback to `NetworkInterface.GetAllNetworkInterfaces()`
**Is your feature request related to a problem? Please describe.**
When running on an proot/chroot Android enviroment, non-rooted apps cannot access /sys/class/net/. It's restricted by SELiux policy

**Describe the solution you'd like**
Change the life-cycle of `Microsoft.DotNet.Interactive.Http.HttpProbingSettings` to be able to read command line arguments
Add a command line argument for bypassing HttpProbing and providing a port directly

**Describe alternatives you've considered**
So far, since the SELiux policy change on Android, the `NetworkInterface.GetAllNetworkInterfaces()` wont work unless the device is rooted.
I heard that Android offers APIs as an alternative to trying to read `/sys/class/net/` directly, preharps it would be possible to create an issue documenting the restriction on the arm64 linux runtime.
For now, Im considering using other REPL alternatives, but the lack of Jupyter integration is going to be missed.
Im also considerimg taking this Issue to the proot/chroot on anroid community instead and create a daemon that populates the `/sys/class/net` instead.
Linked issue comments: user: login: Luiz-Ossinho
created_at: 2025-04-07T03:47:18Z
body: I might get around to writing an PR later, that would solve this using a comand line bypass argument, just wanted to raise discussion about this issue for now.
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-08T16:11:51Z
body: There's been some discussion about improving support for connecting over HTTP. 

Could you describe your scenario at a slightly higher level? 
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-08T16:51:52Z
body: > There's been some discussion about improving support for connecting over HTTP. 
> 
> Could you describe your scenario at a slightly higher level? 

I'm attempting to use .NET Interactive (e.g., in Jupyter Notebook/Lab) on an Android device via a **proot/chroot environment** (like Termux). This setup is common for running Linux tools on non-rooted Android devices. However, the current HTTP probing logic in .NET Interactive, which relies on enumerating network interfaces via `NetworkInterface.GetAllNetworkInterfaces()`, which in turn, requires acces to `/sys/class/net`, therefore being blocked by Android’s SELinux policy for non-root apps. This prevents the kernel from binding to a port and establishing a connection with the Jupyter frontend, even tough the probbing logic implies that loopback address should be added as a fallback, but due to `NetworkInterface.GetAllNetworkInterfaces()` throwing an exception, this logic is not executed.

**Key Goal:**  
Enable .NET Interactive to work seamlessly in Jupyter on Android (via proot) without requiring root access or SELinux policy changes.

**Why This Matters:**  
Android is increasingly used as a lightweight development environment, especially with tools like Termux. .NET Interactive’s Jupyter integration is uniquely powerful here, but the HTTP probing failure blocks this workflow entirely. Without a workaround, users are forced to abandon .NET Interactive for less integrated REPL alternatives.

**Request:**  
1. Allow users to **disable HTTP probing** via a command-line flag (e.g., `--disable-http-probing`).  
2. Combine this with a **`--http-port`** argument to explicitly set the port when probing is disabled.  

This would bypass the problematic `NetworkInterface.GetAllNetworkInterfaces()` call while retaining Jupyter functionality in restricted environments like Android.
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-08T20:38:17Z
body: What command line are you using to start `dotnet interactive`?
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-08T20:59:25Z
body: > What command line are you using to start `dotnet interactive`?

This is my kernel configuration after `dotnet interactive jupyter install`

```
root@localhost:~# cat /root/.local/share/jupyter/kernels/.net-csharp/kernel.json
{
  "argv": [
    "dotnet",
    "interactive",
    "jupyter",
    "--default-kernel",
    "csharp",
    "{connection_file}",
    "--http-port-range",
    "2048-3000"
  ],
  "display_name": ".NET (C#)",
  "language": "C#",
  "metadata": {
    "vscode": {
      "extension_id": "ms-dotnettools.dotnet-interactive-vscode",
      "kernel_id": "dotnet-interactive"
    }
  }
}
```

I do not invoke it directly but i believe jupyter uses the `"argv"` array to start the kernel.
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-09T19:19:46Z
body: Thanks for the detailed explanation. 

I'd be happy to help you if you're interested in submitting a PR. 
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-10T05:48:56Z
body: > Thanks for the detailed explanation.
> 
> I'd be happy to help you if you're interested in submitting a PR.

Here is the [PR](https://github.com/dotnet/interactive/pull/3910)

As said in the PR, its still WIP:
- Tests for the new option are yet to be implemented.
- I would like to gather evidence that this build actually solves the issue.
author_association: CONTRIBUTOR

Diff:
diff --git a/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs b/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs
index ff08f974ec..593b1f55f2 100644
--- a/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs
+++ b/src/dotnet-interactive.Tests/CommandLine/CommandLineParserTests.cs
@@ -9,6 +9,7 @@
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Net.NetworkInformation;
 using System.Text;
 using System.Text.Json;
 using System.Threading.Tasks;
@@ -20,7 +21,6 @@
 using Microsoft.DotNet.Interactive.App.Connection;
 using Microsoft.DotNet.Interactive.App.Tests.Extensions;
 using Microsoft.DotNet.Interactive.Commands;
-using Microsoft.DotNet.Interactive.Connection;
 using Microsoft.DotNet.Interactive.Http;
 using Microsoft.DotNet.Interactive.Telemetry;
 using Microsoft.DotNet.Interactive.Tests.Utility;
@@ -187,6 +187,46 @@ public void jupyter_command_help_shows_default_port_range()
         _console.Out.ToString().Should().Contain("default: 2048-3000");
     }
 
+    [Fact]
+    public void jupyter_command_parses_http_local_only_option()
+    {
+        var result = _parser.Parse($"jupyter --http-local-only {_connectionFile}");
+
+        var binder = new ModelBinder<StartupOptions>();
+
+        var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext);
+
+        options
+            .GetAllNetworkInterfaces
+            .Should()
+            .Match(x => x == StartupOptions.GetNetworkInterfacesHttpLocalOnly);
+
+        options
+            .GetAllNetworkInterfaces
+            .Should()
+            .Match(x => x != NetworkInterface.GetAllNetworkInterfaces);
+    }
+
+    [Fact]
+    public void jupyter_command_default_network_interface_if_no_http_local_only_option()
+    {
+        var result = _parser.Parse($"jupyter {_connectionFile}");
+
+        var binder = new ModelBinder<StartupOptions>();
+
+        var options = (StartupOptions)binder.CreateInstance(new InvocationContext(result).BindingContext);
+
+        options
+            .GetAllNetworkInterfaces
+            .Should()
+            .Match(x => x != StartupOptions.GetNetworkInterfacesHttpLocalOnly);
+
+        options
+            .GetAllNetworkInterfaces
+            .Should()
+            .Match(x => x == NetworkInterface.GetAllNetworkInterfaces);
+    }
+
     [Fact]
     public void jupyter_install_command_parses_path_option()
     {
diff --git a/src/dotnet-interactive/CommandLine/CommandLineParser.cs b/src/dotnet-interactive/CommandLine/CommandLineParser.cs
index 743aee4950..fbbadb917d 100644
--- a/src/dotnet-interactive/CommandLine/CommandLineParser.cs
+++ b/src/dotnet-interactive/CommandLine/CommandLineParser.cs
@@ -171,9 +171,15 @@ Command Jupyter()
                 description: LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description(),
                 isDefault: true);
 
+            var httpLocalOnlyOption = new Option<bool>(
+                "--http-local-only",
+                description: LocalizationResources.Cli_dotnet_interactive_jupyter_http_local_only_Description()
+            );
+
             var jupyterCommand = new Command("jupyter", LocalizationResources.Cli_dotnet_interactive_jupyter_Description())
             {
                 defaultKernelOption,
+                httpLocalOnlyOption,
                 httpPortRangeOption,
                 new Argument<FileInfo>
                 {
diff --git a/src/dotnet-interactive/CommandLine/StartupOptions.cs b/src/dotnet-interactive/CommandLine/StartupOptions.cs
index 638685b24c..90bbe81bb6 100644
--- a/src/dotnet-interactive/CommandLine/StartupOptions.cs
+++ b/src/dotnet-interactive/CommandLine/StartupOptions.cs
@@ -3,6 +3,7 @@
 
 using System;
 using System.IO;
+using System.Net.NetworkInformation;
 using Microsoft.DotNet.Interactive.Http;
 
 namespace Microsoft.DotNet.Interactive.App.CommandLine;
@@ -15,7 +16,9 @@ public StartupOptions(
         HttpPortRange httpPortRange = null,
         HttpPort httpPort = null,
         Uri kernelHost = null,
-        DirectoryInfo workingDir = null)
+        DirectoryInfo workingDir = null,
+        bool httpLocalOnly = false
+    )
     {
         LogPath = logPath;
         Verbose = verbose;
@@ -23,6 +26,11 @@ public StartupOptions(
         HttpPort = httpPort;
         KernelHost = kernelHost;
         WorkingDir = workingDir;
+
+        if (httpLocalOnly)
+            GetAllNetworkInterfaces = GetNetworkInterfacesHttpLocalOnly;
+        else
+            GetAllNetworkInterfaces = NetworkInterface.GetAllNetworkInterfaces;
     }
 
     public DirectoryInfo LogPath { get; }
@@ -37,5 +45,12 @@ public StartupOptions(
 
     public DirectoryInfo WorkingDir { get; internal set; }
 
+    public Func<NetworkInterface[]> GetAllNetworkInterfaces { get; }
+
     public bool EnableHttpApi => HttpPort is not null || HttpPortRange is not null;
+
+    public static NetworkInterface[] GetNetworkInterfacesHttpLocalOnly()
+    { 
+        return [];
+    }
 }
diff --git a/src/dotnet-interactive/Http/HttpApiBootstrapper.cs b/src/dotnet-interactive/Http/HttpApiBootstrapper.cs
index 1102dada63..6c36c721df 100644
--- a/src/dotnet-interactive/Http/HttpApiBootstrapper.cs
+++ b/src/dotnet-interactive/Http/HttpApiBootstrapper.cs
@@ -1,19 +1,17 @@
 ﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
-using System;
-using System.Diagnostics;
-using System.Linq;
 using Microsoft.AspNetCore.Html;
+using System.Collections.Generic;
+using System.Text.Json;
 
 namespace Microsoft.DotNet.Interactive.Http;
 
 internal static class HttpApiBootstrapper
 {
-    public static IHtmlContent GetHtmlInjection(Uri[] probingUris, string seed)
+    public static IHtmlContent GetHtmlInjection(IEnumerable<string> probingUris, string seed)
     {
-        var apiCacheBuster = $"{Process.GetCurrentProcess().Id}.{seed}";
-        var template = 
+        var template =
             $@"
 <div>
     <div id='dotnet-interactive-this-cell-$CACHE_BUSTER$' style='display: none'>
@@ -61,12 +59,12 @@ function timeout(ms, promise) {{
 }}
 
 function loadDotnetInteractiveApi() {{
-    probeAddresses($ADDRESSES$)
+    probeAddresses({JsonSerializer.Serialize(probingUris)})
         .then((root) => {{
         // use probing to find host url and api resources
         // load interactive helpers and language services
         let dotnetInteractiveRequire = require.config({{
-        context: '$CACHE_BUSTER$',
+        context: '{System.Environment.ProcessId}.{seed}',
                 paths:
             {{
                 'dotnet-interactive': `${{root}}resources`
@@ -105,11 +103,7 @@ function loadDotnetInteractiveApi() {{
 {JavascriptUtilities.GetCodeForEnsureRequireJs(onRequirejsLoadedCallBackName: "loadDotnetInteractiveApi")}
     </script>
 </div>";
-            
-        var jsProbingUris = $"[{ string.Join(", ", probingUris.Select(a => $"\"{a.AbsoluteUri}\"")) }]";
-        var html =  template
-            .Replace("$ADDRESSES$", jsProbingUris)
-            .Replace("$CACHE_BUSTER$", apiCacheBuster);
-        return new HtmlString(html);
+
+        return new HtmlString(template);
     }
-}
\ No newline at end of file
+}
diff --git a/src/dotnet-interactive/Http/HttpProbingSettings.cs b/src/dotnet-interactive/Http/HttpProbingSettings.cs
index c1978aeb7a..f153fbed50 100644
--- a/src/dotnet-interactive/Http/HttpProbingSettings.cs
+++ b/src/dotnet-interactive/Http/HttpProbingSettings.cs
@@ -9,53 +9,33 @@
 
 namespace Microsoft.DotNet.Interactive.Http;
 
-internal class HttpProbingSettings
+public class HttpProbingSettings
 {
-    public Uri[] AddressList { get; private set; }
+    public IEnumerable<string> AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? httpPort, Func<NetworkInterface[]> getAllNetworkInterfaces)
     {
+        var ipAddress = getAllNetworkInterfaces()
+                .Where(ni => ni.OperationalStatus == OperationalStatus.Up)
+                .SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
+                .Select(x => x.Address.ToString())
+                .Append(IPAddress.Loopback.ToString())
+                .ToHashSet();
+
+        var uriAddresses = ipAddress
+            .Select(AddHttpPort(httpPort));
+
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = uriAddresses
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Func<string, string> AddHttpPort(int? httpPort)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
-            {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
-                {
-
-                    if (ip != IPAddress.Loopback.ToString())
-                    {
-                        sources.Add(ip);
-                    }
-                }
-            }
-        }
-
-        sources.Add(IPAddress.Loopback.ToString());
-
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
-            .Select(s =>
-            {
-                var uriString = httpPort is not null ? $"http://{s}:{httpPort}/" : $"http://{s}/";
-                if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
-                {
-                    return uri;
-                }
-
-                return null;
-            })
-            .Where(u => u is not null)
-            .ToArray();
-
-        return addresses;
+        if (httpPort is null)
+            return ipAddress => $"http://{ipAddress}/";
+
+        return ipAddress => $"http://{ipAddress}:{httpPort}/";
     }
 }
\ No newline at end of file
diff --git a/src/dotnet-interactive/Http/KernelExtensions.cs b/src/dotnet-interactive/Http/KernelExtensions.cs
index 2c8e4e1dad..d25641cf36 100644
--- a/src/dotnet-interactive/Http/KernelExtensions.cs
+++ b/src/dotnet-interactive/Http/KernelExtensions.cs
@@ -30,7 +30,7 @@ Task EnableHttp(KernelCommand _, KernelInvocationContext context)
                                       ? httpProbingSettings.AddressList
                                       :
                                       [
-                                          new Uri($"http://localhost:{httpPort}")
+                                          $"http://localhost:{httpPort}"
                                       ];
                 var html =
                     HttpApiBootstrapper.GetHtmlInjection(probingUrls, httpPort?.ToString() ?? Guid.NewGuid().ToString("N"));
diff --git a/src/dotnet-interactive/LocalizationResources.cs b/src/dotnet-interactive/LocalizationResources.cs
index 4c360a23c2..db62a348fa 100644
--- a/src/dotnet-interactive/LocalizationResources.cs
+++ b/src/dotnet-interactive/LocalizationResources.cs
@@ -17,6 +17,12 @@ internal static string Magics_about_Description()
     internal static string Cli_dotnet_interactive_jupyter_default_kernel_Description()
         => GetResourceString(Resources.Cli_dotnet_interactive_jupyter_default_kernel_Description);
 
+    /// <summary>
+    ///   Gets a localized message like: Exposes ports only on local network interfaces
+    /// </summary>
+    internal static string Cli_dotnet_interactive_jupyter_http_local_only_Description()
+        => GetResourceString(Resources.Cli_dotnet_interactive_jupyter_http_local_only_Description);
+
     /// <summary>
     ///   Gets a localized message like: Interactive programming for .NET.
     /// </summary>
diff --git a/src/dotnet-interactive/Program.cs b/src/dotnet-interactive/Program.cs
index 83fde0322b..4904e4d9e1 100644
--- a/src/dotnet-interactive/Program.cs
+++ b/src/dotnet-interactive/Program.cs
@@ -5,7 +5,6 @@
 using System.CommandLine.Parsing;
 using System.Globalization;
 using System.IO;
-using System.Linq;
 using System.Net;
 using System.Net.Sockets;
 using System.Reflection;
@@ -108,7 +107,7 @@ public static IWebHostBuilder ConstructWebHostBuilder(
         {
             var httpPort = GetFreePort(options);
             options.HttpPort = httpPort;
-            probingSettings = HttpProbingSettings.Create(httpPort.PortNumber);
+            probingSettings = HttpProbingSettings.Create(httpPort.PortNumber, options.GetAllNetworkInterfaces);
         }
 
         var webHost = new WebHostBuilder()
@@ -118,7 +117,7 @@ public static IWebHostBuilder ConstructWebHostBuilder(
 
         if (options.EnableHttpApi && probingSettings is not null)
         {
-            webHost = webHost.UseUrls(probingSettings.AddressList.Select(a => a.AbsoluteUri).ToArray());
+            webHost = webHost.UseUrls(string.Join(';', probingSettings.AddressList));
         }
 
         return webHost;
diff --git a/src/dotnet-interactive/Resources.Designer.cs b/src/dotnet-interactive/Resources.Designer.cs
index 2516c85758..c5e1a57f9f 100644
--- a/src/dotnet-interactive/Resources.Designer.cs
+++ b/src/dotnet-interactive/Resources.Designer.cs
@@ -96,6 +96,15 @@ internal static string Cli_dotnet_interactive_jupyter_Description {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Exposes ports only on local network interfaces.
+        /// </summary>
+        internal static string Cli_dotnet_interactive_jupyter_http_local_only_Description {
+            get {
+                return ResourceManager.GetString("Cli_dotnet_interactive_jupyter_http_local_only_Description", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Install the .NET kernel for Jupyter.
         /// </summary>
@@ -123,6 +132,15 @@ internal static string Cli_dotnet_interactive_jupyter_install_path_Description {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Exposes ports only on local network interfaces.
+        /// </summary>
+        internal static string Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description {
+            get {
+                return ResourceManager.GetString("Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Enable file logging to the specified directory.
         /// </summary>
diff --git a/src/dotnet-interactive/Resources.resx b/src/dotnet-interactive/Resources.resx
index a6422e2072..d99c10537b 100644
--- a/src/dotnet-interactive/Resources.resx
+++ b/src/dotnet-interactive/Resources.resx
@@ -129,6 +129,9 @@
   <data name="Cli_dotnet_interactive_jupyter_Description" xml:space="preserve">
     <value>Starts dotnet-interactive as a Jupyter kernel</value>
   </data>
+  <data name="Cli_dotnet_interactive_jupyter_http_local_only_Description" xml:space="preserve">
+    <value>Exposes ports only on local network interfaces</value>
+  </data>
   <data name="Cli_dotnet_interactive_jupyter_install_Description" xml:space="preserve">
     <value>Install the .NET kernel for Jupyter</value>
   </data>
diff --git a/src/dotnet-interactive/xlf/Resources.cs.xlf b/src/dotnet-interactive/xlf/Resources.cs.xlf
index f3adc897e4..a1a99aa29b 100644
--- a/src/dotnet-interactive/xlf/Resources.cs.xlf
+++ b/src/dotnet-interactive/xlf/Resources.cs.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Výchozí jazyk jádra</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Nainstalovat jádro platformy .NET pro Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.de.xlf b/src/dotnet-interactive/xlf/Resources.de.xlf
index 93f474f74b..588dd57f90 100644
--- a/src/dotnet-interactive/xlf/Resources.de.xlf
+++ b/src/dotnet-interactive/xlf/Resources.de.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Die Standardsprache für den Kernel</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">.NET-Kernel für Jupyter installieren</target>
diff --git a/src/dotnet-interactive/xlf/Resources.es.xlf b/src/dotnet-interactive/xlf/Resources.es.xlf
index 692a25f257..764b2649a9 100644
--- a/src/dotnet-interactive/xlf/Resources.es.xlf
+++ b/src/dotnet-interactive/xlf/Resources.es.xlf
@@ -37,6 +37,11 @@
         <target state="translated">El idioma predeterminado del kernel</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Instalar el kernel de .NET para Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.fr.xlf b/src/dotnet-interactive/xlf/Resources.fr.xlf
index 206cf89ec6..1c42a5e117 100644
--- a/src/dotnet-interactive/xlf/Resources.fr.xlf
+++ b/src/dotnet-interactive/xlf/Resources.fr.xlf
@@ -37,6 +37,11 @@
         <target state="translated">La langue par défaut du noyau</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Installer le noyau .NET pour Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.it.xlf b/src/dotnet-interactive/xlf/Resources.it.xlf
index 8c8cf857c7..bf84658b7e 100644
--- a/src/dotnet-interactive/xlf/Resources.it.xlf
+++ b/src/dotnet-interactive/xlf/Resources.it.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Lingua predefinita per il kernel</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Installare il kernel .NET per Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.ja.xlf b/src/dotnet-interactive/xlf/Resources.ja.xlf
index dd3cfa92e5..359e98ec81 100644
--- a/src/dotnet-interactive/xlf/Resources.ja.xlf
+++ b/src/dotnet-interactive/xlf/Resources.ja.xlf
@@ -37,6 +37,11 @@
         <target state="translated">カーネルの既定の言語</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Jupyter 用の .NET カーネルをインストールします</target>
diff --git a/src/dotnet-interactive/xlf/Resources.ko.xlf b/src/dotnet-interactive/xlf/Resources.ko.xlf
index df68d345db..0adba33dc8 100644
--- a/src/dotnet-interactive/xlf/Resources.ko.xlf
+++ b/src/dotnet-interactive/xlf/Resources.ko.xlf
@@ -37,6 +37,11 @@
         <target state="translated">커널의 기본 언어</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Jupyter용 .NET 커널 설치</target>
diff --git a/src/dotnet-interactive/xlf/Resources.pl.xlf b/src/dotnet-interactive/xlf/Resources.pl.xlf
index 3655c3d2f8..23b07716ea 100644
--- a/src/dotnet-interactive/xlf/Resources.pl.xlf
+++ b/src/dotnet-interactive/xlf/Resources.pl.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Domyślny język jądra</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Zainstaluj jądro platformy .NET dla programu Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.pt-BR.xlf b/src/dotnet-interactive/xlf/Resources.pt-BR.xlf
index 6dd75f9364..a69f14b810 100644
--- a/src/dotnet-interactive/xlf/Resources.pt-BR.xlf
+++ b/src/dotnet-interactive/xlf/Resources.pt-BR.xlf
@@ -37,6 +37,11 @@
         <target state="translated">O idioma padrão para o kernel</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Instalar o kernel do .NET para Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.ru.xlf b/src/dotnet-interactive/xlf/Resources.ru.xlf
index 7e8742c8a1..3266f5bc98 100644
--- a/src/dotnet-interactive/xlf/Resources.ru.xlf
+++ b/src/dotnet-interactive/xlf/Resources.ru.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Язык по умолчанию для ядра</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Установить ядро .NET для Jupyter</target>
diff --git a/src/dotnet-interactive/xlf/Resources.tr.xlf b/src/dotnet-interactive/xlf/Resources.tr.xlf
index 5cbd502190..ffb33db23f 100644
--- a/src/dotnet-interactive/xlf/Resources.tr.xlf
+++ b/src/dotnet-interactive/xlf/Resources.tr.xlf
@@ -37,6 +37,11 @@
         <target state="translated">Çekirdek için varsayılan dil</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">Jupyter için .NET çekirdeğini yükle</target>
diff --git a/src/dotnet-interactive/xlf/Resources.zh-Hans.xlf b/src/dotnet-interactive/xlf/Resources.zh-Hans.xlf
index a595761ff2..378e3f123b 100644
--- a/src/dotnet-interactive/xlf/Resources.zh-Hans.xlf
+++ b/src/dotnet-interactive/xlf/Resources.zh-Hans.xlf
@@ -37,6 +37,11 @@
         <target state="translated">内核默认语言</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">安装适用于 Jupyter 的 .NET 内核</target>
diff --git a/src/dotnet-interactive/xlf/Resources.zh-Hant.xlf b/src/dotnet-interactive/xlf/Resources.zh-Hant.xlf
index 17ddd5cebe..4db547704e 100644
--- a/src/dotnet-interactive/xlf/Resources.zh-Hant.xlf
+++ b/src/dotnet-interactive/xlf/Resources.zh-Hant.xlf
@@ -37,6 +37,11 @@
         <target state="translated">核心的預設語言</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_http_local_only_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Cli_dotnet_interactive_jupyter_install_Description">
         <source>Install the .NET kernel for Jupyter</source>
         <target state="translated">安裝適用於 Jupyter 的 .NET 核心</target>


Reviews:
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T19:58:44Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:02:26Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:03:20Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:05:42Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T20:07:22Z
user: login: Luiz-Ossinho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T04:26:33Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T14:45:05Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:15:08Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:39:08Z
user: login: jonsequitur
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:40:35Z
user: login: Luiz-Ossinho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:54:14Z
user: login: Luiz-Ossinho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T18:59:26Z
user: login: jonsequitur
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T16:52:37Z

Review comments:
diff_hunk: @@ -13,39 +13,38 @@ internal class HttpProbingSettings
 {
     public Uri[] AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? port, bool localOnlyNetworkInterfaces)
     {
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = GetProbingAddressList(port, localOnlyNetworkInterfaces)
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Uri[] GetProbingAddressList(int? httpPort, bool localOnlyNetworkInterfaces)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
+        HashSet<string> sourcesIpAddresses = [IPAddress.Loopback.ToString()];
+
+        if (!localOnlyNetworkInterfaces)
+            foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
             {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
+                if (ni.OperationalStatus == OperationalStatus.Up)
                 {
-
-                    if (ip != IPAddress.Loopback.ToString())
+                    foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
                     {
-                        sources.Add(ip);
+                        if (ip != IPAddress.Loopback.ToString() && !string.IsNullOrWhiteSpace(ip))
+                        {
+                            sourcesIpAddresses.Add(ip);
+                        }
                     }
                 }
             }
-        }
 
-        sources.Add(IPAddress.Loopback.ToString());
-
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
path: src/dotnet-interactive/Http/HttpProbingSettings.cs
user: login: jonsequitur
body: Was it intentional to remove the `Where` call and if so, what's the reasoning?
created_at: 2025-04-10T19:58:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +52,11 @@
         <target state="translated">Nainstaluje specifikace jádra do zadaného adresáře.</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
path: src/dotnet-interactive/xlf/Resources.cs.xlf
user: login: jonsequitur
body: It's not necessary to update any of the `.xlf` files.
created_at: 2025-04-10T20:02:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,9 +171,16 @@ Command Jupyter()
                 description: LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description(),
                 isDefault: true);
 
+            var localOnlyNetworkInterfacesOption = new Option<bool>(
+                "--local-only-network-interfaces",
+                description: LocalizationResources.Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description(),
+                getDefaultValue: () => false
path: src/dotnet-interactive/CommandLine/CommandLineParser.cs
user: login: jonsequitur
body: There's no need to specify a default value since `false` is already the default.
created_at: 2025-04-10T20:03:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,39 +13,38 @@ internal class HttpProbingSettings
 {
     public Uri[] AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? port, bool localOnlyNetworkInterfaces)
     {
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = GetProbingAddressList(port, localOnlyNetworkInterfaces)
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Uri[] GetProbingAddressList(int? httpPort, bool localOnlyNetworkInterfaces)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
+        HashSet<string> sourcesIpAddresses = [IPAddress.Loopback.ToString()];
+
+        if (!localOnlyNetworkInterfaces)
+            foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
             {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
+                if (ni.OperationalStatus == OperationalStatus.Up)
                 {
-
-                    if (ip != IPAddress.Loopback.ToString())
+                    foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
                     {
-                        sources.Add(ip);
+                        if (ip != IPAddress.Loopback.ToString() && !string.IsNullOrWhiteSpace(ip))
+                        {
+                            sourcesIpAddresses.Add(ip);
+                        }
                     }
                 }
             }
-        }
 
-        sources.Add(IPAddress.Loopback.ToString());
-
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
path: src/dotnet-interactive/Http/HttpProbingSettings.cs
user: login: jonsequitur
body: Oh, I see you moved it into the loop.
created_at: 2025-04-10T20:05:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,9 +171,16 @@ Command Jupyter()
                 description: LocalizationResources.Cli_dotnet_interactive_jupyter_install_http_port_range_Description(),
                 isDefault: true);
 
+            var localOnlyNetworkInterfacesOption = new Option<bool>(
+                "--local-only-network-interfaces",
path: src/dotnet-interactive/CommandLine/CommandLineParser.cs
user: login: jonsequitur
body: Maybe `--http-local-only` to align better with the `--http-port-range` option?
created_at: 2025-04-10T20:07:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +52,11 @@
         <target state="translated">Nainstaluje specifikace jádra do zadaného adresáře.</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
path: src/dotnet-interactive/xlf/Resources.cs.xlf
user: login: Luiz-Ossinho
body: I did redefined it to back to what it was before any changes introduced in my commits, but the CI pipeline failed with the following:


---
.packages/microsoft.dotnet.xlifftasks/1.0.0-beta.23475.1/build/Microsoft.DotNet.XliffTasks.targets(84,5): error : (NETCORE_ENGINEERING_TELEMETRY=Build) 'xlf/Resources.cs.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files or set UpdateXlfOnBuild=true to update them on every build, but note that it is strongly discouraged to set UpdateXlfOnBuild=true in official/CI build environments as they should not modify source code during the build.
created_at: 2025-04-14T04:26:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,6 +52,11 @@
         <target state="translated">Nainstaluje specifikace jádra do zadaného adresáře.</target>
         <note />
       </trans-unit>
+      <trans-unit id="Cli_dotnet_interactive_jupyter_local_only_network_interfaces_Description">
+        <source>Exposes ports only on local network interfaces</source>
+        <target state="new">Exposes ports only on local network interfaces</target>
+        <note />
+      </trans-unit>
path: src/dotnet-interactive/xlf/Resources.cs.xlf
user: login: jonsequitur
body: My mistake. I haven't added one of these strings in a while.
created_at: 2025-04-14T14:45:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,53 +9,49 @@
 
 namespace Microsoft.DotNet.Interactive.Http;
 
-internal class HttpProbingSettings
+public class HttpProbingSettings
 {
-    public Uri[] AddressList { get; private set; }
+    public IEnumerable<string> AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? httpPort, bool httpLocalOnly)
     {
+        HashSet<string> ipAddress = [IPAddress.Loopback.ToString()];
+
+        if (!httpLocalOnly)
+        {
+            ipAddress = GetAllNetworkInterfaces()
+                .Where(ni => ni.OperationalStatus == OperationalStatus.Up)
+                .SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
+                .Select(x => x.Address.ToString())
+                .Concat(ipAddress)
+                .ToHashSet();
+        }
+
+        var uriAddresses = ipAddress
+            .Select(AddHttpPort(httpPort));
+
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = uriAddresses
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Func<string, string> AddHttpPort(int? httpPort)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
-            {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
-                {
-
-                    if (ip != IPAddress.Loopback.ToString())
-                    {
-                        sources.Add(ip);
-                    }
-                }
-            }
-        }
+        if (httpPort is null)
+            return ipAddress => $"http://{ipAddress}/";
 
-        sources.Add(IPAddress.Loopback.ToString());
+        return ipAddress => $"http://{ipAddress}:{httpPort}/";
+    }
 
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
-            .Select(s =>
-            {
-                var uriString = httpPort is not null ? $"http://{s}:{httpPort}/" : $"http://{s}/";
-                if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
-                {
-                    return uri;
-                }
+    // Delegate that matches the signature of GetAllNetworkInterfaces
+    public delegate NetworkInterface[] GetAllNetworkInterfacesDelegate();
path: src/dotnet-interactive/Http/HttpProbingSettings.cs
user: login: jonsequitur
body: The custom delegate isn't needed here since it's such a simple type, e.g. it has a single use and isn't a complex enough delegate type to provide some self-documentation benefits from named parameters, etc. 

So it's preferable to use `Func<NetworkInterface[]>`. We're generally trying to reduce the number of types on the way to GA.
created_at: 2025-04-14T15:15:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -292,6 +303,7 @@ Command StdIO()
                 defaultKernelOption,
                 httpPortRangeOption,
                 httpPortOption,
+                httpLocalOnlyOption,
path: src/dotnet-interactive/CommandLine/CommandLineParser.cs
user: login: jonsequitur
body: Are you actually using this in `stdio` mode?

We're planning to remove the HTTP options from this mode entirely.
created_at: 2025-04-14T15:39:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,53 +9,49 @@
 
 namespace Microsoft.DotNet.Interactive.Http;
 
-internal class HttpProbingSettings
+public class HttpProbingSettings
 {
-    public Uri[] AddressList { get; private set; }
+    public IEnumerable<string> AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? httpPort, bool httpLocalOnly)
     {
+        HashSet<string> ipAddress = [IPAddress.Loopback.ToString()];
+
+        if (!httpLocalOnly)
+        {
+            ipAddress = GetAllNetworkInterfaces()
+                .Where(ni => ni.OperationalStatus == OperationalStatus.Up)
+                .SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
+                .Select(x => x.Address.ToString())
+                .Concat(ipAddress)
+                .ToHashSet();
+        }
+
+        var uriAddresses = ipAddress
+            .Select(AddHttpPort(httpPort));
+
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = uriAddresses
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Func<string, string> AddHttpPort(int? httpPort)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
-            {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
-                {
-
-                    if (ip != IPAddress.Loopback.ToString())
-                    {
-                        sources.Add(ip);
-                    }
-                }
-            }
-        }
+        if (httpPort is null)
+            return ipAddress => $"http://{ipAddress}/";
 
-        sources.Add(IPAddress.Loopback.ToString());
+        return ipAddress => $"http://{ipAddress}:{httpPort}/";
+    }
 
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
-            .Select(s =>
-            {
-                var uriString = httpPort is not null ? $"http://{s}:{httpPort}/" : $"http://{s}/";
-                if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
-                {
-                    return uri;
-                }
+    // Delegate that matches the signature of GetAllNetworkInterfaces
+    public delegate NetworkInterface[] GetAllNetworkInterfacesDelegate();
 
-                return null;
-            })
-            .Where(u => u is not null)
-            .ToArray();
+    // Property to replace the implementation for testing
+    public static GetAllNetworkInterfacesDelegate GetAllNetworkInterfacesImpl { get; set; } = NetworkInterface.GetAllNetworkInterfaces;
path: src/dotnet-interactive/Http/HttpProbingSettings.cs
user: login: jonsequitur
body: Rather than introduce static mutable state here, I'd recommend passing this as a parameter to `Create`.


created_at: 2025-04-14T15:40:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -292,6 +303,7 @@ Command StdIO()
                 defaultKernelOption,
                 httpPortRangeOption,
                 httpPortOption,
+                httpLocalOnlyOption,
path: src/dotnet-interactive/CommandLine/CommandLineParser.cs
user: login: Luiz-Ossinho
body: Nope.

Removing it...
created_at: 2025-04-14T18:54:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,53 +9,49 @@
 
 namespace Microsoft.DotNet.Interactive.Http;
 
-internal class HttpProbingSettings
+public class HttpProbingSettings
 {
-    public Uri[] AddressList { get; private set; }
+    public IEnumerable<string> AddressList { get; private set; }
 
-    public static HttpProbingSettings Create(int? port)
+    public static HttpProbingSettings Create(int? httpPort, bool httpLocalOnly)
     {
+        HashSet<string> ipAddress = [IPAddress.Loopback.ToString()];
+
+        if (!httpLocalOnly)
+        {
+            ipAddress = GetAllNetworkInterfaces()
+                .Where(ni => ni.OperationalStatus == OperationalStatus.Up)
+                .SelectMany(ni => ni.GetIPProperties().UnicastAddresses)
+                .Select(x => x.Address.ToString())
+                .Concat(ipAddress)
+                .ToHashSet();
+        }
+
+        var uriAddresses = ipAddress
+            .Select(AddHttpPort(httpPort));
+
         return new HttpProbingSettings
         {
-            AddressList = GetProbingAddressList(port)
+            AddressList = uriAddresses
         };
     }
 
-    private static Uri[] GetProbingAddressList(int? httpPort)
+    private static Func<string, string> AddHttpPort(int? httpPort)
     {
-        var sources = new List<string>();
-        foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
-        {
-            if (ni.OperationalStatus == OperationalStatus.Up)
-            {
-                foreach (var ip in ni.GetIPProperties().UnicastAddresses.Select(a => a.Address.ToString()))
-                {
-
-                    if (ip != IPAddress.Loopback.ToString())
-                    {
-                        sources.Add(ip);
-                    }
-                }
-            }
-        }
+        if (httpPort is null)
+            return ipAddress => $"http://{ipAddress}/";
 
-        sources.Add(IPAddress.Loopback.ToString());
+        return ipAddress => $"http://{ipAddress}:{httpPort}/";
+    }
 
-        var addresses = sources
-            .Where(s => !string.IsNullOrWhiteSpace(s))
-            .Select(s =>
-            {
-                var uriString = httpPort is not null ? $"http://{s}:{httpPort}/" : $"http://{s}/";
-                if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
-                {
-                    return uri;
-                }
+    // Delegate that matches the signature of GetAllNetworkInterfaces
+    public delegate NetworkInterface[] GetAllNetworkInterfacesDelegate();
 
-                return null;
-            })
-            .Where(u => u is not null)
-            .ToArray();
+    // Property to replace the implementation for testing
+    public static GetAllNetworkInterfacesDelegate GetAllNetworkInterfacesImpl { get; set; } = NetworkInterface.GetAllNetworkInterfaces;
path: src/dotnet-interactive/Http/HttpProbingSettings.cs
user: login: Luiz-Ossinho
body: Storing the Func in the StartupOptions class was the only way I found where I could both  have testable assertions and change its behaviour according to the flag.

Let me know if you meant something else.
created_at: 2025-04-14T18:59:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: Luiz-Ossinho
created_at: 2025-04-10T05:47:27Z
body: > @Luiz-Ossinho please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
> 
> ```
> @dotnet-policy-service agree [company="{your company}"]
> ```
> 
> > Options:
> > 
> > * (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
> > 
> > ```
> > @dotnet-policy-service agree
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > 
> > * (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
> > 
> > ```
> > @dotnet-policy-service agree company="Microsoft"
> > ```
> 
> Contributor License Agreement

@dotnet-policy-service agree
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-14T03:50:16Z
body: Im changing the [entire block around AddressList](https://github.com/dotnet/interactive/pull/3910/files#diff-1d1e7e86074064fc1b7ec38f9292a98a467345b0be8681b4a3b3fcd98a2afe33) to be more fluent, and refactoring code that references it to accept `IEnumerable<string>` instead of `Uri[]`.
I believe this improves readability, and perfomance to a lesser extent.

Every time this property was referenced, it used `Select(x => x.AbsoluteUri)`, making it pointless to be in an `Uri` instance created with `UriKind.Absolute` anyway.

I removed the null or empty check when dealing with the address of network interfaces because:
```
The Address property of a UnicastIPAddressInformation object is of type IPAddress, which is a non-nullable reference type in this context.
When you call ToString() on an IPAddress object, it will always return a formatted string representation of the IP address (like "192.168.1.1" for IPv4 or "2001:db8::1" for IPv6). The IPAddress.ToString() method is implemented to always return a valid string representation.

The only ways this query could fail would be:
- If GetAllNetworkInterfaces() throws an exception (e.g., due to insufficient permissions)
- If any of the network interfaces' GetIPProperties() throws an exception
- If access to UnicastAddresses throws an exception

But in terms of the values contained in the resulting IEnumerable<string>, you will not get null or empty string items. The collection itself could be empty if there are no network interfaces or no unicast addresses, but the individual items will always be valid IP address strings.
```
As Im no longer working with Uri, it made no sense to use `Uri.TryCreate()`.
I thought about adding`.Where(uriAddress => Uri.IsWellFormedUriString(uriAddress, UriKind.Absolute))` in its place, but it seemed weird to verify the validity of a output that is hardcoded, in this case, either `"http://{ipAddress}:{httpPort}/"` or `"http://{ipAddress}/"`, considering that the ipAddress itself is a valid, as previously justifyied.

In that same spirit, I also went and
- implemented the [CA1837](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1837) suggestion at `HttpApiBootstrapper.cs, line 15`
- changed interpolated the values directly instead of using an expensive ` .Replace()`.
- changed the string building at `HttpApiBootstrapper.cs, line 109` to a simpler `JsonSerializer.Serialize(probingUris)`
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-14T03:53:33Z
body: I was wondering why some tests use a common TestConsole while others create a new one:
```
// CommandLineParserTests.cs 

[Fact]
public async Task It_parses_log_output_directory()
{
  var logPath = new DirectoryInfo(Path.GetTempPath());

  await _parser.InvokeAsync($"jupyter --log-path {logPath} {_connectionFile}", _console);

  _startOptions
      .LogPath
      .FullName
      .Should()
      .Be(logPath.FullName);
}
  
[Fact]
public async Task jupyter_command_returns_error_if_connection_file_path_does_not_exist()
{
    var expected = "not_exist.json";

    var testConsole = new TestConsole();
    await _parser.InvokeAsync($"jupyter {expected}", testConsole);

    testConsole.Error.ToString().Should().ContainAll("File does not exist", "not_exist.json");
}
```
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-14T04:18:18Z
body: My build is failing due to: `Error reading resource 'Microsoft.DotNet.Interactive.App.resources.dotnet-interactive.js' -- 'Could not find a part of the path 'C:\[REDACTED]\ms-dotnet-interactive-issue3903\src\polyglot-notebooks-browser\dist\dotnet-interactive.js'.'`

![image](https://github.com/user-attachments/assets/63df9d9e-86b8-4064-8b27-7dc7769ff6c9)

But I think it wont be a problem, thanks to the CI pipeline
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-14T15:01:29Z
body: > My build is failing due to: `Error reading resource 'Microsoft.DotNet.Interactive.App.resources.dotnet-interactive.js' -- 'Could not find a part of the path 'C:\[REDACTED]\ms-dotnet-interactive-issue3903\src\polyglot-notebooks-browser\dist\dotnet-interactive.js'.'`
> 
> ![image](https://private-user-images.githubusercontent.com/73566627/433218851-63df9d9e-86b8-4064-8b27-7dc7769ff6c9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ2NDMwOTksIm5iZiI6MTc0NDY0Mjc5OSwicGF0aCI6Ii83MzU2NjYyNy80MzMyMTg4NTEtNjNkZjlkOWUtODZiOC00MDY0LThiMjctN2RjNzc2OWZmNmM5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA0MTQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNDE0VDE0NTk1OVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWU1NDAxNTNmZmNlMzQwNWFjNjJmN2RiZTczZjRiOTJlZDE4YjA0ZGU1YTk2OWY0MDZiZmU3M2I2MzIxNTA4ZTMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._6DHfWXUurmf8ocKXQzIj71INMujCT5dwg_UM4Y1TvE)
> 
> But I think it wont be a problem, thanks to the CI pipeline

You can fix this by running `ensure-symlinks.ps1` as described in the developer guide: https://github.com/dotnet/interactive/blob/570b9b578ced4e9d65863ea7121476d3e2a45f9d/DEVELOPER-GUIDE.md#L97
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-14T15:44:46Z
body: > I was wondering why some tests use a common TestConsole while others create a new one:
> 
> ```
> // CommandLineParserTests.cs 
> 
> [Fact]
> public async Task It_parses_log_output_directory()
> {
>   var logPath = new DirectoryInfo(Path.GetTempPath());
> 
>   await _parser.InvokeAsync($"jupyter --log-path {logPath} {_connectionFile}", _console);
> 
>   _startOptions
>       .LogPath
>       .FullName
>       .Should()
>       .Be(logPath.FullName);
> }
>   
> [Fact]
> public async Task jupyter_command_returns_error_if_connection_file_path_does_not_exist()
> {
>     var expected = "not_exist.json";
> 
>     var testConsole = new TestConsole();
>     await _parser.InvokeAsync($"jupyter {expected}", testConsole);
> 
>     testConsole.Error.ToString().Should().ContainAll("File does not exist", "not_exist.json");
> }
> ```

I don't know that there's any particular reason. 

Using local variables rather than fields in a test does make it a little easier to understand what the test is doing, assuming the code is concise like this example is. Also, in test frameworks that reuse class instances it can help reduce shared state flakiness between tests. (Xunit doesn't have this problem.)
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-14T15:52:44Z
body: > refactoring code that references it to accept IEnumerable<string> instead of Uri[] 
> I believe this improves readability, and perfomance to a lesser extent.

The refactoring makes sense and I didn't spot any issues. On this one point, there are pitfalls with using `IEnumerable<T>`:

* Deferred execution means the values can change on subsequent calls, depending on what the caller passed, and sometimes surprising bugs can hide there. The array (or, e.g. `IReadOnlyList<T>` is more constrained and safer.
* `IEnumerable` forces people to use an iterator (e.g. a `foreach` loop), whereas arrays and lists allow `for`, which saves an allocation.

I don't think either of these issues is critical here since this is only evaluated once at startup, but they're good general design considerations.


author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-15T00:32:52Z
body: The windows build failed with:


---
```
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
Blame: Attaching crash dump utility to process testhost (5776).
The active test run was aborted. Reason: Test host process crashed
Data collector 'Blame' message: The specified inactivity time of 10 minutes has elapsed. Collecting hang dumps from testhost and its child processes.
Data collector 'Blame' message: Dumping 5776 - testhost.
Results File: D:\a\_work\1\s\artifacts\TestResults\Release\cloudtest_38ff2a48c000004_2025-04-14_20_21_52.trx

Passed!  - Failed:     0, Passed:     6, Skipped:     0, Total:     6, Duration: 3 s - Microsoft.DotNet.Interactive.Tests.dll (net9.0)
Test Run Aborted.

The active Test Run was aborted because the host process exited unexpectedly. Please inspect the call stack above, if available, to get more information about where the exception originated from.

The test running when the crash occurred: 
Microsoft.DotNet.Interactive.Tests.MultipleInputsWithinMagicCommandsTests.When_multiple_inputs_are_enabled_then_RequestInput_is_not_sent_for_a_magic_command_that_uses_custom_binding

This test may, or may not be the source of the crash.
```
---

I have no idea why this happend. 
I know all code is conected but i believe my changes shouldnt fail that test.
Also, it runs on my windows machine:
![image](https://github.com/user-attachments/assets/0d4d2857-c698-4831-b47e-5d70ebbf0706)

I dont know if the process crashed due to a timeout, or it crashed and then timed-out.


author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-15T01:06:20Z
body: This isn't related to your changes.

We have an occasional flakiness in the test infrastructure and when it crashes it looks like this. It leads to a deadlock and the forced timeout is what crashes the test runner. 

I requeued it.
author_association: CONTRIBUTOR
user: login: Luiz-Ossinho
created_at: 2025-04-15T15:38:56Z
body: - The pipeline has finished successfully
- The feature was implemented following recomendations and guidelines
- Teste coverage was added

I believe this is ready for review/merge. Let me know if there is anything else that needs attention.
author_association: CONTRIBUTOR
user: login: jonsequitur
created_at: 2025-04-15T16:53:28Z
body: Thanks @Luiz-Ossinho!
author_association: CONTRIBUTOR