cmd/tta, vnet: add host firewall, env var support, more tests

In particular, tests showing that #3824 works. But that test doesn't
actually work yet; it only gets a DERP connection. (why?)

Updates #13038

Change-Id: Ie1fd1b6a38d4e90fae7e72a0b9a142a95f0b2e8f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2024-08-10 13:46:47 -07:00
committed by Brad Fitzpatrick
parent b692985aef
commit a61825c7b8
9 changed files with 393 additions and 7 deletions

View File

@@ -102,6 +102,14 @@ func easy(c *vnet.Config) *vnet.Node {
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT))
}
// easy + host firewall
func easyFW(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(vnet.HostFirewall, c.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT))
}
func easyAF(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(c.AddNetwork(
@@ -134,6 +142,29 @@ func easyPMP(c *vnet.Config) *vnet.Node {
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP))
}
// easy + port mapping + host firewall
func easyPMPFW(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(vnet.HostFirewall,
c.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP))
}
// easy + port mapping + host firewall - BPF
func easyPMPFWNoBPF(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(
vnet.HostFirewall,
vnet.TailscaledEnv{
Key: "TS_DEBUG_DISABLE_RAW_DISCO",
Value: "1",
},
c.AddNetwork(
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT, vnet.NATPMP))
}
func hard(c *vnet.Config) *vnet.Node {
n := c.NumNodes() + 1
return c.AddNode(c.AddNetwork(
@@ -203,12 +234,18 @@ func (nt *natTest) runTest(node1, node2 addNodeFunc) pingRoute {
t.Fatalf("qemu-img create: %v, %s", err, out)
}
var envBuf bytes.Buffer
for _, e := range node.Env() {
fmt.Fprintf(&envBuf, " tailscaled.env=%s=%s", e.Key, e.Value)
}
envStr := envBuf.String()
cmd := exec.Command("qemu-system-x86_64",
"-M", "microvm,isa-serial=off",
"-m", "384M",
"-nodefaults", "-no-user-config", "-nographic",
"-kernel", nt.kernel,
"-append", "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-dd02023b0001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet tailscale-tta=1",
"-append", "console=hvc0 root=PARTUUID=60c24cc1-f3f9-427a-8199-dd02023b0001/PARTNROFF=1 ro init=/gokrazy/init panic=10 oops=panic pci=off nousb tsc=unstable clocksource=hpet tailscale-tta=1"+envStr,
"-drive", "id=blk0,file="+disk+",format=qcow2",
"-device", "virtio-blk-device,drive=blk0",
"-netdev", "stream,id=net0,addr.type=unix,addr.path="+sockAddr,
@@ -254,10 +291,20 @@ func (nt *natTest) runTest(node1, node2 addNodeFunc) pingRoute {
return fmt.Errorf("node%d status: %w", i, err)
}
t.Logf("node%d status: %v", i, st)
node := nodes[i]
if node.HostFirewall() {
if err := c.EnableHostFirewall(ctx); err != nil {
return fmt.Errorf("node%d firewall: %w", i, err)
}
t.Logf("node%d firewalled", i)
}
if err := up(ctx, c); err != nil {
return fmt.Errorf("node%d up: %w", i, err)
}
t.Logf("node%d up!", i)
st, err = c.Status(ctx)
if err != nil {
return fmt.Errorf("node%d status: %w", i, err)
@@ -408,6 +455,31 @@ func TestEasyEasy(t *testing.T) {
nt.runTest(easy, easy)
}
// Tests https://github.com/tailscale/tailscale/issues/3824 ...
// * server behind a Hard NAT
// * client behind a NAT with UPnP support
// * client machine has a stateful host firewall (e.g. ufw)
func TestBPFDisco(t *testing.T) {
nt := newNatTest(t)
nt.runTest(easyPMPFW, hard)
}
func TestHostFWNoBPF(t *testing.T) {
nt := newNatTest(t)
nt.runTest(easyPMPFWNoBPF, hard)
}
func TestHostFWPair(t *testing.T) {
nt := newNatTest(t)
nt.runTest(easyFW, easyFW)
}
func TestOneHostFW(t *testing.T) {
nt := newNatTest(t)
nt.runTest(easy, easyFW)
}
var pair = flag.String("pair", "", "comma-separated pair of types to test (easy, easyAF, hard, easyPMP, hardPMP, one2one, sameLAN)")
func TestPair(t *testing.T) {

View File

@@ -70,6 +70,16 @@ func (c *Config) AddNode(opts ...any) *Node {
o.nodes = append(o.nodes, n)
}
n.nets = append(n.nets, o)
case TailscaledEnv:
n.env = append(n.env, o)
case NodeOption:
if o == HostFirewall {
n.hostFW = true
} else {
if n.err == nil {
n.err = fmt.Errorf("unknown NodeOption %q", o)
}
}
default:
if n.err == nil {
n.err = fmt.Errorf("unknown AddNode option type %T", o)
@@ -79,6 +89,19 @@ func (c *Config) AddNode(opts ...any) *Node {
return n
}
// NodeOption is an option that can be passed to Config.AddNode.
type NodeOption string
const (
HostFirewall NodeOption = "HostFirewall"
)
// TailscaledEnv is а option that can be passed to Config.AddNode
// to set an environment variable for tailscaled.
type TailscaledEnv struct {
Key, Value string
}
// AddNetwork add a new network.
//
// The opts may be of the following types:
@@ -125,6 +148,9 @@ type Node struct {
err error
n *node // nil until NewServer called
env []TailscaledEnv
hostFW bool
// TODO(bradfitz): this is halfway converted to supporting multiple NICs
// but not done. We need a MAC-per-Network.
@@ -137,6 +163,14 @@ func (n *Node) MAC() MAC {
return n.mac
}
func (n *Node) Env() []TailscaledEnv {
return n.env
}
func (n *Node) HostFirewall() bool {
return n.hostFW
}
// Network returns the first network this node is connected to,
// or nil if none.
func (n *Node) Network() *Network {

View File

@@ -15,6 +15,7 @@ package vnet
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/binary"
@@ -61,6 +62,7 @@ import (
"tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/util/set"
"tailscale.com/util/zstdframe"
)
const nicID = 1
@@ -325,6 +327,13 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
go hs.Serve(netutil.NewOneConnListener(tc, nil))
return
}
if destPort == 443 && destIP == fakeLogCatcherIP {
r.Complete(false)
tc := gonet.NewTCPConn(&wq, ep)
go n.serveLogCatcherConn(clientRemoteIP, tc)
return
}
log.Printf("vnet-AcceptTCP: %v", stringifyTEI(reqDetails))
@@ -354,6 +363,51 @@ func (n *network) acceptTCP(r *tcp.ForwarderRequest) {
}
}
// serveLogCatchConn serves a TCP connection to "log.tailscale.io", speaking the
// logtail/logcatcher protocol.
//
// We terminate TLS with an arbitrary cert; the client is configured to not
// validate TLS certs for this hostname when running under these integration
// tests.
func (n *network) serveLogCatcherConn(clientRemoteIP netip.Addr, c net.Conn) {
tlsConfig := n.s.derps[0].tlsConfig // self-signed (stealing DERP's); test client configure to not check
tlsConn := tls.Server(c, tlsConfig)
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
all, _ := io.ReadAll(r.Body)
if r.Header.Get("Content-Encoding") == "zstd" {
var err error
all, err = zstdframe.AppendDecode(nil, all)
if err != nil {
log.Printf("LOGS DECODE ERROR zstd decode: %v", err)
http.Error(w, "zstd decode error", http.StatusBadRequest)
return
}
}
var logs []struct {
Logtail struct {
Client_Time time.Time
}
Text string
}
if err := json.Unmarshal(all, &logs); err != nil {
log.Printf("Logs decode error: %v", err)
return
}
node := n.nodesByIP[clientRemoteIP]
if node != nil {
node.logMu.Lock()
defer node.logMu.Unlock()
for _, lg := range logs {
tStr := lg.Logtail.Client_Time.Round(time.Millisecond).Format(time.RFC3339Nano)
fmt.Fprintf(&node.logBuf, "[%v] %s\n", tStr, lg.Text)
log.Printf("LOG %v [%v] %s\n", clientRemoteIP, tStr, lg.Text)
}
}
})
hs := &http.Server{Handler: handler}
hs.Serve(netutil.NewOneConnListener(tlsConn, nil))
}
var (
fakeDNSIP = netip.AddrFrom4([4]byte{4, 11, 4, 11})
fakeProxyControlplaneIP = netip.AddrFrom4([4]byte{52, 52, 0, 1}) // real controlplane.tailscale.com proxy
@@ -451,6 +505,12 @@ type node struct {
interfaceID int
net *network
lanIP netip.Addr // must be in net.lanIP prefix + unique in net
// logMu guards logBuf.
// TODO(bradfitz): conditionally write these out to separate files at the end?
// Currently they only hold logcatcher logs.
logMu sync.Mutex
logBuf bytes.Buffer
}
type derpServer struct {
@@ -1153,7 +1213,7 @@ func (s *Server) shouldInterceptTCP(pkt gopacket.Packet) bool {
dstIP, _ := netip.AddrFromSlice(ipv4.DstIP.To4())
if tcp.DstPort == 80 || tcp.DstPort == 443 {
switch dstIP {
case fakeControlIP, fakeDERP1IP, fakeDERP2IP:
case fakeControlIP, fakeDERP1IP, fakeDERP2IP, fakeLogCatcherIP:
return true
}
if dstIP == fakeProxyControlplaneIP {
@@ -1613,7 +1673,9 @@ func (s *Server) NodeAgentClient(n *Node) *NodeAgentClient {
d := s.NodeAgentDialer(n)
return &NodeAgentClient{
LocalClient: &tailscale.LocalClient{
Dial: d,
UseSocketOnly: true,
OmitAuth: true,
Dial: d,
},
HTTPClient: &http.Client{
Transport: &http.Transport{
@@ -1622,3 +1684,21 @@ func (s *Server) NodeAgentClient(n *Node) *NodeAgentClient {
},
}
}
// EnableHostFirewall enables the host's stateful firewall.
func (c *NodeAgentClient) EnableHostFirewall(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", "http://unused/fw", nil)
if err != nil {
return err
}
res, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
all, _ := io.ReadAll(res.Body)
if res.StatusCode != 200 {
return fmt.Errorf("unexpected status code %v: %s", res.Status, all)
}
return nil
}