package agent import ( "bufio" "context" "net" "os" "slices" "strings" "time" "sultan/pkg/docker" "sultan/pkg/registry" ) // osReleasePath: Quelle des Betriebssystems ohne Docker (systemd-Agent). var osReleasePath = "/etc/os-release" // resolver ist der Teil von *net.Resolver, den detectFQDN braucht. type resolver interface { LookupCNAME(ctx context.Context, host string) (string, error) LookupAddr(ctx context.Context, addr string) ([]string, error) } // detectFQDN ermittelt den vollständigen Namen des Knotens für Inventar und // Störfall (Spec 5.3): explizit (--fqdn) > Hostname mit Punkt > Hostname // über die Suchdomain aufgelöst > Reverse-Lookup der Backend-Adresse > // kurzer Hostname. Jede Abfrage ist kurz begrenzt; scheitert alles, bleibt // der Hostname. func detectFQDN(ctx context.Context, res resolver, explicit, hostname, backend string) string { if n := normalizeName(explicit); n != "" { return n } short := normalizeName(hostname) if strings.Contains(short, ".") { return short } lookup := func(f func(context.Context) string) string { lctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() return f(lctx) } if short != "" { if n := lookup(func(c context.Context) string { cname, err := res.LookupCNAME(c, short) if err != nil { return "" } return normalizeName(cname) }); strings.Contains(n, ".") { return n } } if net.ParseIP(backend) != nil { if n := lookup(func(c context.Context) string { names, err := res.LookupAddr(c, backend) if err != nil { return "" } for _, name := range names { if n := normalizeName(name); strings.Contains(n, ".") { return n } } return "" }); n != "" { return n } } return short } func normalizeName(s string) string { return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(s), ".")) } // cgnat ist 100.64.0.0/10 – der Bereich, aus dem NetBird Adressen vergibt. var cgnat = &net.IPNet{IP: net.IPv4(100, 64, 0, 0).To4(), Mask: net.CIDRMask(10, 32)} // netbirdIP wählt die NetBird-Adresse: IPv4 auf wt0, sonst die erste // IPv4-Adresse aus 100.64.0.0/10 (Interfaces alphabetisch). func netbirdIP(addrs map[string][]net.IP) string { for _, ip := range addrs["wt0"] { if ip4 := ip.To4(); ip4 != nil { return ip4.String() } } names := make([]string, 0, len(addrs)) for name := range addrs { names = append(names, name) } slices.Sort(names) for _, name := range names { for _, ip := range addrs[name] { if ip4 := ip.To4(); ip4 != nil && cgnat.Contains(ip4) { return ip4.String() } } } return "" } // interfaceAddrs liest die Adressen aller Interfaces. func interfaceAddrs() map[string][]net.IP { ifaces, err := net.Interfaces() if err != nil { return nil } out := map[string][]net.IP{} for _, ifc := range ifaces { addrs, err := ifc.Addrs() if err != nil { continue } for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok { out[ifc.Name] = append(out[ifc.Name], ipnet.IP) } } } return out } // osPrettyName liest PRETTY_NAME (ersatzweise NAME) aus einer os-release-Datei. func osPrettyName(path string) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() var name string sc := bufio.NewScanner(f) for sc.Scan() { key, value, ok := strings.Cut(strings.TrimSpace(sc.Text()), "=") if !ok { continue } value = strings.Trim(value, `"'`) switch key { case "PRETTY_NAME": return value case "NAME": name = value } } return name } // hostInfo sammelt die Angaben zum Knoten. Mit Docker kommt das // Betriebssystem aus der Docker-API (das des Hosts, auch wenn der Agent in // einem Container läuft); Fehler lassen Felder nur leer. func (a *Agent) hostInfo(ctx context.Context, client *docker.Client) *registry.HostInfo { h := ®istry.HostInfo{FQDN: a.fqdn, NetBirdIP: netbirdIP(interfaceAddrs())} if client != nil { info, err := client.Info(ctx) if err != nil { a.log.Debug("Docker-Info nicht lesbar", "err", err) } h.OS, h.DockerVersion = info.OperatingSystem, info.ServerVersion } if h.OS == "" { h.OS = osPrettyName(osReleasePath) } return h }