mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 04:06:35 +02:00
tstest/natlab/{vmtest,vnet}, cmd/tta: add TestMullvadExitNode
Add a vmtest that brings up a Tailscale client, an Ubuntu VM acting as a Mullvad-style plain-WireGuard exit node, and a non-Tailscale webserver, each on its own NAT'd vnet network with a distinct WAN IP. The test exercises Tailscale's IsWireGuardOnly peer code path: the way the control plane wires Mullvad exit nodes into a client's netmap, including the per-client SelfNodeV4MasqAddrForThisPeer source-IP rewrite that lets a Tailscale CGNAT IP egress through a plain-WireGuard tunnel that has no idea what Tailscale is. The mullvad VM doesn't run wireguard-tools or kernel WireGuard; instead, a new TTA endpoint /wg-server-up creates a real Linux TUN named wg0, drives it with wireguard-go (already vendored), and configures the kernel side (ip addr/up, ip_forward, iptables NAT MASQUERADE) so decrypted traffic from the peer egresses with the mullvad VM's WAN IP. Userspace vs kernel WireGuard makes no difference on the wire — what's being tested is Tailscale's plain-WireGuard exit-node code path, not the kernel module — and this lets the test avoid downloading and installing .deb packages inside the VM. Adds Env.BringUpMullvadWGServer (calls /wg-server-up, returns the generated WG public key as a key.NodePublic), Env.SetExitNodeIP (EditPrefs ExitNodeIP directly, for exit nodes whose IPs aren't discoverable via TTA), Env.ControlServer (exposes the underlying testcontrol.Server so tests can UpdateNode / SetMasqueradeAddresses to inject custom peers), and Env.Status (fetches a node's tailscale status, used to read the client's pubkey so we can pin it as the WG server's only allowed peer). The test verifies that the webserver's echoed source IP is the client's WAN with no exit node selected, the mullvad VM's WAN with the WG-only peer selected as exit, and the client's WAN again after clearing. Updates #13038 Change-Id: I5bac4e0d832f05929f12cb77fa9946d7f5fb5ef1 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
da0a277565
commit
4b8e0ede6d
@ -313,6 +313,13 @@ func main() {
|
||||
io.Copy(w, resp.Body)
|
||||
})
|
||||
ttaMux.HandleFunc("/fw", addFirewallHandler)
|
||||
ttaMux.HandleFunc("/wg-server-up", func(w http.ResponseWriter, r *http.Request) {
|
||||
if wgServerUp == nil {
|
||||
http.Error(w, "wg-server-up not supported on this platform", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
wgServerUp(w, r)
|
||||
})
|
||||
ttaMux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) {
|
||||
logBuf.mu.Lock()
|
||||
defer logBuf.mu.Unlock()
|
||||
@ -477,6 +484,11 @@ func addFirewallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var addFirewall func() error // set by fw_linux.go
|
||||
|
||||
// wgServerUp brings up a userspace WireGuard "Mullvad-style" exit-node
|
||||
// server on this VM. It is set by wgserver_linux.go and is nil on
|
||||
// non-Linux.
|
||||
var wgServerUp func(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// logBuffer is a bytes.Buffer that is safe for concurrent use
|
||||
// intended to capture early logs from the process, even if
|
||||
// gokrazy's syslog streaming isn't working or yet working.
|
||||
|
||||
155
cmd/tta/wgserver_linux.go
Normal file
155
cmd/tta/wgserver_linux.go
Normal file
@ -0,0 +1,155 @@
|
||||
// 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"
|
||||
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func init() {
|
||||
wgServerUp = wgServerUpLinux
|
||||
}
|
||||
|
||||
var (
|
||||
wgServerMu sync.Mutex
|
||||
wgServerDev *device.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 := tun.CreateTUN(name, device.DefaultMTU)
|
||||
if err != nil {
|
||||
http.Error(w, "tun.CreateTUN: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
wglog := &device.Logger{
|
||||
Verbosef: func(string, ...any) {},
|
||||
Errorf: func(f string, a ...any) { log.Printf("wg-server: "+f, a...) },
|
||||
}
|
||||
dev := wgcfg.NewDevice(tdev, conn.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))
|
||||
}
|
||||
@ -17,26 +17,33 @@ package vmtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/tstest/natlab/vnet"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
@ -733,6 +740,110 @@ func (e *Env) SetExitNode(client, exitNode *Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetExitNodeIP sets the client's ExitNodeIP preference directly, by IP.
|
||||
// This is the right helper for plain-WireGuard exit nodes (Mullvad-style)
|
||||
// that aren't on the tailnet — pass an invalid netip.Addr{} to clear.
|
||||
// For tailnet exit nodes whose Tailscale IP is discoverable via TTA, use
|
||||
// [Env.SetExitNode] instead.
|
||||
func (e *Env) SetExitNodeIP(client *Node, ip netip.Addr) {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if _, err := client.agent.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ExitNodeID: "",
|
||||
ExitNodeIP: ip,
|
||||
},
|
||||
ExitNodeIDSet: true,
|
||||
ExitNodeIPSet: true,
|
||||
}); err != nil {
|
||||
e.t.Fatalf("SetExitNodeIP(%s, %v): %v", client.name, ip, err)
|
||||
}
|
||||
if !ip.IsValid() {
|
||||
e.t.Logf("[%s] cleared exit node", client.name)
|
||||
} else {
|
||||
e.t.Logf("[%s] using exit-node IP %v", client.name, ip)
|
||||
}
|
||||
}
|
||||
|
||||
// ControlServer returns the underlying test control server, for tests that
|
||||
// need to inject custom peers, masquerade pairs, etc. The returned server's
|
||||
// Node store is shared with the running tailnet, so changes take effect on
|
||||
// the next netmap update sent to peers.
|
||||
func (e *Env) ControlServer() *testcontrol.Server {
|
||||
return e.server.ControlServer()
|
||||
}
|
||||
|
||||
// BringUpMullvadWGServer brings up a userspace WireGuard server on n,
|
||||
// configured as a single-peer "Mullvad-style" exit-node target. The
|
||||
// server runs inside n's TTA process on a Linux TUN named "wg0".
|
||||
//
|
||||
// gw is the WG interface address (e.g. 10.64.0.1/24). The server listens
|
||||
// on listenPort, accepts only the single peer whose public key is peerPub
|
||||
// at peerAllowedIP, and MASQUERADEs egress traffic from masqSrc so that
|
||||
// decrypted packets from the peer egress with n's WAN IP.
|
||||
//
|
||||
// It returns the freshly generated public key of the WG server, which
|
||||
// the caller must pin as the peer key on the [tailcfg.Node] it injects
|
||||
// into the netmap to advertise this server as a plain-WireGuard exit
|
||||
// node. It fatals the test on error.
|
||||
func (e *Env) BringUpMullvadWGServer(n *Node, gw netip.Prefix, listenPort uint16, peerPub key.NodePublic, peerAllowedIP, masqSrc netip.Prefix) key.NodePublic {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
peerPubRaw := peerPub.Raw32()
|
||||
v := url.Values{
|
||||
"addr": {gw.String()},
|
||||
"listen-port": {strconv.Itoa(int(listenPort))},
|
||||
"peer-pub-b64": {base64.StdEncoding.EncodeToString(peerPubRaw[:])},
|
||||
"peer-allowed-ip": {peerAllowedIP.String()},
|
||||
"masq-src": {masqSrc.String()},
|
||||
}
|
||||
reqURL := "http://unused/wg-server-up?" + v.Encode()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
||||
if err != nil {
|
||||
e.t.Fatalf("BringUpMullvadWGServer: %v", err)
|
||||
}
|
||||
res, err := n.agent.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
e.t.Fatalf("BringUpMullvadWGServer(%s): %v", n.name, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
e.t.Fatalf("BringUpMullvadWGServer(%s): %s: %s", n.name, res.Status, body)
|
||||
}
|
||||
var pubB64 string
|
||||
for _, line := range strings.Split(string(body), "\n") {
|
||||
if s, ok := strings.CutPrefix(strings.TrimSpace(line), "PUBKEY="); ok {
|
||||
pubB64 = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if pubB64 == "" {
|
||||
e.t.Fatalf("BringUpMullvadWGServer(%s): no PUBKEY in response: %q", n.name, body)
|
||||
}
|
||||
pubRaw, err := base64.StdEncoding.DecodeString(pubB64)
|
||||
if err != nil || len(pubRaw) != 32 {
|
||||
e.t.Fatalf("BringUpMullvadWGServer(%s): bad PUBKEY %q: %v", n.name, pubB64, err)
|
||||
}
|
||||
return key.NodePublicFromRaw32(mem.B(pubRaw))
|
||||
}
|
||||
|
||||
// Status returns the tailscale status of the given node, fetched from its
|
||||
// TTA agent. It fatals the test on error.
|
||||
func (e *Env) Status(n *Node) *ipnstate.Status {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
st, err := n.agent.Status(ctx)
|
||||
if err != nil {
|
||||
e.t.Fatalf("Status(%s): %v", n.name, err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// SetAcceptRoutes toggles the node's RouteAll preference (the
|
||||
// --accept-routes flag), controlling whether it installs subnet routes
|
||||
// advertised by peers.
|
||||
@ -849,7 +960,7 @@ func (e *Env) ping(from, to *Node) {
|
||||
// is running (which is all of them — DontJoinTailnet only skips
|
||||
// `tailscale up`; the agent runs regardless). Currently Linux-only in TTA.
|
||||
//
|
||||
// Fatals on error.
|
||||
// It fatals the test on error.
|
||||
func (e *Env) AddRoute(n *Node, prefix, via string) {
|
||||
e.t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
@ -5,9 +5,12 @@ package vmtest_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/tstest/natlab/vmtest"
|
||||
"tailscale.com/tstest/natlab/vnet"
|
||||
)
|
||||
@ -441,3 +444,152 @@ func TestExitNode(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMullvadExitNode verifies that a Tailscale client whose netmap contains
|
||||
// a plain-WireGuard exit node (the way Mullvad exit nodes are wired up by
|
||||
// the control plane) can route internet traffic through it, with the source
|
||||
// IP rewritten to the per-client Mullvad-assigned address.
|
||||
//
|
||||
// Topology:
|
||||
//
|
||||
// client (Tailscale, gokrazy) — clientNet (EasyNAT) WAN 1.0.0.1
|
||||
// mullvad (Ubuntu, userspace WG) — mullvadNet (One2OneNAT) WAN 2.0.0.1
|
||||
// webserver (no Tailscale, gokrazy) — webNet (One2OneNAT) WAN 5.0.0.1
|
||||
//
|
||||
// The mullvad VM impersonates a Mullvad WireGuard server. After boot, the
|
||||
// test asks its TTA agent to bring up a userspace WireGuard interface (a
|
||||
// real Linux TUN driven by wireguard-go) that pins the client's Tailscale
|
||||
// node public key as its only allowed peer, sets up IP-forwarding + a
|
||||
// MASQUERADE rule, and reports the WG server's freshly generated public
|
||||
// key back. Userspace vs kernel WireGuard makes no difference on the wire
|
||||
// — what's being tested is Tailscale's plain-WireGuard exit-node code
|
||||
// path, not the kernel module.
|
||||
//
|
||||
// The test then injects a netmap peer with IsWireGuardOnly=true,
|
||||
// AllowedIPs=[gw/32, 0.0.0.0/0, ::/0], the WG endpoint, and a per-client
|
||||
// SelfNodeV4MasqAddrForThisPeer (the mock equivalent of the per-client IP
|
||||
// Mullvad's API hands out at registration time).
|
||||
//
|
||||
// The webserver echoes the source IP it sees:
|
||||
// - exit-node off: source is client's WAN (direct egress)
|
||||
// - exit-node on: source is mullvad's WAN (egress via WG + MASQUERADE)
|
||||
func TestMullvadExitNode(t *testing.T) {
|
||||
env := vmtest.New(t)
|
||||
|
||||
const (
|
||||
clientWAN = "1.0.0.1"
|
||||
mullvadWAN = "2.0.0.1"
|
||||
webWAN = "5.0.0.1"
|
||||
)
|
||||
// Mullvad-side WG network. The client appears as clientMasqIP to
|
||||
// mullvad's wg0; mullvad terminates the tunnel at gw.
|
||||
var (
|
||||
mullvadWGNet = netip.MustParsePrefix("10.64.0.0/24")
|
||||
gw = netip.MustParsePrefix("10.64.0.1/24")
|
||||
clientMasq = netip.MustParsePrefix("10.64.0.2/32")
|
||||
)
|
||||
const wgListenPort uint16 = 51820
|
||||
|
||||
clientNet := env.AddNetwork(clientWAN, "192.168.1.1/24", vnet.EasyNAT)
|
||||
mullvadNet := env.AddNetwork(mullvadWAN, "192.168.2.1/24", vnet.One2OneNAT)
|
||||
webNet := env.AddNetwork(webWAN, "192.168.5.1/24", vnet.One2OneNAT)
|
||||
|
||||
client := env.AddNode("client", clientNet, vmtest.OS(vmtest.Gokrazy))
|
||||
mullvad := env.AddNode("mullvad", mullvadNet,
|
||||
vmtest.OS(vmtest.Ubuntu2404),
|
||||
vmtest.DontJoinTailnet())
|
||||
env.AddNode("webserver", webNet,
|
||||
vmtest.OS(vmtest.Gokrazy),
|
||||
vmtest.DontJoinTailnet(),
|
||||
vmtest.WebServer(8080))
|
||||
|
||||
// Declare test-specific steps for the web UI.
|
||||
wgUpStep := env.AddStep("Bring up Mullvad WG server")
|
||||
injectStep := env.AddStep("Inject Mullvad netmap peer")
|
||||
checkOff1Step := env.AddStep("HTTP GET (exit off)")
|
||||
checkMullvadStep := env.AddStep("HTTP GET (exit=mullvad)")
|
||||
checkOff2Step := env.AddStep("HTTP GET (exit off, again)")
|
||||
|
||||
env.Start()
|
||||
|
||||
// Bring up the WG server inside mullvad's TTA, pinning the client's
|
||||
// Tailscale node public key as the sole allowed peer.
|
||||
wgUpStep.Begin()
|
||||
clientStatus := env.Status(client)
|
||||
mullvadPub := env.BringUpMullvadWGServer(mullvad,
|
||||
gw, wgListenPort,
|
||||
clientStatus.Self.PublicKey, clientMasq, mullvadWGNet)
|
||||
wgUpStep.End(nil)
|
||||
|
||||
// Inject the mullvad node into the netmap as a plain-WireGuard exit
|
||||
// node. This mirrors how the control plane describes Mullvad exit
|
||||
// nodes to clients (see control/cmullvad in the closed repo): a
|
||||
// peer with IsWireGuardOnly=true, an Endpoints entry pointing at
|
||||
// the public WG host:port, and AllowedIPs covering both the gateway
|
||||
// /32 and the 0.0.0.0/0+::/0 exit-node routes.
|
||||
injectStep.Begin()
|
||||
mullvadEndpoint := netip.AddrPortFrom(netip.MustParseAddr(mullvadWAN), wgListenPort)
|
||||
gwHost := netip.PrefixFrom(gw.Addr(), gw.Addr().BitLen())
|
||||
mullvadNode := &tailcfg.Node{
|
||||
ID: 999_001,
|
||||
StableID: "mullvad-test",
|
||||
Name: "mullvad-test.fake-control.example.net.",
|
||||
Key: mullvadPub,
|
||||
MachineAuthorized: true,
|
||||
IsWireGuardOnly: true,
|
||||
Endpoints: []netip.AddrPort{mullvadEndpoint},
|
||||
Addresses: []netip.Prefix{gwHost},
|
||||
AllowedIPs: []netip.Prefix{
|
||||
gwHost,
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
Hostinfo: (&tailcfg.Hostinfo{
|
||||
Hostname: "mullvad-test",
|
||||
}).View(),
|
||||
}
|
||||
cs := env.ControlServer()
|
||||
cs.UpdateNode(mullvadNode)
|
||||
|
||||
// Set the per-peer source-IP masquerade. The control plane normally
|
||||
// derives this from the Mullvad API's per-client registration; here
|
||||
// we just pin it to the address mullvad's wg0 was told to accept.
|
||||
cs.SetMasqueradeAddresses([]testcontrol.MasqueradePair{{
|
||||
Node: clientStatus.Self.PublicKey,
|
||||
Peer: mullvadPub,
|
||||
NodeMasqueradesAs: clientMasq.Addr(),
|
||||
}})
|
||||
injectStep.End(nil)
|
||||
|
||||
webURL := fmt.Sprintf("http://%s:8080/", webWAN)
|
||||
check := func(step *vmtest.Step, label, wantSrc string) {
|
||||
t.Helper()
|
||||
step.Begin()
|
||||
body := env.HTTPGet(client, webURL)
|
||||
t.Logf("[%s] response: %s", label, body)
|
||||
if !strings.Contains(body, "Hello world I am webserver") {
|
||||
step.End(fmt.Errorf("[%s] unexpected webserver response: %q", label, body))
|
||||
t.Fatalf("[%s] unexpected webserver response: %q", label, body)
|
||||
}
|
||||
if !strings.Contains(body, "from "+wantSrc) {
|
||||
step.End(fmt.Errorf("[%s] expected source %q in response, got %q", label, wantSrc, body))
|
||||
t.Fatalf("[%s] expected source %q in response, got %q", label, wantSrc, body)
|
||||
}
|
||||
step.End(nil)
|
||||
}
|
||||
|
||||
// Exit-node off: client routes 0.0.0.0/0 directly via its host stack,
|
||||
// so the webserver sees client's WAN IP.
|
||||
check(checkOff1Step, "exit-off", clientWAN)
|
||||
|
||||
// Switch to the Mullvad WG-only peer as exit node. The client should
|
||||
// now route 0.0.0.0/0 through the WG tunnel; mullvad MASQUERADEs to
|
||||
// its WAN; the webserver sees the mullvad VM's WAN IP.
|
||||
env.SetExitNodeIP(client, gw.Addr())
|
||||
check(checkMullvadStep, "exit-mullvad", mullvadWAN)
|
||||
|
||||
// And back off again, to make sure the transition works in both
|
||||
// directions.
|
||||
env.SetExitNodeIP(client, netip.Addr{})
|
||||
check(checkOff2Step, "exit-off (again)", clientWAN)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user