fix(host): align launcher and gateway host normalization semantics

This commit is contained in:
lc6464
2026-04-13 21:33:22 +08:00
parent 4e977367c2
commit 448027c02a
11 changed files with 380 additions and 22 deletions
+13 -2
View File
@@ -731,8 +731,19 @@ func (h *Handler) startGatewayLocked(initialStatus string, existingPid int) (int
if h.configPath != "" {
cmd.Env = append(cmd.Env, config.EnvConfig+"="+h.configPath)
}
if host := h.gatewayHostOverride(); host != "" {
cmd.Env = append(cmd.Env, config.EnvGatewayHost+"="+host)
gatewayHostOverride := h.gatewayHostOverrideForConfig(cfg)
if h.serverHostExplicit && gatewayHostOverride == "" {
logger.WarnC(
"gateway",
fmt.Sprintf(
"Explicit launcher host %q was not forwarded to gateway because configured gateway host is %q; gateway keeps original bind host",
strings.TrimSpace(h.serverHost),
strings.TrimSpace(cfg.Gateway.Host),
),
)
}
if gatewayHostOverride != "" {
cmd.Env = append(cmd.Env, config.EnvGatewayHost+"="+gatewayHostOverride)
}
stdoutPipe, err := cmd.StdoutPipe()
+105 -9
View File
@@ -6,10 +6,76 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"github.com/sipeed/picoclaw/pkg/config"
)
var (
adaptiveLoopbackHostOnce sync.Once
adaptiveLoopbackHost string
)
func selectAdaptiveLoopbackHost(hasIPv4, hasIPv6 bool) string {
switch {
case hasIPv4 && hasIPv6:
return "localhost"
case hasIPv6:
return "::1"
case hasIPv4:
return "127.0.0.1"
default:
return "127.0.0.1"
}
}
func isLoopbackEquivalentHost(host string) bool {
host = strings.TrimSpace(host)
if host == "" {
return false
}
if strings.EqualFold(host, "localhost") {
return true
}
trimmed := strings.Trim(host, "[]")
ip := net.ParseIP(trimmed)
return ip != nil && ip.IsLoopback()
}
func resolveAdaptiveLoopbackHost() string {
adaptiveLoopbackHostOnce.Do(func() {
ips, err := net.LookupIP("localhost")
if err != nil {
adaptiveLoopbackHost = selectAdaptiveLoopbackHost(false, false)
return
}
hasIPv4 := false
hasIPv6 := false
for _, ip := range ips {
if ip == nil {
continue
}
if ip.To4() != nil {
hasIPv4 = true
continue
}
hasIPv6 = true
}
adaptiveLoopbackHost = selectAdaptiveLoopbackHost(hasIPv4, hasIPv6)
})
return adaptiveLoopbackHost
}
func resolveDefaultLoopbackHost() string {
return resolveAdaptiveLoopbackHost()
}
func resolveLocalhostLoopbackHost() string {
return resolveAdaptiveLoopbackHost()
}
func (h *Handler) effectiveLauncherPublic() bool {
if h.serverHostExplicit {
// -host takes precedence over -public and launcher-config public setting.
@@ -30,27 +96,33 @@ func (h *Handler) effectiveLauncherPublic() bool {
func canonicalLauncherBindHost(host string) string {
host = strings.TrimSpace(host)
if host == "" || strings.EqualFold(host, "localhost") {
return "127.0.0.1"
if host == "" {
return resolveDefaultLoopbackHost()
}
if strings.EqualFold(host, "localhost") {
return resolveLocalhostLoopbackHost()
}
return host
}
func (h *Handler) launcherAndGatewayBindHostsAligned() bool {
cfg, err := config.LoadConfig(h.configPath)
if err != nil || cfg == nil {
func (h *Handler) launcherAndGatewayBindHostsAligned(cfg *config.Config) bool {
if cfg == nil {
return false
}
// With -host specified, -public is ignored, so launcher's legacy bind host is loopback.
launcherHost := canonicalLauncherBindHost("127.0.0.1")
gatewayHost := canonicalLauncherBindHost(cfg.Gateway.Host)
if isLoopbackEquivalentHost(launcherHost) && isLoopbackEquivalentHost(gatewayHost) {
return true
}
return launcherHost == gatewayHost
}
func (h *Handler) gatewayHostOverride() string {
func (h *Handler) gatewayHostOverrideForConfig(cfg *config.Config) string {
if h.serverHostExplicit {
if h.launcherAndGatewayBindHostsAligned() {
if h.launcherAndGatewayBindHostsAligned(cfg) {
return strings.TrimSpace(h.serverHost)
}
return ""
@@ -62,8 +134,20 @@ func (h *Handler) gatewayHostOverride() string {
return ""
}
func (h *Handler) gatewayHostOverride() string {
if !h.serverHostExplicit {
return h.gatewayHostOverrideForConfig(nil)
}
cfg, err := config.LoadConfig(h.configPath)
if err != nil {
return ""
}
return h.gatewayHostOverrideForConfig(cfg)
}
func (h *Handler) effectiveGatewayBindHost(cfg *config.Config) string {
if override := h.gatewayHostOverride(); override != "" {
if override := h.gatewayHostOverrideForConfig(cfg); override != "" {
return override
}
if cfg == nil {
@@ -73,7 +157,19 @@ func (h *Handler) effectiveGatewayBindHost(cfg *config.Config) string {
}
func gatewayProbeHost(bindHost string) string {
if bindHost == "" || bindHost == "0.0.0.0" {
bindHost = strings.TrimSpace(bindHost)
if bindHost == "" {
return resolveDefaultLoopbackHost()
}
if strings.EqualFold(bindHost, "localhost") {
return resolveLocalhostLoopbackHost()
}
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 bindHost
+55
View File
@@ -63,12 +63,54 @@ func TestBuildWsURLUsesRequestHostWhenLauncherPublicSaved(t *testing.T) {
}
}
func TestSelectAdaptiveLoopbackHost(t *testing.T) {
tests := []struct {
name string
hasIPv4 bool
hasIPv6 bool
want string
}{
{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"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := selectAdaptiveLoopbackHost(tt.hasIPv4, tt.hasIPv6); got != tt.want {
t.Fatalf("selectAdaptiveLoopbackHost(%t, %t) = %q, want %q", tt.hasIPv4, tt.hasIPv6, got, tt.want)
}
})
}
}
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")
}
}
func TestGatewayProbeHostUsesPreferredLoopbackForEmptyBind(t *testing.T) {
want := resolveDefaultLoopbackHost()
if got := gatewayProbeHost(""); got != want {
t.Fatalf("gatewayProbeHost(empty) = %q, want %q", got, want)
}
}
func TestGatewayProbeHostUsesPreferredLoopbackForLocalhostBind(t *testing.T) {
want := resolveLocalhostLoopbackHost()
if got := gatewayProbeHost("localhost"); got != want {
t.Fatalf("gatewayProbeHost(localhost) = %q, want %q", got, want)
}
}
func TestGatewayProbeHostUsesLoopbackForIPv6WildcardBind(t *testing.T) {
if got := gatewayProbeHost("::"); got != "::1" {
t.Fatalf("gatewayProbeHost(::) = %q, want %q", got, "::1")
}
}
func TestGatewayProxyURLUsesConfiguredHost(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
h := NewHandler(configPath)
@@ -254,6 +296,19 @@ func TestGatewayHostOverrideWithExplicitHostAndAlignedGatewayHost(t *testing.T)
}
}
func TestGatewayHostOverrideWithExplicitHostAndLocalhostGatewayHost(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
writeGatewayHostConfig(t, configPath, "localhost")
h := NewHandler(configPath)
h.SetServerOptions(18800, false, false, nil)
h.SetServerBindHost("::", true)
if got := h.gatewayHostOverride(); got != "::" {
t.Fatalf("gatewayHostOverride() = %q, want %q", got, "::")
}
}
func TestGatewayHostOverrideWithExplicitHostAndMismatchedGatewayHost(t *testing.T) {
configPath := filepath.Join(t.TempDir(), "config.json")
writeGatewayHostConfig(t, configPath, "0.0.0.0")