v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,192 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user