Files
tailscale/cmd/tta/wgserver_linux.go
Alex Valiushko 7d18a06292 go.mod,wgengine/magicsock: pull wireguard-go fix for roaming endpoints (#20118)
Bumps wireguard-go pin to include the roaming endpoints fix, and
two internal enhancements.

Pulls stock wireguard-go for non-tailscale simulation in tests,
to use its endpoint discovery mechanism.

Updates #20082

Change-Id: I2ff282cb7fe4ab099ce5e780a1d40ae86a6a6964
Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-06-12 10:50:35 -07:00

157 lines
4.6 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"cmp"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"sync"
"golang.org/x/crypto/curve25519"
// Stock wireguard-go to simulate non-Tailscale peers.
extwgconn "golang.zx2c4.com/wireguard/conn"
extwgdevice "golang.zx2c4.com/wireguard/device"
extwgtun "golang.zx2c4.com/wireguard/tun"
)
func init() {
wgServerUp = wgServerUpLinux
}
var (
wgServerMu sync.Mutex
wgServerDev *extwgdevice.Device // retained so the goroutines stay alive
)
// wgServerUpLinux brings up a userspace WireGuard interface on the local VM
// configured as a single-peer "Mullvad-style" exit node, then sets up the
// kernel-side IP/forwarding/MASQUERADE so that decrypted traffic from the
// peer egresses to the test internet.
//
// Required URL query parameters:
// - addr: CIDR for the WG interface (e.g. "10.64.0.1/24")
// - listen-port: WG listen port
// - peer-pub-b64: base64-encoded 32-byte WG public key of the only peer
// - peer-allowed-ip: prefix the peer is allowed to source from
// (e.g. "10.64.0.2/32")
// - masq-src: prefix to MASQUERADE on egress (e.g. "10.64.0.0/24")
//
// Optional:
// - name: TUN device name (default "wg0")
//
// On success, it writes "PUBKEY=<base64>\n" — the freshly generated public
// key the caller must pin as the peer's WG public key.
func wgServerUpLinux(w http.ResponseWriter, r *http.Request) {
wgServerMu.Lock()
defer wgServerMu.Unlock()
if wgServerDev != nil {
http.Error(w, "wg server already up", http.StatusConflict)
return
}
q := r.URL.Query()
name := cmp.Or(q.Get("name"), "wg0")
addr := q.Get("addr")
listenPort := q.Get("listen-port")
peerPubB64 := q.Get("peer-pub-b64")
peerAllowedIP := q.Get("peer-allowed-ip")
masqSrc := q.Get("masq-src")
for _, kv := range []struct{ k, v string }{
{"addr", addr},
{"listen-port", listenPort},
{"peer-pub-b64", peerPubB64},
{"peer-allowed-ip", peerAllowedIP},
{"masq-src", masqSrc},
} {
if kv.v == "" {
http.Error(w, "missing "+kv.k, http.StatusBadRequest)
return
}
}
peerPub, err := base64.StdEncoding.DecodeString(peerPubB64)
if err != nil || len(peerPub) != 32 {
http.Error(w, fmt.Sprintf("bad peer-pub-b64: %v (len=%d)", err, len(peerPub)), http.StatusBadRequest)
return
}
var priv [32]byte
if _, err := rand.Read(priv[:]); err != nil {
http.Error(w, "rand: "+err.Error(), http.StatusInternalServerError)
return
}
// X25519 key clamping.
priv[0] &= 248
priv[31] = (priv[31] & 127) | 64
pub, err := curve25519.X25519(priv[:], curve25519.Basepoint)
if err != nil {
http.Error(w, "deriving pubkey: "+err.Error(), http.StatusInternalServerError)
return
}
tdev, err := extwgtun.CreateTUN(name, extwgdevice.DefaultMTU)
if err != nil {
http.Error(w, "tun.CreateTUN: "+err.Error(), http.StatusInternalServerError)
return
}
wglog := &extwgdevice.Logger{
Verbosef: func(string, ...any) {},
Errorf: func(f string, a ...any) { log.Printf("wg-server: "+f, a...) },
}
dev := extwgdevice.NewDevice(tdev, extwgconn.NewDefaultBind(), wglog)
uapi := fmt.Sprintf("private_key=%s\nlisten_port=%s\npublic_key=%s\nallowed_ip=%s\n",
hex.EncodeToString(priv[:]), listenPort,
hex.EncodeToString(peerPub), peerAllowedIP)
if err := dev.IpcSet(uapi); err != nil {
dev.Close()
http.Error(w, "IpcSet: "+err.Error(), http.StatusInternalServerError)
return
}
if err := dev.Up(); err != nil {
dev.Close()
http.Error(w, "dev.Up: "+err.Error(), http.StatusInternalServerError)
return
}
steps := []struct {
why string
exec []string
file struct{ path, data string }
}{
{why: "ip addr add", exec: []string{"ip", "addr", "add", addr, "dev", name}},
{why: "ip link up", exec: []string{"ip", "link", "set", name, "up"}},
{why: "enable forwarding", file: struct{ path, data string }{"/proc/sys/net/ipv4/ip_forward", "1\n"}},
{why: "FORWARD policy", exec: []string{"iptables", "-P", "FORWARD", "ACCEPT"}},
{why: "MASQUERADE", exec: []string{"iptables", "-t", "nat", "-A", "POSTROUTING", "-s", masqSrc, "-j", "MASQUERADE"}},
}
for _, s := range steps {
if s.file.path != "" {
if err := os.WriteFile(s.file.path, []byte(s.file.data), 0644); err != nil {
dev.Close()
http.Error(w, fmt.Sprintf("%s: %v", s.why, err), http.StatusInternalServerError)
return
}
continue
}
if out, err := exec.Command(s.exec[0], s.exec[1:]...).CombinedOutput(); err != nil {
dev.Close()
http.Error(w, fmt.Sprintf("%s: %v: %s", s.why, err, out), http.StatusInternalServerError)
return
}
}
wgServerDev = dev
fmt.Fprintf(w, "PUBKEY=%s\n", base64.StdEncoding.EncodeToString(pub))
}