package app import ( "sort" "strings" ) func normalizeTransportIfaceID(raw string) string { id := sanitizeID(raw) if strings.TrimSpace(id) == "" { return transportDefaultIfaceID } return id } func normalizeTransportInterfaceMode(raw TransportInterfaceMode, ifaceID string) TransportInterfaceMode { mode := TransportInterfaceMode(strings.ToLower(strings.TrimSpace(string(raw)))) switch mode { case TransportInterfaceModeShared, TransportInterfaceModeDedicated: return mode default: if normalizeTransportIfaceID(ifaceID) == transportDefaultIfaceID { return TransportInterfaceModeShared } return TransportInterfaceModeDedicated } } func defaultTransportInterfaceName(ifaceID string) string { if normalizeTransportIfaceID(ifaceID) == transportDefaultIfaceID { return "Shared interface" } return ifaceID } func transportInterfaceRoutingTableHint(iface TransportInterface) string { if strings.TrimSpace(iface.RoutingTable) != "" { return strings.TrimSpace(iface.RoutingTable) } if iface.Config == nil { return "" } if v := strings.TrimSpace(transportConfigString(iface.Config, "routing_table")); v != "" { return v } if v := strings.TrimSpace(transportConfigString(iface.Config, "table")); v != "" { return v } return "" } func resolveTransportInterfaceRoutingTable(iface TransportInterface, ifaceID string) string { hint := transportInterfaceRoutingTableHint(iface) if hint != "" { return normalizeTransportRoutingTable(hint, transportRoutingTableForIfaceID(ifaceID)) } if normalizeTransportIfaceID(ifaceID) == transportDefaultIfaceID { return "" } return transportRoutingTableForIfaceID(ifaceID) } func normalizeTransportInterfacesState(st transportInterfacesState, clients []TransportClient) (transportInterfacesState, bool) { changed := false st.Version = transportStateVersion if st.Items == nil { st.Items = nil } byID := map[string]int{} out := make([]TransportInterface, 0, len(st.Items)+1) for _, raw := range st.Items { it := raw id := normalizeTransportIfaceID(it.ID) if id == "" { changed = true continue } if it.ID != id { it.ID = id changed = true } if strings.TrimSpace(it.Name) == "" { it.Name = defaultTransportInterfaceName(id) changed = true } mode := normalizeTransportInterfaceMode(it.Mode, id) if it.Mode != mode { it.Mode = mode changed = true } wantTable := resolveTransportInterfaceRoutingTable(it, id) if strings.TrimSpace(it.RoutingTable) != wantTable { it.RoutingTable = wantTable changed = true } if it.Config != nil { it.Config = cloneMap(it.Config) } if idx, ok := byID[id]; ok { if preferTransportInterface(it, out[idx]) { out[idx] = it } changed = true continue } byID[id] = len(out) out = append(out, it) } ensureIface := func(id string) { normID := normalizeTransportIfaceID(id) if normID == "" { return } if _, ok := byID[normID]; ok { return } it := TransportInterface{ ID: normID, Name: defaultTransportInterfaceName(normID), Mode: normalizeTransportInterfaceMode("", normID), } it.RoutingTable = resolveTransportInterfaceRoutingTable(it, normID) out = append(out, it) byID[normID] = len(out) - 1 changed = true } ensureIface(transportDefaultIfaceID) for _, it := range clients { ensureIface(it.IfaceID) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) st.Items = out return st, changed } func preferTransportInterface(cand, cur TransportInterface) bool { cu := strings.TrimSpace(cand.UpdatedAt) ou := strings.TrimSpace(cur.UpdatedAt) if cu != ou { if cu == "" { return false } if ou == "" { return true } return cu > ou } return strings.TrimSpace(cand.Name) > strings.TrimSpace(cur.Name) } func buildTransportInterfaceItems(ifaces []TransportInterface, clients []TransportClient) []TransportInterfaceItem { group := map[string][]TransportClient{} for _, it := range clients { id := normalizeTransportIfaceID(it.IfaceID) group[id] = append(group[id], it) } items := make([]TransportInterfaceItem, 0, len(ifaces)) seen := map[string]struct{}{} for _, iface := range ifaces { id := normalizeTransportIfaceID(iface.ID) if id == "" { continue } seen[id] = struct{}{} clientsForIface := append([]TransportClient(nil), group[id]...) sort.Slice(clientsForIface, func(i, j int) bool { return clientsForIface[i].ID < clientsForIface[j].ID }) clientIDs := make([]string, 0, len(clientsForIface)) upCount := 0 for _, cl := range clientsForIface { clientIDs = append(clientIDs, cl.ID) if cl.Status == TransportClientUp { upCount++ } } items = append(items, TransportInterfaceItem{ ID: id, Name: strings.TrimSpace(iface.Name), Mode: normalizeTransportInterfaceMode(iface.Mode, id), RuntimeIface: strings.TrimSpace(iface.RuntimeIface), NetnsName: strings.TrimSpace(iface.NetnsName), RoutingTable: strings.TrimSpace(iface.RoutingTable), Config: cloneMap(iface.Config), UpdatedAt: strings.TrimSpace(iface.UpdatedAt), ClientIDs: clientIDs, ClientCount: len(clientIDs), UpCount: upCount, }) } for id, clientsForIface := range group { if _, ok := seen[id]; ok { continue } sort.Slice(clientsForIface, func(i, j int) bool { return clientsForIface[i].ID < clientsForIface[j].ID }) clientIDs := make([]string, 0, len(clientsForIface)) upCount := 0 routingTable := "" for _, cl := range clientsForIface { clientIDs = append(clientIDs, cl.ID) if cl.Status == TransportClientUp { upCount++ } if routingTable == "" && strings.TrimSpace(cl.RoutingTable) != "" { routingTable = strings.TrimSpace(cl.RoutingTable) } } if routingTable == "" && id != transportDefaultIfaceID { routingTable = transportRoutingTableForIfaceID(id) } items = append(items, TransportInterfaceItem{ ID: id, Name: defaultTransportInterfaceName(id), Mode: normalizeTransportInterfaceMode("", id), RoutingTable: routingTable, ClientIDs: clientIDs, ClientCount: len(clientIDs), UpCount: upCount, }) } sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) return items } func appendTransportVirtualInterfaceItems(items []TransportInterfaceItem, clients []TransportClient) []TransportInterfaceItem { if len(clients) == 0 { return items } byID := map[string]int{} for idx, it := range items { byID[normalizeTransportIfaceID(it.ID)] = idx if id := strings.TrimSpace(sanitizeID(it.ID)); id != "" { byID[id] = idx } } for _, client := range clients { if !isTransportPolicyVirtualClient(client) { continue } id := strings.TrimSpace(sanitizeID(client.ID)) if id == "" { continue } item := TransportInterfaceItem{ ID: id, Name: strings.TrimSpace(client.Name), Mode: normalizeTransportInterfaceMode("", client.IfaceID), RuntimeIface: strings.TrimSpace(client.Iface), NetnsName: "", RoutingTable: strings.TrimSpace(client.RoutingTable), UpdatedAt: strings.TrimSpace(client.UpdatedAt), ClientIDs: []string{id}, ClientCount: 1, UpCount: transportInterfaceItemUpCountForStatus(client.Status), Config: map[string]any{ "virtual": true, "virtual_owner": id, }, } if item.Name == "" { item.Name = id } if idx, ok := byID[id]; ok { items[idx] = item continue } items = append(items, item) byID[id] = len(items) - 1 } sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) return items } func transportInterfaceItemUpCountForStatus(status TransportClientStatus) int { if normalizeTransportStatus(status) == TransportClientUp { return 1 } return 0 }