Skip to content

Commit 8b23ba7

Browse files
committed
tstest/natlab/vnet: add qemu + Virtualization.framework protocol tests
To test how virtual machines connect to the natlab vnet code. Updates tailscale#13038 Change-Id: Ia4fd4b0c1803580ee7d94cc9878d777ad4f24f82 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent ff1d0aa commit 8b23ba7

File tree

2 files changed

+189
-15
lines changed

2 files changed

+189
-15
lines changed

tstest/natlab/vnet/vnet.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,17 @@ func (n *network) unregisterWriter(mac MAC) {
559559
n.writers.Delete(mac)
560560
}
561561

562+
// RegisteredWritersForTest returns the number of registered connections (VM
563+
// guests with a known MAC to whom a packet can be sent) there are to the
564+
// server. It exists for testing.
565+
func (s *Server) RegisteredWritersForTest() int {
566+
num := 0
567+
for n := range s.networks {
568+
num += n.writers.Len()
569+
}
570+
return num
571+
}
572+
562573
func (n *network) MACOfIP(ip netip.Addr) (_ MAC, ok bool) {
563574
if n.lanIP4.Addr() == ip {
564575
return n.mac, true
@@ -776,12 +787,12 @@ func (s *Server) writeEthernetFrameToVM(c vmClient, ethPkt []byte, interfaceID i
776787
s.scratch = binary.BigEndian.AppendUint32(s.scratch[:0], uint32(len(ethPkt)))
777788
s.scratch = append(s.scratch, ethPkt...)
778789
if _, err := c.uc.Write(s.scratch); err != nil {
779-
log.Printf("Write pkt: %v", err)
790+
s.logf("Write pkt: %v", err)
780791
}
781792

782793
case ProtocolUnixDGRAM:
783794
if _, err := c.uc.WriteToUnix(ethPkt, c.raddr); err != nil {
784-
log.Printf("Write pkt : %v", err)
795+
s.logf("Write pkt : %v", err)
785796
return
786797
}
787798
}
@@ -821,7 +832,7 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
821832
context.AfterFunc(s.shutdownCtx, func() {
822833
uc.SetDeadline(time.Now())
823834
})
824-
log.Printf("Got conn %T %p", uc, uc)
835+
s.logf("Got conn %T %p", uc, uc)
825836
defer uc.Close()
826837

827838
buf := make([]byte, 16<<10)
@@ -835,7 +846,11 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
835846
n, addr, err := uc.ReadFromUnix(buf)
836847
raddr = addr
837848
if err != nil {
838-
log.Printf("ReadFromUnix: %v", err)
849+
if s.shutdownCtx.Err() != nil {
850+
// Return without logging.
851+
return
852+
}
853+
s.logf("ReadFromUnix: %#v", err)
839854
continue
840855
}
841856
packetRaw = buf[:n]
@@ -845,7 +860,7 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
845860
// Return without logging.
846861
return
847862
}
848-
log.Printf("ReadFull header: %v", err)
863+
s.logf("ReadFull header: %v", err)
849864
return
850865
}
851866
n := binary.BigEndian.Uint32(buf[:4])
@@ -855,7 +870,7 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
855870
// Return without logging.
856871
return
857872
}
858-
log.Printf("ReadFull pkt: %v", err)
873+
s.logf("ReadFull pkt: %v", err)
859874
return
860875
}
861876
packetRaw = buf[4 : 4+n] // raw ethernet frame
@@ -869,12 +884,12 @@ func (s *Server) ServeUnixConn(uc *net.UnixConn, proto Protocol) {
869884
srcMAC := MAC(packetRaw[6:12])
870885
srcNode, ok := s.nodeByMAC[srcMAC]
871886
if !ok {
872-
log.Printf("[conn %p] got frame from unknown MAC %v", c.uc, srcMAC)
887+
s.logf("[conn %p] got frame from unknown MAC %v", c.uc, srcMAC)
873888
continue
874889
}
875890
if !didReg[srcMAC] {
876891
didReg[srcMAC] = true
877-
log.Printf("[conn %p] Registering writer for MAC %v, node %v", c.uc, srcMAC, srcNode.lanIP)
892+
s.logf("[conn %p] Registering writer for MAC %v, node %v", c.uc, srcMAC, srcNode.lanIP)
878893
srcNode.net.registerWriter(srcMAC, c)
879894
defer srcNode.net.unregisterWriter(srcMAC)
880895
}

tstest/natlab/vnet/vnet_test.go

Lines changed: 166 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ import (
1010
"fmt"
1111
"net"
1212
"net/netip"
13+
"path/filepath"
14+
"runtime"
1315
"strings"
1416
"testing"
17+
"time"
1518

1619
"github.com/google/gopacket"
1720
"github.com/google/gopacket/layers"
21+
"tailscale.com/util/must"
1822
)
1923

2024
// TestPacketSideEffects tests that upon receiving certain
@@ -32,13 +36,7 @@ func TestPacketSideEffects(t *testing.T) {
3236
}{
3337
{
3438
netName: "basic",
35-
setup: func() (*Server, error) {
36-
var c Config
37-
nw := c.AddNetwork("192.168.0.1/24")
38-
c.AddNode(nw)
39-
c.AddNode(nw)
40-
return New(&c)
41-
},
39+
setup: newTwoNodesSameNetworkServer,
4240
tests: []netTest{
4341
{
4442
name: "drop-rando-ethertype",
@@ -129,6 +127,14 @@ func mkEth(dst, src MAC, ethType layers.EthernetType, payload []byte) []byte {
129127
return append(ret, payload...)
130128
}
131129

130+
// mkLenPrefixed prepends a uint32 length to the given packet.
131+
func mkLenPrefixed(pkt []byte) []byte {
132+
ret := make([]byte, 4+len(pkt))
133+
binary.BigEndian.PutUint32(ret, uint32(len(pkt)))
134+
copy(ret[4:], pkt)
135+
return ret
136+
}
137+
132138
// mkIPv6RouterSolicit makes a IPv6 router solicitation packet
133139
// ethernet frame.
134140
func mkIPv6RouterSolicit(srcMAC MAC, srcIP netip.Addr) []byte {
@@ -230,3 +236,156 @@ func numPkts(want int) func(*sideEffects) error {
230236
return fmt.Errorf("got %d packets, want %d. packets were:\n%s", len(se.got), want, pkts.Bytes())
231237
}
232238
}
239+
240+
func newTwoNodesSameNetworkServer() (*Server, error) {
241+
var c Config
242+
nw := c.AddNetwork("192.168.0.1/24")
243+
c.AddNode(nw)
244+
c.AddNode(nw)
245+
return New(&c)
246+
}
247+
248+
// TestProtocolQEMU tests the protocol that qemu uses to connect to natlab's
249+
// vnet. (uint32-length prefixed ethernet frames over a unix stream socket)
250+
//
251+
// This test makes two clients (as qemu would act) and has one send an ethernet
252+
// packet to the other virtual LAN segment.
253+
func TestProtocolQEMU(t *testing.T) {
254+
if runtime.GOOS == "windows" {
255+
t.Skipf("skipping on %s", runtime.GOOS)
256+
}
257+
s := must.Get(newTwoNodesSameNetworkServer())
258+
defer s.Close()
259+
s.SetLoggerForTest(t.Logf)
260+
261+
td := t.TempDir()
262+
serverSock := filepath.Join(td, "vnet.sock")
263+
264+
ln, err := net.Listen("unix", serverSock)
265+
if err != nil {
266+
t.Fatal(err)
267+
}
268+
defer ln.Close()
269+
270+
var clientc [2]*net.UnixConn
271+
for i := range clientc {
272+
c, err := net.Dial("unix", serverSock)
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
defer c.Close()
277+
clientc[i] = c.(*net.UnixConn)
278+
}
279+
280+
for range clientc {
281+
conn, err := ln.Accept()
282+
if err != nil {
283+
t.Fatal(err)
284+
}
285+
go s.ServeUnixConn(conn.(*net.UnixConn), ProtocolQEMU)
286+
}
287+
288+
sendBetweenClients(t, clientc, s, mkLenPrefixed)
289+
}
290+
291+
// TestProtocolUnixDgram tests the protocol that macOS Virtualization.framework
292+
// uses to connect to vnet. (unix datagram sockets)
293+
//
294+
// It is similar to TestProtocolQEMU but uses unix datagram sockets instead of
295+
// streams.
296+
func TestProtocolUnixDgram(t *testing.T) {
297+
if runtime.GOOS == "windows" {
298+
t.Skipf("skipping on %s", runtime.GOOS)
299+
}
300+
s := must.Get(newTwoNodesSameNetworkServer())
301+
defer s.Close()
302+
s.SetLoggerForTest(t.Logf)
303+
304+
td := t.TempDir()
305+
serverSock := filepath.Join(td, "vnet.sock")
306+
serverAddr := must.Get(net.ResolveUnixAddr("unixgram", serverSock))
307+
308+
var clientSock [2]string
309+
for i := range clientSock {
310+
clientSock[i] = filepath.Join(td, fmt.Sprintf("c%d.sock", i))
311+
}
312+
313+
uc, err := net.ListenUnixgram("unixgram", serverAddr)
314+
if err != nil {
315+
t.Fatal(err)
316+
}
317+
go s.ServeUnixConn(uc, ProtocolUnixDGRAM)
318+
319+
var clientc [2]*net.UnixConn
320+
for i := range clientc {
321+
c, err := net.DialUnix("unixgram",
322+
must.Get(net.ResolveUnixAddr("unixgram", clientSock[i])),
323+
serverAddr)
324+
if err != nil {
325+
t.Fatal(err)
326+
}
327+
defer c.Close()
328+
clientc[i] = c
329+
}
330+
331+
sendBetweenClients(t, clientc, s, nil)
332+
}
333+
334+
// sendBetweenClients is a test helper that tries to send an ethernet frame from
335+
// one client to another.
336+
//
337+
// It first makes the two clients send a packet to a fictitious node 3, which
338+
// forces their src MACs to be registered with a networkWriter internally so
339+
// they can receive traffic.
340+
//
341+
// Normally a node starts up spamming DHCP + NDP but we don't get that as a side
342+
// effect here, so this does it manually.
343+
//
344+
// It also then waits for them to be registered.
345+
//
346+
// wrap is an optional function that wraps the packet before sending it.
347+
func sendBetweenClients(t testing.TB, clientc [2]*net.UnixConn, s *Server, wrap func([]byte) []byte) {
348+
t.Helper()
349+
if wrap == nil {
350+
wrap = func(b []byte) []byte { return b }
351+
}
352+
for i, c := range clientc {
353+
must.Get(c.Write(wrap(mkEth(nodeMac(3), nodeMac(i+1), testingEthertype, []byte("hello")))))
354+
}
355+
awaitCond(t, 5*time.Second, func() error {
356+
if n := s.RegisteredWritersForTest(); n != 2 {
357+
return fmt.Errorf("got %d registered writers, want 2", n)
358+
}
359+
return nil
360+
})
361+
362+
// Now see if node1 can write to node2 and node2 receives it.
363+
pkt := wrap(mkEth(nodeMac(2), nodeMac(1), testingEthertype, []byte("test-msg")))
364+
t.Logf("writing % 02x", pkt)
365+
must.Get(clientc[0].Write(pkt))
366+
367+
buf := make([]byte, len(pkt))
368+
clientc[1].SetReadDeadline(time.Now().Add(5 * time.Second))
369+
n, err := clientc[1].Read(buf)
370+
if err != nil {
371+
t.Fatal(err)
372+
}
373+
got := buf[:n]
374+
if !bytes.Equal(got, pkt) {
375+
t.Errorf("bad packet\n got: % 02x\nwant: % 02x", got, pkt)
376+
}
377+
}
378+
379+
func awaitCond(t testing.TB, timeout time.Duration, cond func() error) {
380+
t.Helper()
381+
t0 := time.Now()
382+
for {
383+
if err := cond(); err == nil {
384+
return
385+
}
386+
if time.Since(t0) > timeout {
387+
t.Fatalf("timed out after %v", timeout)
388+
}
389+
time.Sleep(10 * time.Millisecond)
390+
}
391+
}

0 commit comments

Comments
 (0)