fix(launcher): refine console host display
This commit is contained in:
parent
d4d652b455
commit
93bf871bd2
3 changed files with 249 additions and 67 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue