fix(launcher): refine console host display

This commit is contained in:
lc6464
2026-04-14 13:35:48 +08:00
parent d4d652b455
commit 93bf871bd2
3 changed files with 249 additions and 67 deletions
+90 -30
View File
@@ -139,45 +139,105 @@ func advertiseIPForWildcardBindHosts(bindHosts []string) string {
return wildcardAdvertiseIP(bindHosts, utils.GetLocalIPv4(), utils.GetLocalIPv6())
}
func launcherConsoleHosts(bindHosts []string, probeHost string) []string {
hosts := make([]string, 0, 6)
seen := make(map[string]struct{}, 6)
func appendLauncherConsoleHostList(hosts []string, seen map[string]struct{}, values []string) []string {
for _, value := range values {
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 {
switch {
case netbind.IsUnspecifiedHost(bindHost):
if ip := net.ParseIP(strings.Trim(bindHost, "[]")); ip != nil && ip.To4() != nil {
hosts = appendUniqueHost(hosts, seen, "127.0.0.1")
} else {
hosts = appendUniqueHost(hosts, seen, "::1")
}
case netbind.IsLoopbackHost(bindHost):
hosts = appendUniqueHost(hosts, seen, "localhost")
if ip := net.ParseIP(strings.Trim(bindHost, "[]")); ip != nil {
if ip.To4() != nil {
hosts = appendUniqueHost(hosts, seen, "127.0.0.1")
} else {
hosts = appendUniqueHost(hosts, seen, "::1")
}
}
default:
hosts = appendUniqueHost(hosts, seen, bindHost)
func launcherConsoleHostsWithLocalAddrs(
hostInput string,
public bool,
ipv4s []string,
globalIPv6s []string,
) []string {
hosts := make([]string, 0, 8)
seen := make(map[string]struct{}, 8)
hosts = appendUniqueHost(hosts, seen, "localhost")
normalizedHostInput := strings.TrimSpace(hostInput)
if normalizedHostInput == "" {
if public {
hosts = appendLauncherConsoleHostList(hosts, seen, globalIPv6s)
hosts = appendLauncherConsoleHostList(hosts, seen, ipv4s)
}
return hosts
}
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) {
hosts = appendUniqueHost(hosts, seen, "localhost")
hosts = appendUniqueHost(hosts, seen, "::1")
hosts = appendUniqueHost(hosts, seen, "127.0.0.1")
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv6())
hosts = appendUniqueHost(hosts, seen, utils.GetLocalIPv4())
if hasStar {
hosts = appendLauncherConsoleHostList(hosts, seen, globalIPv6s)
hosts = appendLauncherConsoleHostList(hosts, seen, ipv4s)
return hosts
}
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
}
func launcherConsoleHosts(_ []string, hostInput string, public bool) []string {
return launcherConsoleHostsWithLocalAddrs(
hostInput,
public,
utils.GetLocalIPv4s(),
utils.GetGlobalIPv6s(),
)
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
@@ -443,7 +503,7 @@ func main() {
// Print startup banner and token (console mode only).
if enableConsole || debug {
consoleHosts := launcherConsoleHosts(openResult.BindHosts, openResult.ProbeHost)
consoleHosts := launcherConsoleHosts(openResult.BindHosts, hostInput, effectivePublic)
fmt.Print(utils.Banner)
fmt.Println()
+92 -18
View File
@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"strconv"
"strings"
"testing"
"time"
@@ -123,27 +124,100 @@ func TestResolveLauncherHostInput(t *testing.T) {
}
func TestLauncherConsoleHosts(t *testing.T) {
t.Run("wildcard exposes local loopback hints", func(t *testing.T) {
hosts := launcherConsoleHosts([]string{"::"}, netbind.ResolveAdaptiveLoopbackHost())
seen := make(map[string]bool, len(hosts))
for _, host := range 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("default loopback shows localhost only", 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"}
if strings.Join(hosts, ",") != strings.Join(want, ",") {
t.Fatalf("hosts = %#v, want %#v", hosts, want)
}
})
t.Run("explicit ipv6 host remains visible", func(t *testing.T) {
hosts := launcherConsoleHosts([]string{"::1"}, "::1")
if len(hosts) < 1 || hosts[0] != "::1" {
t.Fatalf("hosts = %#v, want probe host first", hosts)
t.Run("explicit loopback hosts collapse to localhost", func(t *testing.T) {
tests := []struct {
name string
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)
}
})
}
+67 -19
View File
@@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
@@ -54,41 +55,88 @@ func FindPicoclawBinary() string {
return "picoclaw"
}
// GetLocalIPv4 returns a non-loopback local IPv4 address.
func GetLocalIPv4() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
func appendUniqueIP(addrs []string, seen map[string]struct{}, value string) []string {
value = strings.TrimSpace(value)
if value == "" {
return addrs
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
if _, ok := seen[value]; ok {
return addrs
}
return ""
seen[value] = struct{}{}
return append(addrs, value)
}
// GetLocalIPv6 returns a non-loopback local IPv6 address.
func GetLocalIPv6() string {
// GetLocalIPv4s returns all non-loopback local IPv4 addresses.
func GetLocalIPv4s() []string {
addrs, err := net.InterfaceAddrs()
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 {
ipnet, ok := a.(*net.IPNet)
if !ok || ipnet.IP == nil {
continue
}
ip := ipnet.IP
if ip.IsLoopback() || ip.To4() != nil {
if !isDisplayGlobalIPv6(ip) {
continue
}
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
continue
}
return ip.String()
results = appendUniqueIP(results, seen, 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.