package main import ( "context" "io" "net" "net/http" "sort" "strings" "sync" "time" ) func validIP(raw string) string { ip := net.ParseIP(strings.TrimSpace(raw)) if ip == nil || ip.IsUnspecified() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() { return "" } return ip.String() } func publicIPFromURL(ctx context.Context, rawURL string) string { if rawURL == "" { return "" } client := &http.Client{Timeout: 2 * time.Second} req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { return "" } resp, err := client.Do(req) if err != nil { return "" } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "" } body, err := io.ReadAll(io.LimitReader(resp.Body, 128)) if err != nil { return "" } return validIP(string(body)) } func routeIP() string { conn, err := net.DialTimeout("udp", "1.1.1.1:53", time.Second) if err != nil { return "" } defer conn.Close() if addr, ok := conn.LocalAddr().(*net.UDPAddr); ok { return validIP(addr.IP.String()) } return "" } func ipScope(ip net.IP) string { switch { case ip.IsPrivate(): return "private" case ip.IsLinkLocalUnicast(): return "link_local" default: return "public" } } func skipInterface(name string) bool { name = strings.ToLower(strings.TrimSpace(name)) return name == "docker0" || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "veth") || strings.HasPrefix(name, "br-") || strings.HasPrefix(name, "virbr") } func localHostAddresses() []hostAddress { interfaces, err := net.Interfaces() if err != nil { return nil } out := make([]hostAddress, 0) for _, iface := range interfaces { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 || skipInterface(iface.Name) { continue } addrs, err := iface.Addrs() if err != nil { continue } for _, addr := range addrs { raw := addr.String() if host, _, err := net.ParseCIDR(raw); err == nil { raw = host.String() } else if host, _, err := net.SplitHostPort(raw); err == nil { raw = host } clean := validIP(raw) if clean == "" { continue } ip := net.ParseIP(clean) out = append(out, hostAddress{IP: clean, Source: "interface", Interface: iface.Name, Scope: ipScope(ip)}) } } sort.Slice(out, func(i, j int) bool { if out[i].Interface != out[j].Interface { return out[i].Interface < out[j].Interface } return out[i].IP < out[j].IP }) return out } type ipDetector struct { mu sync.Mutex publicIP string expires time.Time } func (d *ipDetector) public(ctx context.Context, rawURL string) string { if rawURL == "" { return "" } d.mu.Lock() defer d.mu.Unlock() if time.Now().Before(d.expires) { return d.publicIP } d.publicIP = publicIPFromURL(ctx, rawURL) d.expires = time.Now().Add(5 * time.Minute) if d.publicIP == "" { d.expires = time.Now().Add(30 * time.Second) } return d.publicIP } func mergeHostAddress(out *[]hostAddress, index map[string]int, candidate hostAddress) { candidate.IP = validIP(candidate.IP) if candidate.IP == "" { return } if candidate.Scope == "" { candidate.Scope = ipScope(net.ParseIP(candidate.IP)) } if pos, ok := index[candidate.IP]; ok { existing := &(*out)[pos] if candidate.Interface != "" && existing.Interface == "" { existing.Interface = candidate.Interface } if candidate.Source != "" && !strings.Contains(existing.Source, candidate.Source) { if existing.Source == "" { existing.Source = candidate.Source } else { existing.Source += "," + candidate.Source } } return } index[candidate.IP] = len(*out) *out = append(*out, candidate) } func detectedAddresses(ctx context.Context, cfg config, detector *ipDetector) ([]hostAddress, string, string) { out := make([]hostAddress, 0, 8) index := make(map[string]int) for _, item := range localHostAddresses() { mergeHostAddress(&out, index, item) } routed := routeIP() if routed != "" { mergeHostAddress(&out, index, hostAddress{IP: routed, Source: "route"}) } public := detector.public(ctx, cfg.PublicIPURL) if public != "" { mergeHostAddress(&out, index, hostAddress{IP: public, Source: "public_api"}) } advertised := validIP(cfg.AdvertiseIP) if advertised != "" { mergeHostAddress(&out, index, hostAddress{IP: advertised, Source: "override"}) } switch { case advertised != "": return out, advertised, "override" case routed != "": return out, routed, "route" case public != "": return out, public, "public_api" case len(out) > 0: return out, out[0].IP, out[0].Source default: return out, "", "unavailable" } }