WIP: rebase for 2026-05-18 #7
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user