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

@@ -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
}