package app import ( "fmt" "hash/fnv" "net/netip" "strconv" "strings" "time" ) func transportBuildNetnsSpec(client TransportClient) (transportNetnsSpec, error) { ns := transportNetnsName(client) if ns == "" { return transportNetnsSpec{}, fmt.Errorf("netns name is empty") } pfx, err := transportNetnsPrefix(client) if err != nil { return transportNetnsSpec{}, err } hostIP := pfx.Addr().Next() peerIP := hostIP.Next() if !hostIP.IsValid() || !peerIP.IsValid() || !pfx.Contains(peerIP) { return transportNetnsSpec{}, fmt.Errorf("netns subnet has no usable host pair: %s", pfx.String()) } uplink := strings.TrimSpace(transportConfigString(client.Config, "netns_uplink_iface")) if uplink == "" { mainRoute, err := transportDetectMainIPv4Route() if err != nil { return transportNetnsSpec{}, err } uplink = strings.TrimSpace(mainRoute.Dev) } if uplink == "" { return transportNetnsSpec{}, fmt.Errorf("netns uplink iface is empty") } tag := transportShortHash("netns:"+ns, 8) return transportNetnsSpec{ Name: ns, HostVeth: "svh" + tag, PeerVeth: "svn" + tag, Prefix: pfx, HostIP: hostIP, PeerIP: peerIP, Uplink: uplink, }, nil } func transportNetnsName(client TransportClient) string { raw := strings.TrimSpace(transportConfigString(client.Config, "netns_name")) if raw == "" { base := sanitizeID(client.ID) if base == "" { base = "client" } raw = "svpn-" + base } return normalizeTransportNetnsName(raw, client.ID) } func normalizeTransportNetnsName(raw, fallbackClientID string) string { ns := sanitizeID(raw) if ns == "" { base := sanitizeID(fallbackClientID) if base == "" { base = "client" } ns = "svpn-" + base } if len(ns) > 63 { ns = ns[:63] } return ns } func transportNetnsPrefix(client TransportClient) (netip.Prefix, error) { raw := strings.TrimSpace(transportConfigString(client.Config, "netns_subnet")) if raw == "" { seed := transportShortHash(client.ID, 2) n, _ := strconv.ParseUint(seed, 16, 8) raw = fmt.Sprintf("10.240.%d.0/30", 10+int(n)%200) } pfx, err := netip.ParsePrefix(raw) if err != nil { return netip.Prefix{}, fmt.Errorf("invalid netns_subnet: %q", raw) } pfx = pfx.Masked() if !pfx.Addr().Is4() { return netip.Prefix{}, fmt.Errorf("netns_subnet must be IPv4: %q", raw) } if pfx.Bits() > 30 { return netip.Prefix{}, fmt.Errorf("netns_subnet must provide at least 2 host addresses: %q", raw) } return pfx, nil } func transportNetnsExists(name string) (bool, error) { stdout, stderr, code, err := transportRunCommand(4*time.Second, "ip", "netns", "list") if err != nil || code != 0 { return false, transportCommandError("ip netns list", stdout, stderr, code, err) } for _, line := range strings.Split(stdout, "\n") { fields := strings.Fields(strings.TrimSpace(line)) if len(fields) == 0 { continue } if fields[0] == name { return true, nil } } return false, nil } func transportShortHash(raw string, n int) string { if n <= 0 { n = 8 } h := fnv.New32a() _, _ = h.Write([]byte(strings.TrimSpace(raw))) s := fmt.Sprintf("%08x", h.Sum32()) if n >= len(s) { return s } return s[:n] }