193 lines
4.5 KiB
Go
193 lines
4.5 KiB
Go
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"
|
|
}
|
|
}
|