fix(host): modernize default host selection order

This commit is contained in:
lc6464
2026-04-13 22:49:25 +08:00
parent 448027c02a
commit e7b3654313
13 changed files with 497 additions and 98 deletions
+1 -1
View File
@@ -262,7 +262,7 @@ func (h *Handler) getGatewayHealthForPidData(
host = gatewayProbeHost(h.effectiveGatewayBindHost(cfg))
}
if host == "" {
host = "127.0.0.1"
host = resolveDefaultLoopbackHost()
}
url := "http://" + net.JoinHostPort(host, strconv.Itoa(port)) + "/health"
+71 -29
View File
@@ -12,8 +12,11 @@ import (
)
var (
adaptiveLoopbackHostOnce sync.Once
adaptiveLoopbackHost string
adaptiveIPFamiliesOnce sync.Once
adaptiveHasIPv4 bool
adaptiveHasIPv6 bool
lookupLocalhostIPs = func() ([]net.IP, error) { return net.LookupIP("localhost") }
listInterfaceAddrs = net.InterfaceAddrs
)
func selectAdaptiveLoopbackHost(hasIPv4, hasIPv6 bool) string {
@@ -25,7 +28,20 @@ func selectAdaptiveLoopbackHost(hasIPv4, hasIPv6 bool) string {
case hasIPv4:
return "127.0.0.1"
default:
return "127.0.0.1"
return "localhost"
}
}
func selectAdaptiveAnyHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "::"
case hasIPv6:
return "::"
case hasIPv4:
return "0.0.0.0"
default:
return "::"
}
}
@@ -42,36 +58,61 @@ func isLoopbackEquivalentHost(host string) bool {
return ip != nil && ip.IsLoopback()
}
func resolveAdaptiveLoopbackHost() string {
adaptiveLoopbackHostOnce.Do(func() {
ips, err := net.LookupIP("localhost")
if err != nil {
adaptiveLoopbackHost = selectAdaptiveLoopbackHost(false, false)
func detectAdaptiveIPFamilies() (bool, bool) {
adaptiveIPFamiliesOnce.Do(func() {
if ips, err := lookupLocalhostIPs(); err == nil {
for _, ip := range ips {
if ip == nil {
continue
}
if ip.To4() != nil {
adaptiveHasIPv4 = true
continue
}
adaptiveHasIPv6 = true
}
}
if adaptiveHasIPv4 && adaptiveHasIPv6 {
return
}
hasIPv4 := false
hasIPv6 := false
for _, ip := range ips {
if ip == nil {
continue
if addrs, err := listInterfaceAddrs(); err == nil {
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok || ipnet.IP == nil {
continue
}
if ipnet.IP.To4() != nil {
adaptiveHasIPv4 = true
continue
}
adaptiveHasIPv6 = true
}
if ip.To4() != nil {
hasIPv4 = true
continue
}
hasIPv6 = true
}
adaptiveLoopbackHost = selectAdaptiveLoopbackHost(hasIPv4, hasIPv6)
})
return adaptiveLoopbackHost
return adaptiveHasIPv4, adaptiveHasIPv6
}
func resolveAdaptiveLoopbackHost() string {
hasIPv4, hasIPv6 := detectAdaptiveIPFamilies()
return selectAdaptiveLoopbackHost(hasIPv4, hasIPv6)
}
func resolveAdaptiveAnyHost() string {
hasIPv4, hasIPv6 := detectAdaptiveIPFamilies()
return selectAdaptiveAnyHost(hasIPv4, hasIPv6)
}
func resolveDefaultLoopbackHost() string {
return resolveAdaptiveLoopbackHost()
}
func resolveDefaultAnyHost() string {
return resolveAdaptiveAnyHost()
}
func resolveLocalhostLoopbackHost() string {
return resolveAdaptiveLoopbackHost()
}
@@ -102,6 +143,10 @@ func canonicalLauncherBindHost(host string) string {
if strings.EqualFold(host, "localhost") {
return resolveLocalhostLoopbackHost()
}
trimmed := strings.Trim(host, "[]")
if ip := net.ParseIP(trimmed); ip != nil && ip.IsUnspecified() {
return resolveDefaultAnyHost()
}
return host
}
@@ -110,8 +155,8 @@ func (h *Handler) launcherAndGatewayBindHostsAligned(cfg *config.Config) bool {
return false
}
// With -host specified, -public is ignored, so launcher's legacy bind host is loopback.
launcherHost := canonicalLauncherBindHost("127.0.0.1")
// With -host specified, -public is ignored, so launcher baseline bind host is loopback.
launcherHost := canonicalLauncherBindHost("")
gatewayHost := canonicalLauncherBindHost(cfg.Gateway.Host)
if isLoopbackEquivalentHost(launcherHost) && isLoopbackEquivalentHost(gatewayHost) {
return true
@@ -129,7 +174,7 @@ func (h *Handler) gatewayHostOverrideForConfig(cfg *config.Config) string {
}
if h.effectiveLauncherPublic() {
return "0.0.0.0"
return resolveDefaultAnyHost()
}
return ""
}
@@ -167,10 +212,7 @@ func gatewayProbeHost(bindHost string) string {
trimmed := strings.Trim(bindHost, "[]")
if ip := net.ParseIP(trimmed); ip != nil && ip.IsUnspecified() {
if ip.To4() == nil {
return "::1"
}
return "127.0.0.1"
return resolveDefaultLoopbackHost()
}
return bindHost
}
@@ -200,7 +242,7 @@ func requestHostName(r *http.Request) string {
if strings.TrimSpace(r.Host) != "" {
return r.Host
}
return "127.0.0.1"
return resolveDefaultLoopbackHost()
}
func requestWSScheme(r *http.Request) string {
+72 -11
View File
@@ -3,9 +3,11 @@ package api
import (
"crypto/tls"
"errors"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"sync"
"testing"
"time"
@@ -13,6 +15,12 @@ import (
"github.com/sipeed/picoclaw/web/backend/launcherconfig"
)
func resetAdaptiveIPFamiliesForTest() {
adaptiveIPFamiliesOnce = sync.Once{}
adaptiveHasIPv4 = false
adaptiveHasIPv6 = false
}
func TestGatewayHostOverrideUsesExplicitRuntimePublic(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
launcherPath := launcherconfig.PathForAppConfig(configPath)
@@ -26,8 +34,8 @@ func TestGatewayHostOverrideUsesExplicitRuntimePublic(t *testing.T) {
h := NewHandler(configPath)
h.SetServerOptions(18800, true, true, nil)
if got := h.gatewayHostOverride(); got != "0.0.0.0" {
t.Fatalf("gatewayHostOverride() = %q, want %q", got, "0.0.0.0")
if got := h.gatewayHostOverride(); got != resolveDefaultAnyHost() {
t.Fatalf("gatewayHostOverride() = %q, want %q", got, resolveDefaultAnyHost())
}
}
@@ -73,7 +81,7 @@ func TestSelectAdaptiveLoopbackHost(t *testing.T) {
{name: "dual stack prefers localhost", hasIPv4: true, hasIPv6: true, want: "localhost"},
{name: "ipv6 only", hasIPv4: false, hasIPv6: true, want: "::1"},
{name: "ipv4 only", hasIPv4: true, hasIPv6: false, want: "127.0.0.1"},
{name: "fallback", hasIPv4: false, hasIPv6: false, want: "127.0.0.1"},
{name: "fallback", hasIPv4: false, hasIPv6: false, want: "localhost"},
}
for _, tt := range tests {
@@ -85,9 +93,60 @@ func TestSelectAdaptiveLoopbackHost(t *testing.T) {
}
}
func TestSelectAdaptiveAnyHost(t *testing.T) {
tests := []struct {
name string
hasIPv4 bool
hasIPv6 bool
want string
}{
{name: "dual stack prefers ipv6 wildcard", hasIPv4: true, hasIPv6: true, want: "::"},
{name: "ipv6 only", hasIPv4: false, hasIPv6: true, want: "::"},
{name: "ipv4 only", hasIPv4: true, hasIPv6: false, want: "0.0.0.0"},
{name: "fallback", hasIPv4: false, hasIPv6: false, want: "::"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := selectAdaptiveAnyHost(tt.hasIPv4, tt.hasIPv6); got != tt.want {
t.Fatalf("selectAdaptiveAnyHost(%t, %t) = %q, want %q", tt.hasIPv4, tt.hasIPv6, got, tt.want)
}
})
}
}
func TestAdaptiveHostSelectionFallsBackToInterfaceAddrs(t *testing.T) {
oldLookup := lookupLocalhostIPs
oldList := listInterfaceAddrs
lookupLocalhostIPs = func() ([]net.IP, error) {
return nil, errors.New("lookup failed")
}
_, v4Net, err := net.ParseCIDR("192.0.2.10/24")
if err != nil {
t.Fatalf("ParseCIDR() error = %v", err)
}
listInterfaceAddrs = func() ([]net.Addr, error) {
return []net.Addr{v4Net}, nil
}
resetAdaptiveIPFamiliesForTest()
t.Cleanup(func() {
lookupLocalhostIPs = oldLookup
listInterfaceAddrs = oldList
resetAdaptiveIPFamiliesForTest()
})
if got := resolveDefaultAnyHost(); got != "0.0.0.0" {
t.Fatalf("resolveDefaultAnyHost() = %q, want %q", got, "0.0.0.0")
}
if got := resolveDefaultLoopbackHost(); got != "127.0.0.1" {
t.Fatalf("resolveDefaultLoopbackHost() = %q, want %q", got, "127.0.0.1")
}
}
func TestGatewayProbeHostUsesLoopbackForWildcardBind(t *testing.T) {
if got := gatewayProbeHost("0.0.0.0"); got != "127.0.0.1" {
t.Fatalf("gatewayProbeHost() = %q, want %q", got, "127.0.0.1")
want := resolveDefaultLoopbackHost()
if got := gatewayProbeHost("0.0.0.0"); got != want {
t.Fatalf("gatewayProbeHost() = %q, want %q", got, want)
}
}
@@ -106,8 +165,9 @@ func TestGatewayProbeHostUsesPreferredLoopbackForLocalhostBind(t *testing.T) {
}
func TestGatewayProbeHostUsesLoopbackForIPv6WildcardBind(t *testing.T) {
if got := gatewayProbeHost("::"); got != "::1" {
t.Fatalf("gatewayProbeHost(::) = %q, want %q", got, "::1")
want := resolveDefaultLoopbackHost()
if got := gatewayProbeHost("::"); got != want {
t.Fatalf("gatewayProbeHost(::) = %q, want %q", got, want)
}
}
@@ -179,8 +239,9 @@ func TestGetGatewayHealthUsesProbeHostForPublicLauncher(t *testing.T) {
_ = statusCode
_ = err
if requestedURL != "http://127.0.0.1:18791/health" {
t.Fatalf("health url = %q, want %q", requestedURL, "http://127.0.0.1:18791/health")
want := "http://" + net.JoinHostPort(resolveDefaultLoopbackHost(), "18791") + "/health"
if requestedURL != want {
t.Fatalf("health url = %q, want %q", requestedURL, want)
}
}
@@ -291,8 +352,8 @@ func TestGatewayHostOverrideWithExplicitHostAndAlignedGatewayHost(t *testing.T)
h.SetServerOptions(18800, false, false, nil)
h.SetServerBindHost("0.0.0.0", true)
if got := h.gatewayHostOverride(); got != "0.0.0.0" {
t.Fatalf("gatewayHostOverride() = %q, want %q", got, "0.0.0.0")
if got := h.gatewayHostOverride(); got != resolveDefaultAnyHost() {
t.Fatalf("gatewayHostOverride() = %q, want %q", got, resolveDefaultAnyHost())
}
}
+6 -5
View File
@@ -32,7 +32,7 @@ func NewHandler(configPath string) *Handler {
return &Handler{
configPath: configPath,
serverPort: launcherconfig.DefaultPort,
serverHost: "127.0.0.1",
serverHost: resolveDefaultLoopbackHost(),
oauthFlows: make(map[string]*oauthFlow),
oauthState: make(map[string]string),
weixinFlows: make(map[string]*weixinFlow),
@@ -45,9 +45,9 @@ func (h *Handler) SetServerOptions(port int, public bool, publicExplicit bool, a
h.serverPort = port
h.serverPublic = public
h.serverPublicExplicit = publicExplicit
h.serverHost = "127.0.0.1"
h.serverHost = resolveDefaultLoopbackHost()
if public {
h.serverHost = "0.0.0.0"
h.serverHost = resolveDefaultAnyHost()
}
h.serverHostExplicit = false
h.serverCIDRs = append([]string(nil), allowedCIDRs...)
@@ -58,12 +58,13 @@ func (h *Handler) SetServerOptions(port int, public bool, publicExplicit bool, a
func (h *Handler) SetServerBindHost(host string, explicit bool) {
host = strings.TrimSpace(host)
if host == "" {
host = "127.0.0.1"
host = resolveDefaultLoopbackHost()
if h.serverPublic {
host = "0.0.0.0"
host = resolveDefaultAnyHost()
}
explicit = false
}
host = canonicalLauncherBindHost(host)
h.serverHost = host
h.serverHostExplicit = explicit
+168 -24
View File
@@ -23,6 +23,7 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -46,6 +47,10 @@ const (
var (
appVersion = config.Version
launcherIPFamiliesOnce sync.Once
launcherHasIPv4 bool
launcherHasIPv6 bool
server *http.Server
serverAddr string
// browserLaunchURL is opened by openBrowser() (auto-open + tray "open console").
@@ -67,6 +72,103 @@ func dashboardTokenConfigHelpPath(source launcherconfig.DashboardTokenSource, la
return launcherPath
}
func detectLauncherIPFamilies() (bool, bool) {
launcherIPFamiliesOnce.Do(func() {
if ips, err := net.LookupIP("localhost"); err == nil {
for _, ip := range ips {
if ip == nil {
continue
}
if ip.To4() != nil {
launcherHasIPv4 = true
continue
}
launcherHasIPv6 = true
}
}
if launcherHasIPv4 && launcherHasIPv6 {
return
}
if addrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if !ok || ipnet.IP == nil {
continue
}
if ipnet.IP.To4() != nil {
launcherHasIPv4 = true
continue
}
launcherHasIPv6 = true
}
}
})
return launcherHasIPv4, launcherHasIPv6
}
func selectAdaptiveLauncherLoopbackHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "localhost"
case hasIPv6:
return "::1"
case hasIPv4:
return "127.0.0.1"
default:
return "localhost"
}
}
func selectAdaptiveLauncherAnyHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "::"
case hasIPv6:
return "::"
case hasIPv4:
return "0.0.0.0"
default:
return "::"
}
}
func resolveDefaultLauncherLoopbackHost() string {
hasIPv4, hasIPv6 := detectLauncherIPFamilies()
return selectAdaptiveLauncherLoopbackHost(hasIPv4, hasIPv6)
}
func resolveDefaultLauncherAnyHost() string {
hasIPv4, hasIPv6 := detectLauncherIPFamilies()
return selectAdaptiveLauncherAnyHost(hasIPv4, hasIPv6)
}
func resolveDefaultLauncherPrivateHost() string {
hasIPv4, hasIPv6 := detectLauncherIPFamilies()
if hasIPv4 && hasIPv6 {
// In dual-stack environments, use wildcard IPv6 bind so localhost can serve both families.
return selectAdaptiveLauncherAnyHost(hasIPv4, hasIPv6)
}
return selectAdaptiveLauncherLoopbackHost(hasIPv4, hasIPv6)
}
func normalizeLauncherSpecialHost(host string) string {
host = strings.TrimSpace(host)
if host == "" {
return host
}
if strings.EqualFold(host, "localhost") {
return resolveDefaultLauncherLoopbackHost()
}
trimmed := strings.Trim(host, "[]")
if ip := net.ParseIP(trimmed); ip != nil && ip.IsUnspecified() {
return resolveDefaultLauncherAnyHost()
}
return host
}
func resolveLauncherBindHost(
host string,
explicitHost bool,
@@ -79,25 +181,30 @@ func resolveLauncherBindHost(
return "", false, false, errors.New("host cannot be empty")
}
// When -host is specified, -public is ignored.
return host, false, true, nil
return normalizeLauncherSpecialHost(host), false, true, nil
}
envHost = strings.TrimSpace(envHost)
if envHost != "" {
// Environment host follows explicit override semantics.
return envHost, false, true, nil
return normalizeLauncherSpecialHost(envHost), false, true, nil
}
if effectivePublic {
return "0.0.0.0", true, false, nil
return resolveDefaultLauncherAnyHost(), true, false, nil
}
return "127.0.0.1", false, false, nil
return resolveDefaultLauncherPrivateHost(), false, false, nil
}
func isWildcardBindHost(host string) bool {
host = strings.TrimSpace(host)
return host == "0.0.0.0" || host == "::"
if host == "" {
return false
}
trimmed := strings.Trim(host, "[]")
ip := net.ParseIP(trimmed)
return ip != nil && ip.IsUnspecified()
}
func browserHostForLauncher(bindHost string) string {
@@ -109,20 +216,57 @@ func browserHostForLauncher(bindHost string) string {
}
func wildcardAdvertiseIP(bindHost, ipv4, ipv6 string) string {
switch strings.TrimSpace(bindHost) {
case "0.0.0.0":
return strings.TrimSpace(ipv4)
case "::":
return strings.TrimSpace(ipv6)
default:
if !isWildcardBindHost(bindHost) {
return ""
}
if v6 := strings.TrimSpace(ipv6); v6 != "" {
return v6
}
return strings.TrimSpace(ipv4)
}
func advertiseIPForWildcardBindHost(bindHost string) string {
return wildcardAdvertiseIP(bindHost, utils.GetLocalIPv4(), utils.GetLocalIPv6())
}
func appendUniqueHost(hosts []string, seen map[string]struct{}, host string) []string {
host = strings.TrimSpace(host)
if host == "" {
return hosts
}
key := strings.ToLower(host)
if _, ok := seen[key]; ok {
return hosts
}
seen[key] = struct{}{}
return append(hosts, host)
}
func launcherConsoleHosts(bindHost string, hostExplicit bool, effectivePublic bool) []string {
hosts := make([]string, 0, 6)
seen := make(map[string]struct{}, 6)
hosts = appendUniqueHost(hosts, seen, "localhost")
if isWildcardBindHost(bindHost) {
hosts = appendUniqueHost(hosts, seen, "::1")
hosts = appendUniqueHost(hosts, seen, "127.0.0.1")
if effectivePublic || hostExplicit {
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv6())
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv4())
}
return hosts
}
if hostExplicit {
hosts = appendUniqueHost(hosts, seen, bindHost)
}
return hosts
}
// maskSecret masks a secret for display. It always shows up to the first 3
// runes. The last 4 runes are only appended when at least 5 runes remain
// hidden in the middle (i.e. string length >= 12), so an 8-char minimum
@@ -144,7 +288,7 @@ func maskSecret(s string) string {
func main() {
port := flag.String("port", "18800", "Port to listen on")
host := flag.String("host", "", "Host to listen on (overrides -public when set)")
public := flag.Bool("public", false, "Listen on all interfaces (0.0.0.0) instead of localhost only")
public := flag.Bool("public", false, "Listen on all interfaces (dual-stack) instead of localhost only")
noBrowser = flag.Bool("no-browser", false, "Do not auto-open browser on startup")
lang := flag.String("lang", "", "Language: en (English) or zh (Chinese). Default: auto-detect from system locale")
console := flag.Bool("console", false, "Console mode, no GUI")
@@ -171,8 +315,8 @@ func main() {
os.Args[0],
)
fmt.Fprintf(os.Stderr, " Allow access from other devices on the local network\n")
fmt.Fprintf(os.Stderr, " %s -host 0.0.0.0 ./config.json\n", os.Args[0])
fmt.Fprintf(os.Stderr, " Bind launcher host explicitly (gateway forwarding follows compatibility rules)\n")
fmt.Fprintf(os.Stderr, " %s -host :: ./config.json\n", os.Args[0])
fmt.Fprintf(os.Stderr, " Bind launcher host explicitly (dual-stack normalization applies)\n")
fmt.Fprintf(os.Stderr, " %s -console -d ./config.json\n", os.Args[0])
fmt.Fprintf(os.Stderr, " Run in the terminal with debug logs enabled\n")
}
@@ -287,6 +431,12 @@ func main() {
logger.Fatalf("Invalid host %q: %v", *host, err)
}
effectiveAllowedCIDRs := append([]string(nil), launcherCfg.AllowedCIDRs...)
if len(effectiveAllowedCIDRs) == 0 && !effectivePublic && !hostExplicit && isWildcardBindHost(effectiveHost) {
effectiveAllowedCIDRs = []string{"127.0.0.1/32", "::1/128"}
logger.InfoC("web", "Applying loopback-only access policy for default dual-stack bind")
}
if !explicitHost && envHost != "" {
logger.InfoC("web", "Using launcher host from environment PICOCLAW_LAUNCHER_HOST")
}
@@ -349,14 +499,14 @@ func main() {
if _, err = apiHandler.EnsurePicoChannel(""); err != nil {
logger.ErrorC("web", fmt.Sprintf("Warning: failed to ensure pico channel on startup: %v", err))
}
apiHandler.SetServerOptions(portNum, effectivePublic, explicitPublic, launcherCfg.AllowedCIDRs)
apiHandler.SetServerOptions(portNum, effectivePublic, explicitPublic, effectiveAllowedCIDRs)
apiHandler.SetServerBindHost(effectiveHost, hostExplicit)
apiHandler.RegisterRoutes(mux)
// Frontend Embedded Assets
registerEmbedRoutes(mux)
accessControlledMux, err := middleware.IPAllowlist(launcherCfg.AllowedCIDRs, mux)
accessControlledMux, err := middleware.IPAllowlist(effectiveAllowedCIDRs, mux)
if err != nil {
logger.Fatalf("Invalid allowed CIDR configuration: %v", err)
}
@@ -381,14 +531,8 @@ func main() {
fmt.Println()
fmt.Println(" Open the following URL in your browser:")
fmt.Println()
fmt.Printf(" >> http://localhost:%s <<\n", effectivePort)
if isWildcardBindHost(effectiveHost) {
if ip := advertiseIPForWildcardBindHost(effectiveHost); ip != "" {
fmt.Printf(" >> http://%s <<\n", net.JoinHostPort(ip, effectivePort))
}
}
if hostExplicit {
fmt.Printf(" >> http://%s <<\n", net.JoinHostPort(browserHostForLauncher(effectiveHost), effectivePort))
for _, host := range launcherConsoleHosts(effectiveHost, hostExplicit, effectivePublic) {
fmt.Printf(" >> http://%s <<\n", net.JoinHostPort(host, effectivePort))
}
fmt.Println()
switch dashboardTokenSource {
+39 -7
View File
@@ -113,7 +113,7 @@ func TestResolveLauncherBindHost(t *testing.T) {
host: "0.0.0.0",
explicitHost: true,
effectivePub: true,
wantHost: "0.0.0.0",
wantHost: resolveDefaultLauncherAnyHost(),
wantPublic: false,
wantExplicit: true,
},
@@ -139,7 +139,7 @@ func TestResolveLauncherBindHost(t *testing.T) {
envHost: "0.0.0.0",
explicitHost: false,
effectivePub: true,
wantHost: "0.0.0.0",
wantHost: resolveDefaultLauncherAnyHost(),
wantPublic: false,
wantExplicit: true,
},
@@ -148,7 +148,7 @@ func TestResolveLauncherBindHost(t *testing.T) {
host: "",
explicitHost: false,
effectivePub: true,
wantHost: "0.0.0.0",
wantHost: resolveDefaultLauncherAnyHost(),
wantPublic: true,
wantExplicit: false,
},
@@ -157,7 +157,7 @@ func TestResolveLauncherBindHost(t *testing.T) {
host: "",
explicitHost: false,
effectivePub: false,
wantHost: "127.0.0.1",
wantHost: resolveDefaultLauncherPrivateHost(),
wantPublic: false,
wantExplicit: false,
},
@@ -190,6 +190,38 @@ func TestResolveLauncherBindHost(t *testing.T) {
}
}
func TestLauncherConsoleHosts(t *testing.T) {
t.Run("explicit wildcard dedupes localhost and includes loopback ipv6", func(t *testing.T) {
hosts := launcherConsoleHosts("0.0.0.0", true, false)
seen := make(map[string]bool, len(hosts))
for _, host := range hosts {
if seen[host] {
t.Fatalf("duplicate host %q in %#v", host, hosts)
}
seen[host] = true
}
if !seen["localhost"] {
t.Fatalf("expected localhost in %#v", hosts)
}
if !seen["::1"] {
t.Fatalf("expected ::1 in %#v", hosts)
}
if !seen["127.0.0.1"] {
t.Fatalf("expected 127.0.0.1 in %#v", hosts)
}
})
t.Run("explicit ipv6 host remains visible", func(t *testing.T) {
hosts := launcherConsoleHosts("::1", true, false)
if len(hosts) != 2 {
t.Fatalf("len(hosts) = %d, want 2 (%#v)", len(hosts), hosts)
}
if hosts[0] != "localhost" || hosts[1] != "::1" {
t.Fatalf("hosts = %#v, want [localhost ::1]", hosts)
}
})
}
func TestBrowserHostForLauncher(t *testing.T) {
if got := browserHostForLauncher("0.0.0.0"); got != "localhost" {
t.Fatalf("browserHostForLauncher(0.0.0.0) = %q, want %q", got, "localhost")
@@ -210,10 +242,10 @@ func TestWildcardAdvertiseIP(t *testing.T) {
ipv6 string
want string
}{
{name: "ipv4 wildcard uses ipv4", bindHost: "0.0.0.0", ipv4: "192.168.1.2", ipv6: "2001:db8::1", want: "192.168.1.2"},
{name: "ipv4 wildcard prefers ipv6 when available", bindHost: "0.0.0.0", ipv4: "192.168.1.2", ipv6: "2001:db8::1", want: "2001:db8::1"},
{name: "ipv6 wildcard uses ipv6", bindHost: "::", ipv4: "192.168.1.2", ipv6: "2001:db8::1", want: "2001:db8::1"},
{name: "ipv6 wildcard with no ipv6 address", bindHost: "::", ipv4: "192.168.1.2", ipv6: "", want: ""},
{name: "ipv4 wildcard with no ipv4 address", bindHost: "0.0.0.0", ipv4: "", ipv6: "2001:db8::1", want: ""},
{name: "ipv6 wildcard falls back to ipv4", bindHost: "::", ipv4: "192.168.1.2", ipv6: "", want: "192.168.1.2"},
{name: "ipv4 wildcard uses ipv6-only network", bindHost: "0.0.0.0", ipv4: "", ipv6: "2001:db8::1", want: "2001:db8::1"},
{name: "non wildcard does not advertise", bindHost: "127.0.0.1", ipv4: "192.168.1.2", ipv6: "2001:db8::1", want: ""},
}