package app import ( "encoding/json" "net/netip" "os" "path/filepath" "sort" "strings" "time" ) func loadTransportBootstrapState() transportBootstrapState { st := transportBootstrapState{Version: transportBootstrapStateVersion} data, err := os.ReadFile(transportBootstrapStatePath) if err != nil { return st } if err := json.Unmarshal(data, &st); err != nil { return transportBootstrapState{Version: transportBootstrapStateVersion} } return normalizeTransportBootstrapState(st) } func saveTransportBootstrapState(st transportBootstrapState) error { st = normalizeTransportBootstrapState(st) st.Version = transportBootstrapStateVersion st.UpdatedAt = time.Now().UTC().Format(time.RFC3339) data, err := json.MarshalIndent(st, "", " ") if err != nil { return err } if err := os.MkdirAll(filepath.Dir(transportBootstrapStatePath), 0o755); err != nil { return err } tmp := transportBootstrapStatePath + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } return os.Rename(tmp, transportBootstrapStatePath) } func normalizeTransportBootstrapState(st transportBootstrapState) transportBootstrapState { st.Version = transportBootstrapStateVersion if len(st.Clients) == 0 { st.Clients = nil return st } out := make(map[string][]string, len(st.Clients)) for rawID, rawIPs := range st.Clients { id := sanitizeID(rawID) if id == "" { continue } ips := normalizeBootstrapIPv4List(rawIPs) if len(ips) == 0 { continue } out[id] = ips } if len(out) == 0 { st.Clients = nil return st } st.Clients = out return st } func normalizeBootstrapIPv4List(in []string) []string { seen := map[string]struct{}{} out := make([]string, 0, len(in)) for _, raw := range in { ip := strings.TrimSpace(raw) addr, err := netip.ParseAddr(ip) if err != nil || !addr.Is4() { continue } key := addr.String() if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, key) } sort.Strings(out) return out } func dedupeStrings(in []string) []string { if len(in) == 0 { return nil } seen := map[string]struct{}{} out := make([]string, 0, len(in)) for _, raw := range in { v := strings.TrimSpace(raw) if v == "" { continue } if _, ok := seen[v]; ok { continue } seen[v] = struct{}{} out = append(out, v) } return out }