package trafficmode import ( "sort" "strings" ) func NormalizePreferredIface(raw string) string { v := strings.TrimSpace(raw) l := strings.ToLower(v) if l == "" || l == "auto" || l == "-" || l == "default" { return "" } return v } func IfaceExists(iface string, run RunCommandSimpleFunc) bool { iface = strings.TrimSpace(iface) if iface == "" || run == nil { return false } _, _, code, _ := run("ip", "link", "show", iface) return code == 0 } func ParseUpIfaces(ipLinkOutput string) []string { seen := map[string]struct{}{} outIfaces := make([]string, 0, 8) for _, line := range strings.Split(ipLinkOutput, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.SplitN(line, ":", 3) if len(parts) < 3 { continue } name := strings.TrimSpace(parts[1]) name = strings.SplitN(name, "@", 2)[0] name = strings.TrimSpace(name) if name == "" || name == "lo" { continue } if _, ok := seen[name]; ok { continue } seen[name] = struct{}{} outIfaces = append(outIfaces, name) } return outIfaces } func ListUpIfaces(run RunCommandSimpleFunc) []string { if run == nil { return nil } out, _, code, _ := run("ip", "-o", "link", "show", "up") if code != 0 { return nil } return ParseUpIfaces(out) } func IsVPNLikeIface(iface string) bool { l := strings.ToLower(strings.TrimSpace(iface)) return strings.HasPrefix(l, "tun") || strings.HasPrefix(l, "wg") || strings.HasPrefix(l, "ppp") || strings.HasPrefix(l, "tap") || strings.HasPrefix(l, "utun") || strings.HasPrefix(l, "vpn") } func ListSelectableIfaces(up []string, preferred string) []string { seen := map[string]struct{}{} var vpnLike []string var other []string add := func(dst *[]string, iface string) { iface = strings.TrimSpace(iface) if iface == "" { return } if _, ok := seen[iface]; ok { return } seen[iface] = struct{}{} *dst = append(*dst, iface) } for _, iface := range up { if IsVPNLikeIface(iface) { add(&vpnLike, iface) } } for _, iface := range up { if !IsVPNLikeIface(iface) { add(&other, iface) } } sort.Strings(vpnLike) sort.Strings(other) selected := make([]string, 0, len(vpnLike)+len(other)+1) selected = append(selected, vpnLike...) selected = append(selected, other...) pref := NormalizePreferredIface(preferred) if pref != "" { if _, ok := seen[pref]; !ok { selected = append([]string{pref}, selected...) } } return selected } func ResolveTrafficIface( preferred string, ifaceExists func(string) bool, statusIface func() string, listUp func() []string, ) (string, string) { exists := ifaceExists if exists == nil { exists = func(string) bool { return false } } status := statusIface if status == nil { status = func() string { return "" } } upIfaces := listUp if upIfaces == nil { upIfaces = func() []string { return nil } } pref := NormalizePreferredIface(preferred) if pref != "" && exists(pref) { return pref, "preferred" } statusResolved := strings.TrimSpace(status()) if statusResolved != "" && exists(statusResolved) { return statusResolved, "status" } for _, iface := range upIfaces() { if IsVPNLikeIface(iface) { return iface, "auto-vpn-like" } } if pref != "" { return "", "preferred-not-found" } return "", "iface-not-found" }