package app import ( "net/http" "sort" "strconv" "strings" "time" ) // --------------------------------------------------------------------- // traffic candidates (subnets / systemd units / UIDs) // --------------------------------------------------------------------- // EN: Provides best-effort suggestions for traffic overrides UI. // EN: This endpoint must never apply anything automatically. // RU: Отдаёт подсказки для UI overrides. // RU: Этот эндпоинт никогда не должен применять что-либо автоматически. func handleTrafficCandidates(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } resp := TrafficCandidatesResponse{ GeneratedAt: time.Now().UTC().Format(time.RFC3339), Subnets: trafficCandidateSubnets(), Units: trafficCandidateUnits(), UIDs: trafficCandidateUIDs(), } writeJSON(w, http.StatusOK, resp) } func trafficCandidateSubnets() []TrafficCandidateSubnet { out, _, code, _ := runCommand("ip", "-4", "route", "show", "table", "main") if code != 0 { return nil } seen := map[string]struct{}{} items := make([]TrafficCandidateSubnet, 0, 24) for _, raw := range strings.Split(out, "\n") { line := strings.TrimSpace(raw) if line == "" { continue } fields := strings.Fields(line) if len(fields) == 0 { continue } dst := strings.TrimSpace(fields[0]) if dst == "" || dst == "default" { continue } dev := parseRouteDevice(fields) if dev == "" || dev == "lo" { continue } if isVPNLikeIface(dev) { continue } isDocker := isContainerIface(dev) isLocal := isAutoBypassDestination(dst) if !isDocker && !isLocal { // keep suggestions intentionally small: only local/LAN + container subnets continue } kind := "lan" if isDocker { kind = "docker" } else if strings.Contains(" "+strings.ToLower(line)+" ", " scope link ") { kind = "link" } key := kind + "|" + dst + "|" + dev if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} items = append(items, TrafficCandidateSubnet{ CIDR: dst, Dev: dev, Kind: kind, LinkDown: strings.Contains(strings.ToLower(line), " linkdown"), }) } sort.Slice(items, func(i, j int) bool { if items[i].Kind != items[j].Kind { return items[i].Kind < items[j].Kind } if items[i].Dev != items[j].Dev { return items[i].Dev < items[j].Dev } return items[i].CIDR < items[j].CIDR }) return items } func trafficCandidateUnits() []TrafficCandidateUnit { stdout, _, code, _ := runCommand( "systemctl", "list-units", "--type=service", "--state=running", "--no-legend", "--no-pager", "--plain", ) if code != 0 { return nil } seen := map[string]struct{}{} items := make([]TrafficCandidateUnit, 0, 32) for _, raw := range strings.Split(stdout, "\n") { line := strings.TrimSpace(raw) if line == "" { continue } fields := strings.Fields(line) if len(fields) < 1 { continue } unit := strings.TrimSpace(fields[0]) if unit == "" { continue } if _, ok := seen[unit]; ok { continue } seen[unit] = struct{}{} desc := "" // UNIT LOAD ACTIVE SUB DESCRIPTION if len(fields) >= 5 { desc = strings.Join(fields[4:], " ") } items = append(items, TrafficCandidateUnit{ Unit: unit, Description: strings.TrimSpace(desc), Cgroup: "system.slice/" + unit, }) } sort.Slice(items, func(i, j int) bool { return items[i].Unit < items[j].Unit }) return items } func trafficCandidateUIDs() []TrafficCandidateUID { stdout, _, code, _ := runCommand("ps", "-eo", "uid,user,comm", "--no-headers") if code != 0 { return nil } type agg struct { uid int user string comms map[string]struct{} } aggs := map[int]*agg{} for _, raw := range strings.Split(stdout, "\n") { line := strings.TrimSpace(raw) if line == "" { continue } fields := strings.Fields(line) if len(fields) < 2 { continue } uidN, err := strconv.Atoi(strings.TrimSpace(fields[0])) if err != nil || uidN < 0 { continue } user := strings.TrimSpace(fields[1]) comm := "" if len(fields) >= 3 { comm = strings.TrimSpace(fields[2]) } a := aggs[uidN] if a == nil { a = &agg{uid: uidN, user: user, comms: map[string]struct{}{}} aggs[uidN] = a } if a.user == "" && user != "" { a.user = user } if comm != "" { a.comms[comm] = struct{}{} } } items := make([]TrafficCandidateUID, 0, len(aggs)) for _, a := range aggs { examples := make([]string, 0, len(a.comms)) for c := range a.comms { examples = append(examples, c) } sort.Strings(examples) if len(examples) > 3 { examples = examples[:3] } items = append(items, TrafficCandidateUID{ UID: a.uid, User: a.user, Examples: examples, }) } sort.Slice(items, func(i, j int) bool { return items[i].UID < items[j].UID }) return items }