fix(launcher): refine console host display

This commit is contained in:
lc6464 2026-04-14 13:35:48 +08:00
parent d4d652b455
commit 93bf871bd2
No known key found for this signature in database
GPG key ID: 53C61B42FEC71D6D
3 changed files with 249 additions and 67 deletions

View file

@ -139,45 +139,105 @@ func advertiseIPForWildcardBindHosts(bindHosts []string) string {
return wildcardAdvertiseIP(bindHosts, utils.GetLocalIPv4(), utils.GetLocalIPv6()) return wildcardAdvertiseIP(bindHosts, utils.GetLocalIPv4(), utils.GetLocalIPv6())
} }
func launcherConsoleHosts(bindHosts []string, probeHost string) []string { func appendLauncherConsoleHostList(hosts []string, seen map[string]struct{}, values []string) []string {
hosts := make([]string, 0, 6) for _, value := range values {
seen := make(map[string]struct{}, 6) hosts = appendUniqueHost(hosts, seen, value)
}
return hosts
}
hosts = appendUniqueHost(hosts, seen, probeHost) func isConsoleDisplayGlobalIPv6(ip net.IP) bool {
if ip == nil || ip.IsLoopback() || ip.To4() != nil {
return false
}
ip = ip.To16()
if ip == nil {
return false
}
return ip[0]&0xe0 == 0x20
}
for _, bindHost := range bindHosts { func launcherConsoleHostsWithLocalAddrs(
switch { hostInput string,
case netbind.IsUnspecifiedHost(bindHost): public bool,
if ip := net.ParseIP(strings.Trim(bindHost, "[]")); ip != nil && ip.To4() != nil { ipv4s []string,
hosts = appendUniqueHost(hosts, seen, "127.0.0.1") globalIPv6s []string,
} else { ) []string {
hosts = appendUniqueHost(hosts, seen, "::1") hosts := make([]string, 0, 8)
} seen := make(map[string]struct{}, 8)
case netbind.IsLoopbackHost(bindHost):
hosts = appendUniqueHost(hosts, seen, "localhost") hosts = appendUniqueHost(hosts, seen, "localhost")
if ip := net.ParseIP(strings.Trim(bindHost, "[]")); ip != nil {
if ip.To4() != nil { normalizedHostInput := strings.TrimSpace(hostInput)
hosts = appendUniqueHost(hosts, seen, "127.0.0.1") if normalizedHostInput == "" {
} else { if public {
hosts = appendUniqueHost(hosts, seen, "::1") hosts = appendLauncherConsoleHostList(hosts, seen, globalIPv6s)
} hosts = appendLauncherConsoleHostList(hosts, seen, ipv4s)
} }
default: return hosts
hosts = appendUniqueHost(hosts, seen, bindHost) }
hasStar := false
hasIPv4Any := false
hasIPv6Any := false
for _, token := range strings.Split(normalizedHostInput, ",") {
switch strings.TrimSpace(token) {
case "*":
hasStar = true
case "0.0.0.0":
hasIPv4Any = true
case "::":
hasIPv6Any = true
} }
} }
if hasWildcardBindHosts(bindHosts) { if hasStar {
hosts = appendUniqueHost(hosts, seen, "localhost") hosts = appendLauncherConsoleHostList(hosts, seen, globalIPv6s)
hosts = appendUniqueHost(hosts, seen, "::1") hosts = appendLauncherConsoleHostList(hosts, seen, ipv4s)
hosts = appendUniqueHost(hosts, seen, "127.0.0.1") return hosts
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv6()) }
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv4())
for _, token := range strings.Split(normalizedHostInput, ",") {
token = strings.TrimSpace(token)
if token == "" || strings.EqualFold(token, "localhost") || netbind.IsLoopbackHost(token) {
continue
}
ip := net.ParseIP(strings.Trim(token, "[]"))
switch {
case token == "::":
hosts = appendLauncherConsoleHostList(hosts, seen, globalIPv6s)
case token == "0.0.0.0":
hosts = appendLauncherConsoleHostList(hosts, seen, ipv4s)
case ip != nil && ip.To4() != nil:
if hasIPv4Any {
continue
}
hosts = appendUniqueHost(hosts, seen, ip.String())
case ip != nil:
if hasIPv6Any {
continue
}
if isConsoleDisplayGlobalIPv6(ip) {
hosts = appendUniqueHost(hosts, seen, ip.String())
}
default:
hosts = appendUniqueHost(hosts, seen, token)
}
} }
return hosts return hosts
} }
func launcherConsoleHosts(_ []string, hostInput string, public bool) []string {
return launcherConsoleHostsWithLocalAddrs(
hostInput,
public,
utils.GetLocalIPv4s(),
utils.GetGlobalIPv6s(),
)
}
func firstNonEmpty(values ...string) string { func firstNonEmpty(values ...string) string {
for _, value := range values { for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
@ -443,7 +503,7 @@ func main() {
// Print startup banner and token (console mode only). // Print startup banner and token (console mode only).
if enableConsole || debug { if enableConsole || debug {
consoleHosts := launcherConsoleHosts(openResult.BindHosts, openResult.ProbeHost) consoleHosts := launcherConsoleHosts(openResult.BindHosts, hostInput, effectivePublic)
fmt.Print(utils.Banner) fmt.Print(utils.Banner)
fmt.Println() fmt.Println()

View file

@ -7,6 +7,7 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"testing" "testing"
"time" "time"
@ -123,27 +124,100 @@ func TestResolveLauncherHostInput(t *testing.T) {
} }
func TestLauncherConsoleHosts(t *testing.T) { func TestLauncherConsoleHosts(t *testing.T) {
t.Run("wildcard exposes local loopback hints", func(t *testing.T) { t.Run("default loopback shows localhost only", func(t *testing.T) {
hosts := launcherConsoleHosts([]string{"::"}, netbind.ResolveAdaptiveLoopbackHost()) hosts := launcherConsoleHostsWithLocalAddrs(
seen := make(map[string]bool, len(hosts)) "",
for _, host := range hosts { false,
seen[host] = true []string{"192.168.1.2", "10.0.0.8"},
} []string{"2001:db8::1", "2001:db8::2"},
if !seen["localhost"] { )
t.Fatalf("expected localhost in %#v", hosts) want := []string{"localhost"}
} if strings.Join(hosts, ",") != strings.Join(want, ",") {
if !seen["::1"] { t.Fatalf("hosts = %#v, want %#v", hosts, want)
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) { t.Run("explicit loopback hosts collapse to localhost", func(t *testing.T) {
hosts := launcherConsoleHosts([]string{"::1"}, "::1") tests := []struct {
if len(hosts) < 1 || hosts[0] != "::1" { name string
t.Fatalf("hosts = %#v, want probe host first", hosts) hostInput string
}{
{name: "ipv6 loopback", hostInput: "::1"},
{name: "ipv4 loopback", hostInput: "127.0.0.1"},
{name: "localhost", hostInput: "localhost"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hosts := launcherConsoleHostsWithLocalAddrs(
tt.hostInput,
false,
[]string{"192.168.1.2", "10.0.0.8"},
[]string{"2001:db8::1", "2001:db8::2"},
)
want := []string{"localhost"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
}
})
}
})
t.Run("public wildcard shows localhost then ipv6 and ipv4", func(t *testing.T) {
hosts := launcherConsoleHostsWithLocalAddrs(
"",
true,
[]string{"192.168.1.2", "10.0.0.8"},
[]string{"2001:db8::1", "2001:db8::2"},
)
want := []string{"localhost", "2001:db8::1", "2001:db8::2", "192.168.1.2", "10.0.0.8"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
}
})
t.Run("explicit ipv6 any shows localhost then ipv6 variants", func(t *testing.T) {
hosts := launcherConsoleHostsWithLocalAddrs(
"::",
false,
[]string{"192.168.1.2", "10.0.0.8"},
[]string{"2001:db8::1", "2001:db8::2"},
)
want := []string{"localhost", "2001:db8::1", "2001:db8::2"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
}
for _, host := range hosts {
if host == "::1" || host == "127.0.0.1" || strings.HasPrefix(strings.ToLower(host), "fe80:") {
t.Fatalf("hosts = %#v, loopback IPs must not be displayed", hosts)
}
}
})
t.Run("explicit ipv4 any shows localhost then lan ipv4", func(t *testing.T) {
hosts := launcherConsoleHostsWithLocalAddrs(
"0.0.0.0",
false,
[]string{"192.168.1.2", "10.0.0.8"},
[]string{"2001:db8::1", "2001:db8::2"},
)
want := []string{"localhost", "192.168.1.2", "10.0.0.8"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
}
})
t.Run("explicit multi-address binding shows all exact ipv4 and global ipv6 addresses", func(t *testing.T) {
hosts := launcherConsoleHostsWithLocalAddrs(
"192.168.1.2,10.0.0.8,2001:db8::1,2001:db8::2,fe80::1",
false,
[]string{"192.168.1.2", "10.0.0.8"},
[]string{"2001:db8::1", "2001:db8::2"},
)
want := []string{"localhost", "192.168.1.2", "10.0.0.8", "2001:db8::1", "2001:db8::2"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
} }
}) })
} }

View file

@ -7,6 +7,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
@ -54,41 +55,88 @@ func FindPicoclawBinary() string {
return "picoclaw" return "picoclaw"
} }
// GetLocalIPv4 returns a non-loopback local IPv4 address. func appendUniqueIP(addrs []string, seen map[string]struct{}, value string) []string {
func GetLocalIPv4() string { value = strings.TrimSpace(value)
addrs, err := net.InterfaceAddrs() if value == "" {
if err != nil { return addrs
return ""
} }
for _, a := range addrs { if _, ok := seen[value]; ok {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { return addrs
return ipnet.IP.String()
}
} }
return "" seen[value] = struct{}{}
return append(addrs, value)
} }
// GetLocalIPv6 returns a non-loopback local IPv6 address. // GetLocalIPv4s returns all non-loopback local IPv4 addresses.
func GetLocalIPv6() string { func GetLocalIPv4s() []string {
addrs, err := net.InterfaceAddrs() addrs, err := net.InterfaceAddrs()
if err != nil { if err != nil {
return "" return nil
} }
results := make([]string, 0, 4)
seen := make(map[string]struct{}, 4)
for _, a := range addrs {
ipnet, ok := a.(*net.IPNet)
if !ok || ipnet.IP == nil || ipnet.IP.IsLoopback() {
continue
}
if ip4 := ipnet.IP.To4(); ip4 != nil {
results = appendUniqueIP(results, seen, ip4.String())
}
}
return results
}
func isDisplayGlobalIPv6(ip net.IP) bool {
if ip == nil || ip.IsLoopback() || ip.To4() != nil {
return false
}
ip = ip.To16()
if ip == nil {
return false
}
// Only show IPv6 global unicast addresses in 2000::/3.
return ip[0]&0xe0 == 0x20
}
// GetGlobalIPv6s returns all IPv6 global unicast addresses.
func GetGlobalIPv6s() []string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return nil
}
results := make([]string, 0, 4)
seen := make(map[string]struct{}, 4)
for _, a := range addrs { for _, a := range addrs {
ipnet, ok := a.(*net.IPNet) ipnet, ok := a.(*net.IPNet)
if !ok || ipnet.IP == nil { if !ok || ipnet.IP == nil {
continue continue
} }
ip := ipnet.IP ip := ipnet.IP
if ip.IsLoopback() || ip.To4() != nil { if !isDisplayGlobalIPv6(ip) {
continue continue
} }
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { results = appendUniqueIP(results, seen, ip.String())
continue
}
return ip.String()
} }
return "" return results
}
// GetLocalIPv4 returns the first non-loopback local IPv4 address.
func GetLocalIPv4() string {
addrs := GetLocalIPv4s()
if len(addrs) == 0 {
return ""
}
return addrs[0]
}
// GetLocalIPv6 returns the first IPv6 global unicast address.
func GetLocalIPv6() string {
addrs := GetGlobalIPv6s()
if len(addrs) == 0 {
return ""
}
return addrs[0]
} }
// GetLocalIP returns a non-loopback local IPv4 address for backward compatibility. // GetLocalIP returns a non-loopback local IPv4 address for backward compatibility.