tstest/natlab/vnet: add pcap support

Updates #13038

Change-Id: I89ce2129fee856f97986d6313d2b661c76476c0c
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2024-08-08 20:11:50 -07:00 committed by Maisem Ali
parent d0e8375b53
commit d4cc074187
4 changed files with 123 additions and 9 deletions

View File

@ -33,7 +33,10 @@
"tailscale.com/tstest/natlab/vnet" "tailscale.com/tstest/natlab/vnet"
) )
var logTailscaled = flag.Bool("log-tailscaled", false, "log tailscaled output") var (
logTailscaled = flag.Bool("log-tailscaled", false, "log tailscaled output")
pcapFile = flag.String("pcap", "", "write pcap to file")
)
type natTest struct { type natTest struct {
tb testing.TB tb testing.TB
@ -142,6 +145,7 @@ func (nt *natTest) runTest(node1, node2 addNodeFunc) pingRoute {
t := nt.tb t := nt.tb
var c vnet.Config var c vnet.Config
c.SetPCAPFile(*pcapFile)
nodes := []*vnet.Node{ nodes := []*vnet.Node{
node1(&c), node1(&c),
node2(&c), node2(&c),

View File

@ -8,8 +8,11 @@
"fmt" "fmt"
"log" "log"
"net/netip" "net/netip"
"os"
"slices" "slices"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcapgo"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
@ -27,6 +30,11 @@
type Config struct { type Config struct {
nodes []*Node nodes []*Node
networks []*Network networks []*Network
pcapFile string
}
func (c *Config) SetPCAPFile(file string) {
c.pcapFile = file
} }
func (c *Config) NumNodes() int { func (c *Config) NumNodes() int {
@ -183,6 +191,21 @@ func (n *Network) AddService(s NetworkService) {
// there were any configuration issues. // there were any configuration issues.
func (s *Server) initFromConfig(c *Config) error { func (s *Server) initFromConfig(c *Config) error {
netOfConf := map[*Network]*network{} netOfConf := map[*Network]*network{}
if c.pcapFile != "" {
pcf, err := os.OpenFile(c.pcapFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
nw, err := pcapgo.NewNgWriter(pcf, layers.LinkTypeEthernet)
if err != nil {
return err
}
pw := &pcapWriter{
f: pcf,
w: nw,
}
s.pcapWriter = pw
}
for _, conf := range c.networks { for _, conf := range c.networks {
if conf.err != nil { if conf.err != nil {
return conf.err return conf.err
@ -206,14 +229,21 @@ func (s *Server) initFromConfig(c *Config) error {
} }
s.networkByWAN[conf.wanIP] = n s.networkByWAN[conf.wanIP] = n
} }
for _, conf := range c.nodes { for i, conf := range c.nodes {
if conf.err != nil { if conf.err != nil {
return conf.err return conf.err
} }
n := &node{ n := &node{
mac: conf.mac, mac: conf.mac,
id: i + 1,
net: netOfConf[conf.Network()], net: netOfConf[conf.Network()],
} }
if s.pcapWriter != nil {
s.pcapWriter.w.AddInterface(pcapgo.NgInterface{
Name: fmt.Sprintf("node%d", n.id),
LinkType: layers.LinkTypeEthernet,
})
}
conf.n = n conf.n = n
if _, ok := s.nodeByMAC[n.mac]; ok { if _, ok := s.nodeByMAC[n.mac]; ok {
return fmt.Errorf("two nodes have the same MAC %v", n.mac) return fmt.Errorf("two nodes have the same MAC %v", n.mac)

View File

@ -0,0 +1,39 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package vnet
import (
"io"
"os"
"sync"
"github.com/google/gopacket"
"github.com/google/gopacket/pcapgo"
)
type pcapWriter struct {
f *os.File
mu sync.Mutex
w *pcapgo.NgWriter
}
func (p *pcapWriter) WritePacket(ci gopacket.CaptureInfo, data []byte) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.w == nil {
return io.ErrClosedPipe
}
return p.w.WritePacket(ci, data)
}
func (p *pcapWriter) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.w != nil {
p.w.Flush()
p.w = nil
}
return p.f.Close()
}

View File

@ -31,6 +31,7 @@
"os/exec" "os/exec"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/google/gopacket" "github.com/google/gopacket"
@ -58,6 +59,7 @@
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/mak" "tailscale.com/util/mak"
"tailscale.com/util/must"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
@ -444,6 +446,7 @@ func (n *network) MACOfIP(ip netip.Addr) (_ MAC, ok bool) {
type node struct { type node struct {
mac MAC mac MAC
id int
net *network net *network
lanIP netip.Addr // must be in net.lanIP prefix + unique in net lanIP netip.Addr // must be in net.lanIP prefix + unique in net
} }
@ -474,6 +477,8 @@ func newDERPServer() *derpServer {
type Server struct { type Server struct {
shutdownCtx context.Context shutdownCtx context.Context
shutdownCancel context.CancelFunc shutdownCancel context.CancelFunc
shuttingDown atomic.Bool
wg sync.WaitGroup
blendReality bool blendReality bool
derpIPs set.Set[netip.Addr] derpIPs set.Set[netip.Addr]
@ -485,6 +490,7 @@ type Server struct {
control *testcontrol.Server control *testcontrol.Server
derps []*derpServer derps []*derpServer
pcapWriter *pcapWriter
mu sync.Mutex mu sync.Mutex
agentConnWaiter map[*node]chan<- struct{} // signaled after added to set agentConnWaiter map[*node]chan<- struct{} // signaled after added to set
@ -562,7 +568,13 @@ func New(c *Config) (*Server, error) {
} }
func (s *Server) Close() { func (s *Server) Close() {
if shutdown := s.shuttingDown.Swap(true); !shutdown {
s.shutdownCancel() s.shutdownCancel()
if s.pcapWriter != nil {
s.pcapWriter.Close()
}
}
s.wg.Wait()
} }
func (s *Server) HWAddr(mac MAC) net.HardwareAddr { func (s *Server) HWAddr(mac MAC) net.HardwareAddr {
@ -601,11 +613,20 @@ func (s *Server) IPv4ForDNS(qname string) (netip.Addr, bool) {
// serveConn serves a single connection from a client. // serveConn serves a single connection from a client.
func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) { func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
if s.shuttingDown.Load() {
return
}
s.wg.Add(1)
defer s.wg.Done()
context.AfterFunc(s.shutdownCtx, func() {
uc.SetDeadline(time.Now())
})
log.Printf("Got conn %T %p", uc, uc) log.Printf("Got conn %T %p", uc, uc)
defer uc.Close() defer uc.Close()
bw := bufio.NewWriterSize(uc, 2<<10) bw := bufio.NewWriterSize(uc, 2<<10)
var writeMu sync.Mutex var writeMu sync.Mutex
var srcNode *node
writePkt := func(pkt []byte) { writePkt := func(pkt []byte) {
if pkt == nil { if pkt == nil {
return return
@ -626,10 +647,20 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
if err := bw.Flush(); err != nil { if err := bw.Flush(); err != nil {
log.Printf("Flush: %v", err) log.Printf("Flush: %v", err)
} }
if s.pcapWriter != nil {
ci := gopacket.CaptureInfo{
Timestamp: time.Now(),
CaptureLength: len(pkt),
Length: len(pkt),
}
if srcNode != nil {
ci.InterfaceIndex = srcNode.id
}
must.Do(s.pcapWriter.WritePacket(ci, pkt))
}
} }
buf := make([]byte, 16<<10) buf := make([]byte, 16<<10)
var srcNode *node
var netw *network // non-nil after first packet var netw *network // non-nil after first packet
for { for {
var packetRaw []byte var packetRaw []byte
@ -686,6 +717,17 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
continue continue
} }
} }
if s.pcapWriter != nil {
ci := gopacket.CaptureInfo{
Timestamp: time.Now(),
CaptureLength: len(packetRaw),
Length: len(packetRaw),
}
if srcNode != nil {
ci.InterfaceIndex = srcNode.id
}
must.Do(s.pcapWriter.WritePacket(ci, packetRaw))
}
netw.HandleEthernetPacket(ep) netw.HandleEthernetPacket(ep)
} }
} }
@ -840,7 +882,6 @@ func (n *network) WriteUDPPacketNoNAT(p UDPPacket) {
// IP may be the router's IP, or an internet (routed) IP. // IP may be the router's IP, or an internet (routed) IP.
func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) { func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) {
packet := ep.gp packet := ep.gp
writePkt := n.writeEth
v4, ok := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4) v4, ok := packet.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
if !ok { if !ok {
@ -857,7 +898,7 @@ func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) {
n.logf("createDHCPResponse: %v", err) n.logf("createDHCPResponse: %v", err)
return return
} }
writePkt(res) n.writeEth(res)
return return
} }
@ -874,7 +915,7 @@ func (n *network) HandleEthernetIPv4PacketForRouter(ep EthernetPacket) {
n.logf("createDNSResponse: %v", err) n.logf("createDNSResponse: %v", err)
return return
} }
writePkt(res) n.writeEth(res)
return return
} }