package app import ( "errors" "fmt" "net/netip" "strconv" "strings" "time" ) const ( transportNetnsNATTable = "svpn_netns" ) type transportNetnsSpec struct { Name string HostVeth string PeerVeth string Prefix netip.Prefix HostIP netip.Addr PeerIP netip.Addr Uplink string } func transportNetnsEnabled(client TransportClient) bool { if !transportConfigHasKey(client.Config, "netns_enabled") { return false } return transportConfigBool(client.Config, "netns_enabled") } func transportNetnsStrict(client TransportClient) bool { return transportConfigBool(client.Config, "netns_setup_strict") } func transportNetnsAutoCleanup(client TransportClient) bool { return transportConfigBool(client.Config, "netns_auto_cleanup") } func transportEnsureNetnsForClient(client TransportClient) (string, error) { spec, err := transportBuildNetnsSpec(client) if err != nil { return "", err } created := false exists, err := transportNetnsExists(spec.Name) if err != nil { return "", err } if !exists { if err := transportRunMust(6*time.Second, "ip", "netns", "add", spec.Name); err != nil { return "", err } created = true } // Recreate veth pair for deterministic state before every start/restart. _ = transportRunSoft(3*time.Second, "ip", "link", "del", spec.HostVeth) _ = transportRunInNetnsSoft(3*time.Second, client, spec.Name, "ip", "link", "del", spec.PeerVeth) if err := transportRunMust(5*time.Second, "ip", "link", "add", spec.HostVeth, "type", "veth", "peer", "name", spec.PeerVeth); err != nil { return "", err } if err := transportRunMust(5*time.Second, "ip", "link", "set", spec.PeerVeth, "netns", spec.Name); err != nil { return "", err } mask := strconv.Itoa(spec.Prefix.Bits()) if err := transportRunMust(5*time.Second, "ip", "addr", "replace", spec.HostIP.String()+"/"+mask, "dev", spec.HostVeth); err != nil { return "", err } if err := transportRunMust(5*time.Second, "ip", "link", "set", spec.HostVeth, "up"); err != nil { return "", err } if err := transportRunInNetnsMust(5*time.Second, client, spec.Name, "ip", "link", "set", "lo", "up"); err != nil { return "", err } if err := transportRunInNetnsMust(5*time.Second, client, spec.Name, "ip", "addr", "replace", spec.PeerIP.String()+"/"+mask, "dev", spec.PeerVeth); err != nil { return "", err } if err := transportRunInNetnsMust(5*time.Second, client, spec.Name, "ip", "link", "set", spec.PeerVeth, "up"); err != nil { return "", err } if err := transportRunInNetnsMust(5*time.Second, client, spec.Name, "ip", "route", "replace", "default", "via", spec.HostIP.String(), "dev", spec.PeerVeth); err != nil { return "", err } if err := transportRunMust(5*time.Second, "sysctl", "-w", "net.ipv4.ip_forward=1"); err != nil { return "", err } if err := transportEnsureNetnsNAT(spec); err != nil { return "", err } if err := transportEnsureNetnsPolicyRoute(spec); err != nil { return "", err } msg := fmt.Sprintf( "netns ready: %s (%s<->%s, subnet=%s, uplink=%s)", spec.Name, spec.HostVeth, spec.PeerVeth, spec.Prefix.String(), spec.Uplink, ) if created { msg += "; created" } return msg, nil } func transportCleanupNetnsForClient(client TransportClient) (string, error) { spec, err := transportBuildNetnsSpec(client) if err != nil { return "", err } exists, err := transportNetnsExists(spec.Name) if err != nil { return "", err } if !exists { _ = transportNetnsDeleteNATRule(spec.Name) return "netns not found (skip cleanup)", nil } var warnings []string if err := transportNetnsDeleteNATRule(spec.Name); err != nil { warnings = append(warnings, err.Error()) } if err := transportDeleteNetnsPolicyRoute(spec); err != nil { warnings = append(warnings, err.Error()) } if err := transportRunSoft(4*time.Second, "ip", "netns", "del", spec.Name); err != nil { warnings = append(warnings, err.Error()) } _ = transportRunSoft(3*time.Second, "ip", "link", "del", spec.HostVeth) msg := "netns cleaned: " + spec.Name if len(warnings) > 0 { return msg, errors.New(strings.Join(warnings, "; ")) } return msg, nil }