Files
elmprodvpn/selective-vpn-api/app/transport_netns_spec.go

128 lines
3.1 KiB
Go

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]
}