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
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/binary"
"github.com/google/nftables"
"github.com/google/nftables/expr"
"tailscale.com/types/ptr"
)
func init() {
addFirewall = addFirewallLinux
}
func addFirewallLinux() error {
c, err := nftables.New()
if err != nil {
return err
}
// Create a new table
table := &nftables.Table{
Family: nftables.TableFamilyIPv4, // TableFamilyINet doesn't work (why?. oh well.)
Name: "filter",
}
c.AddTable(table)
// Create a new chain for incoming traffic
inputChain := &nftables.Chain{
Name: "input",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
Policy: ptr.To(nftables.ChainPolicyDrop),
}
c.AddChain(inputChain)
// Allow traffic from the loopback interface
c.AddRule(&nftables.Rule{
Table: table,
Chain: inputChain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte("lo"),
},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
})
// Accept established and related connections
c.AddRule(&nftables.Rule{
Table: table,
Chain: inputChain,
Exprs: []expr.Any{
&expr.Ct{
Register: 1,
Key: expr.CtKeySTATE,
},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 4,
Mask: binary.NativeEndian.AppendUint32(nil, 0x06), // CT_STATE_BIT_ESTABLISHED | CT_STATE_BIT_RELATED
Xor: binary.NativeEndian.AppendUint32(nil, 0),
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: binary.NativeEndian.AppendUint32(nil, 0x00),
},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
})
// Allow TCP packets in that don't have the SYN bit set, even if they're not
// ESTABLISHED or RELATED. This is because the test suite gets TCP
// connections up & idle (for HTTP) before it conditionally installs these
// firewall rules. But because conntrack wasn't previously active, existing
// TCP flows aren't ESTABLISHED and get dropped. So this rule allows
// previously established TCP connections that predates the firewall rules
// to continue working, as they don't have conntrack state.
c.AddRule(&nftables.Rule{
Table: table,
Chain: inputChain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{0x06}, // TCP
},
&expr.Payload{ // get TCP flags
DestRegister: 1,
Base: 2,
Offset: 13, // flags
Len: 1,
},
&expr.Bitwise{
SourceRegister: 1,
DestRegister: 1,
Len: 1,
Mask: []byte{2}, // TCP_SYN
Xor: []byte{0},
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 1,
Data: []byte{2}, // TCP_SYN
},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
})
return c.Flush()
}
+34 -2
View File
@@ -130,13 +130,14 @@ func main() {
hs.ConnState = func(c net.Conn, s http.ConnState) {
stMu.Lock()
defer stMu.Unlock()
oldLen := len(newSet)
switch s {
case http.StateNew:
newSet.Add(c)
default:
newSet.Delete(c)
}
if len(newSet) == 0 {
if oldLen != 0 && len(newSet) == 0 {
select {
case needConnCh <- true:
default:
@@ -147,7 +148,12 @@ func main() {
lcRP := httputil.NewSingleHostReverseProxy(must.Get(url.Parse("http://local-tailscaled.sock")))
lcRP.Transport = new(localClientRoundTripper)
ttaMux.Handle("/localapi/", lcRP)
ttaMux.HandleFunc("/localapi/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Got localapi request: %v", r.URL)
t0 := time.Now()
lcRP.ServeHTTP(w, r)
log.Printf("Did localapi request in %v: %v", time.Since(t0).Round(time.Millisecond), r.URL)
})
ttaMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "TTA\n")
@@ -156,8 +162,19 @@ func main() {
ttaMux.HandleFunc("/up", func(w http.ResponseWriter, r *http.Request) {
serveCmd(w, "tailscale", "up", "--login-server=http://control.tailscale")
})
ttaMux.HandleFunc("/fw", addFirewallHandler)
go hs.Serve(chanListener(conns))
// For doing agent operations locally from gokrazy:
// (e.g. with "wget -O - localhost:8123/fw")
go func() {
err := http.ListenAndServe("127.0.0.1:8123", &ttaMux)
if err != nil {
log.Fatalf("ListenAndServe: %v", err)
}
}()
var lastErr string
needConnCh <- true
for {
@@ -204,3 +221,18 @@ func (cl chanListener) Addr() net.Addr {
Port: 123,
}
}
func addFirewallHandler(w http.ResponseWriter, r *http.Request) {
if addFirewall == nil {
http.Error(w, "firewall not supported", 500)
return
}
err := addFirewall()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.WriteString(w, "OK\n")
}
var addFirewall func() error // set by fw_linux.go
+1 -1
View File
@@ -7,7 +7,7 @@ qemu-system-x86_64 -M microvm,isa-serial=off \
-m 1G \
-nodefaults -no-user-config -nographic \
-kernel $HOME/src/github.com/tailscale/gokrazy-kernel/vmlinuz \
-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 tailscaled.env=TS_DEBUG_RAW_DISCO=1" \
-drive id=blk0,file=$HOME/src/tailscale.com/gokrazy/tsapp.img,format=raw \
-device virtio-blk-device,drive=blk0 \
-netdev stream,id=net0,addr.type=unix,addr.path=/tmp/qemu.sock \