128 lines
3.1 KiB
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]
|
|
}
|