mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
3d9e3a17fa
No need to make callers specify the redundant IP version or TTL/HopLimit or EthernetType in the common case. The mkPacket helper can set those when unset. And use the mkIPLayer in another place, simplifying some code. And rename mkPacketErr to just mkPacket, then move mkPacket to test-only code, as mustPacket. Updates #13038 Change-Id: Ic216e44dda760c69ab9bfc509370040874a47d30 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
665 lines
18 KiB
Go
665 lines
18 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package vnet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
const (
|
|
ethType4 = layers.EthernetTypeIPv4
|
|
ethType6 = layers.EthernetTypeIPv6
|
|
)
|
|
|
|
// TestPacketSideEffects tests that upon receiving certain
|
|
// packets, other packets and/or log statements are generated.
|
|
func TestPacketSideEffects(t *testing.T) {
|
|
type netTest struct {
|
|
name string
|
|
pkt []byte // to send
|
|
check func(*sideEffects) error
|
|
}
|
|
tests := []struct {
|
|
netName string // name of the Server returned by setup
|
|
setup func() (*Server, error)
|
|
tests []netTest // to run against setup's Server
|
|
}{
|
|
{
|
|
netName: "basic",
|
|
setup: newTwoNodesSameNetwork,
|
|
tests: []netTest{
|
|
{
|
|
name: "drop-rando-ethertype",
|
|
pkt: mkEth(nodeMac(2), nodeMac(1), 0x4321, []byte("hello")),
|
|
check: all(
|
|
logSubstr("Dropping non-IP packet"),
|
|
),
|
|
},
|
|
{
|
|
name: "dst-mac-between-nodes",
|
|
pkt: mkEth(nodeMac(2), nodeMac(1), testingEthertype, []byte("hello")),
|
|
check: all(
|
|
numPkts(1),
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=52:cc:cc:cc:cc:02 EthernetType=UnknownEthernetType"),
|
|
pktSubstr("Unable to decode EthernetType 4660"),
|
|
),
|
|
},
|
|
{
|
|
name: "broadcast-mac",
|
|
pkt: mkEth(macBroadcast, nodeMac(1), testingEthertype, []byte("hello")),
|
|
check: all(
|
|
numPkts(1),
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=ff:ff:ff:ff:ff:ff EthernetType=UnknownEthernetType"),
|
|
pktSubstr("Unable to decode EthernetType 4660"),
|
|
),
|
|
},
|
|
{
|
|
name: "dns-request-v4",
|
|
pkt: mkDNSReq(4),
|
|
check: all(
|
|
numPkts(1),
|
|
pktSubstr("Data=[52, 52, 0, 3] IP=52.52.0.3"),
|
|
),
|
|
},
|
|
{
|
|
name: "dns-request-v6",
|
|
pkt: mkDNSReq(6),
|
|
check: all(
|
|
numPkts(1),
|
|
pktSubstr(" IP=2052::3 "),
|
|
),
|
|
},
|
|
{
|
|
name: "syslog-v4",
|
|
pkt: mkSyslogPacket(clientIPv4(1), "<6>2024-08-30T10:36:06-07:00 natlabapp tailscaled[1]: 2024/08/30 10:36:06 some-message"),
|
|
check: all(
|
|
numPkts(0),
|
|
logSubstr("some-message"),
|
|
),
|
|
},
|
|
{
|
|
name: "syslog-v6",
|
|
pkt: mkSyslogPacket(nodeWANIP6(1), "<6>2024-08-30T10:36:06-07:00 natlabapp tailscaled[1]: 2024/08/30 10:36:06 some-message"),
|
|
check: all(
|
|
numPkts(0),
|
|
logSubstr("some-message"),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
netName: "v4",
|
|
setup: newTwoNodesSameV4Network,
|
|
tests: []netTest{
|
|
{
|
|
name: "no-v6-reply-on-v4-only",
|
|
pkt: mkIPv6RouterSolicit(nodeMac(1), nodeLANIP6(1)),
|
|
check: all(
|
|
numPkts(0),
|
|
logSubstr("dropping IPv6 packet on v4-only network"),
|
|
),
|
|
},
|
|
{
|
|
name: "dhcp-discover",
|
|
pkt: mkDHCP(nodeMac(1), layers.DHCPMsgTypeDiscover),
|
|
check: all(
|
|
numPkts(2), // DHCP discover broadcast to node2 also, and the DHCP reply from router
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=ff:ff:ff:ff:ff:ff"),
|
|
pktSubstr("Options=[Option(ServerID:192.168.0.1), Option(MessageType:Offer)]}"),
|
|
),
|
|
},
|
|
{
|
|
name: "dhcp-request",
|
|
pkt: mkDHCP(nodeMac(1), layers.DHCPMsgTypeRequest),
|
|
check: all(
|
|
numPkts(2), // DHCP discover broadcast to node2 also, and the DHCP reply from router
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=ff:ff:ff:ff:ff:ff"),
|
|
pktSubstr("YourClientIP=192.168.0.101"),
|
|
pktSubstr("Options=[Option(ServerID:192.168.0.1), Option(MessageType:Ack), Option(LeaseTime:3600), Option(Router:[192 168 0 1]), Option(DNS:[4 11 4 11]), Option(SubnetMask:255.255.255.0)]}"),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
netName: "v6",
|
|
setup: func() (*Server, error) {
|
|
var c Config
|
|
nw := c.AddNetwork("2000:52::1/64")
|
|
c.AddNode(nw)
|
|
c.AddNode(nw)
|
|
return New(&c)
|
|
},
|
|
tests: []netTest{
|
|
{
|
|
name: "router-solicit",
|
|
pkt: mkIPv6RouterSolicit(nodeMac(1), nodeLANIP6(1)),
|
|
check: all(
|
|
logSubstr("sending IPv6 router advertisement to 52:cc:cc:cc:cc:01 from 52:ee:ee:ee:ee:01"),
|
|
numPkts(1),
|
|
pktSubstr("TypeCode=RouterAdvertisement"),
|
|
pktSubstr("HopLimit=255 "), // per RFC 4861, 7.1.1 etc (all NDP messages)
|
|
pktSubstr("= ICMPv6RouterAdvertisement"),
|
|
pktSubstr("SrcMAC=52:ee:ee:ee:ee:01 DstMAC=52:cc:cc:cc:cc:01 EthernetType=IPv6"),
|
|
),
|
|
},
|
|
{
|
|
name: "all-nodes",
|
|
pkt: mkAllNodesPing(nodeMac(1), nodeLANIP6(1)),
|
|
check: all(
|
|
numPkts(1),
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=33:33:00:00:00:01"),
|
|
pktSubstr("SrcIP=fe80::50cc:ccff:fecc:cc01 DstIP=ff02::1"),
|
|
pktSubstr("TypeCode=EchoRequest"),
|
|
),
|
|
},
|
|
{
|
|
name: "no-dhcp-on-v6-disco",
|
|
pkt: mkDHCP(nodeMac(1), layers.DHCPMsgTypeDiscover),
|
|
check: all(
|
|
numPkts(1), // DHCP discover broadcast to node2 only
|
|
logSubstr("dropping DHCPv4 packet on v6-only network"),
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=ff:ff:ff:ff:ff:ff"),
|
|
),
|
|
},
|
|
{
|
|
name: "no-dhcp-on-v6-request",
|
|
pkt: mkDHCP(nodeMac(1), layers.DHCPMsgTypeRequest),
|
|
check: all(
|
|
numPkts(1), // DHCP request broadcast to node2 only
|
|
pktSubstr("SrcMAC=52:cc:cc:cc:cc:01 DstMAC=ff:ff:ff:ff:ff:ff"),
|
|
logSubstr("dropping DHCPv4 packet on v6-only network"),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.netName, func(t *testing.T) {
|
|
s, err := tt.setup()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
for _, tt := range tt.tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
se := newSideEffects(s)
|
|
|
|
if err := s.handleEthernetFrameFromVM(tt.pkt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tt.check != nil {
|
|
if err := tt.check(se); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
if t.Failed() {
|
|
t.Logf("logs were:\n%s", strings.Join(se.logs, "\n"))
|
|
for i, rp := range se.got {
|
|
p := gopacket.NewPacket(rp.eth, layers.LayerTypeEthernet, gopacket.Lazy)
|
|
got := p.String()
|
|
t.Logf("[pkt%d, port %v]:\n%s\n", i, rp.port, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
// mustPacket is like mkPacket but panics on error.
|
|
func mustPacket(layers ...gopacket.SerializableLayer) []byte {
|
|
return must.Get(mkPacket(layers...))
|
|
}
|
|
|
|
// mkEth encodes an ethernet frame with the given payload.
|
|
func mkEth(dst, src MAC, ethType layers.EthernetType, payload []byte) []byte {
|
|
ret := make([]byte, 0, 14+len(payload))
|
|
ret = append(ret, dst.HWAddr()...)
|
|
ret = append(ret, src.HWAddr()...)
|
|
ret = binary.BigEndian.AppendUint16(ret, uint16(ethType))
|
|
return append(ret, payload...)
|
|
}
|
|
|
|
// mkLenPrefixed prepends a uint32 length to the given packet.
|
|
func mkLenPrefixed(pkt []byte) []byte {
|
|
ret := make([]byte, 4+len(pkt))
|
|
binary.BigEndian.PutUint32(ret, uint32(len(pkt)))
|
|
copy(ret[4:], pkt)
|
|
return ret
|
|
}
|
|
|
|
// mkIPv6RouterSolicit makes a IPv6 router solicitation packet
|
|
// ethernet frame.
|
|
func mkIPv6RouterSolicit(srcMAC MAC, srcIP netip.Addr) []byte {
|
|
ip := &layers.IPv6{
|
|
Version: 6,
|
|
HopLimit: 255,
|
|
NextHeader: layers.IPProtocolICMPv6,
|
|
SrcIP: srcIP.AsSlice(),
|
|
DstIP: net.ParseIP("ff02::2"), // all routers
|
|
}
|
|
icmp := &layers.ICMPv6{
|
|
TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeRouterSolicitation, 0),
|
|
}
|
|
|
|
ra := &layers.ICMPv6RouterSolicitation{
|
|
Options: []layers.ICMPv6Option{{
|
|
Type: layers.ICMPv6OptSourceAddress,
|
|
Data: srcMAC.HWAddr(),
|
|
}},
|
|
}
|
|
icmp.SetNetworkLayerForChecksum(ip)
|
|
return mkEth(macAllRouters, srcMAC, ethType6, mustPacket(ip, icmp, ra))
|
|
}
|
|
|
|
func mkAllNodesPing(srcMAC MAC, srcIP netip.Addr) []byte {
|
|
ip := &layers.IPv6{
|
|
Version: 6,
|
|
HopLimit: 255,
|
|
NextHeader: layers.IPProtocolICMPv6,
|
|
SrcIP: srcIP.AsSlice(),
|
|
DstIP: net.ParseIP("ff02::1"), // all nodes
|
|
}
|
|
icmp := &layers.ICMPv6{
|
|
TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeEchoRequest, 0),
|
|
}
|
|
icmp.SetNetworkLayerForChecksum(ip)
|
|
return mkEth(macAllNodes, srcMAC, ethType6, mustPacket(ip, icmp))
|
|
}
|
|
|
|
// mkDNSReq makes a DNS request to "control.tailscale" using the source IPs as
|
|
// defined in this test file.
|
|
//
|
|
// ipVer must be 4 or 6:
|
|
// If 4, it makes an A record request.
|
|
// If 6, it makes a AAAA record request.
|
|
//
|
|
// (Yes, this is technically unrelated (you can request A records over IPv6 or
|
|
// AAAA records over IPv4), but for test coverage reasons, assume that the ipVer
|
|
// of 6 means to also request an AAAA record.)
|
|
func mkDNSReq(ipVer int) []byte {
|
|
eth := &layers.Ethernet{
|
|
SrcMAC: nodeMac(1).HWAddr(),
|
|
DstMAC: routerMac(1).HWAddr(),
|
|
EthernetType: layers.EthernetTypeIPv4,
|
|
}
|
|
if ipVer == 6 {
|
|
eth.EthernetType = layers.EthernetTypeIPv6
|
|
}
|
|
|
|
var ip serializableNetworkLayer
|
|
switch ipVer {
|
|
case 4:
|
|
ip = &layers.IPv4{
|
|
Version: 4,
|
|
Protocol: layers.IPProtocolUDP,
|
|
SrcIP: clientIPv4(1).AsSlice(),
|
|
TTL: 64,
|
|
DstIP: FakeDNSIPv4().AsSlice(),
|
|
}
|
|
case 6:
|
|
ip = &layers.IPv6{
|
|
Version: 6,
|
|
HopLimit: 64,
|
|
NextHeader: layers.IPProtocolUDP,
|
|
SrcIP: net.ParseIP("2000:52::1"),
|
|
DstIP: FakeDNSIPv6().AsSlice(),
|
|
}
|
|
default:
|
|
panic("bad ipVer")
|
|
}
|
|
|
|
udp := &layers.UDP{
|
|
SrcPort: 12345,
|
|
DstPort: 53,
|
|
}
|
|
udp.SetNetworkLayerForChecksum(ip)
|
|
dns := &layers.DNS{
|
|
ID: 789,
|
|
Questions: []layers.DNSQuestion{{
|
|
Name: []byte("control.tailscale"),
|
|
Type: layers.DNSTypeA,
|
|
Class: layers.DNSClassIN,
|
|
}},
|
|
}
|
|
if ipVer == 6 {
|
|
dns.Questions[0].Type = layers.DNSTypeAAAA
|
|
}
|
|
return mustPacket(eth, ip, udp, dns)
|
|
}
|
|
|
|
func mkDHCP(srcMAC MAC, typ layers.DHCPMsgType) []byte {
|
|
eth := &layers.Ethernet{
|
|
SrcMAC: srcMAC.HWAddr(),
|
|
DstMAC: macBroadcast.HWAddr(),
|
|
EthernetType: layers.EthernetTypeIPv4,
|
|
}
|
|
ip := &layers.IPv4{
|
|
Version: 4,
|
|
Protocol: layers.IPProtocolUDP,
|
|
SrcIP: net.ParseIP("0.0.0.0"),
|
|
DstIP: net.ParseIP("255.255.255.255"),
|
|
}
|
|
udp := &layers.UDP{
|
|
SrcPort: 68,
|
|
DstPort: 67,
|
|
}
|
|
dhcp := &layers.DHCPv4{
|
|
Operation: layers.DHCPOpRequest,
|
|
HardwareType: layers.LinkTypeEthernet,
|
|
HardwareLen: 6,
|
|
Xid: 0,
|
|
Secs: 0,
|
|
Flags: 0,
|
|
ClientHWAddr: srcMAC[:],
|
|
Options: []layers.DHCPOption{
|
|
{Type: layers.DHCPOptMessageType, Length: 1, Data: []byte{byte(typ)}},
|
|
},
|
|
}
|
|
return mustPacket(eth, ip, udp, dhcp)
|
|
}
|
|
|
|
func mkSyslogPacket(srcIP netip.Addr, msg string) []byte {
|
|
eth := &layers.Ethernet{
|
|
SrcMAC: nodeMac(1).HWAddr(),
|
|
DstMAC: routerMac(1).HWAddr(),
|
|
}
|
|
ip := mkIPLayer(layers.IPProtocolUDP, srcIP, matchingIP(srcIP, FakeSyslogIPv4(), FakeSyslogIPv6()))
|
|
udp := &layers.UDP{
|
|
SrcPort: 123,
|
|
DstPort: 456, // unused; only IP matches
|
|
}
|
|
return mustPacket(eth, ip, udp, gopacket.Payload([]byte(msg)))
|
|
}
|
|
|
|
// matchingIP returns ip4 if toMatch is an IPv4 address, otherwise ip6.
|
|
func matchingIP(toMatch, if4, if6 netip.Addr) netip.Addr {
|
|
if toMatch.Is4() {
|
|
return if4
|
|
}
|
|
return if6
|
|
}
|
|
|
|
// receivedPacket is an ethernet frame that was received during a test.
|
|
type receivedPacket struct {
|
|
port MAC // MAC address of client that received the packet
|
|
eth []byte // ethernet frame; dst MAC might be ff:ff:ff:ff:ff:ff, etc
|
|
}
|
|
|
|
// sideEffects gathers side effects as a result of sending a packet and tests
|
|
// whether those effects were as desired.
|
|
type sideEffects struct {
|
|
logs []string
|
|
got []receivedPacket // ethernet packets received
|
|
}
|
|
|
|
// newSideEffects creates a new sideEffects recorder, registering itself with s.
|
|
func newSideEffects(s *Server) *sideEffects {
|
|
se := &sideEffects{}
|
|
s.SetLoggerForTest(se.logf)
|
|
for mac := range s.MACs() {
|
|
s.RegisterSinkForTest(mac, func(eth []byte) {
|
|
se.got = append(se.got, receivedPacket{
|
|
port: mac,
|
|
eth: eth,
|
|
})
|
|
})
|
|
}
|
|
return se
|
|
}
|
|
|
|
func (se *sideEffects) logf(format string, args ...any) {
|
|
se.logs = append(se.logs, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// all aggregates several side effects checkers into one.
|
|
func all(checks ...func(*sideEffects) error) func(*sideEffects) error {
|
|
return func(se *sideEffects) error {
|
|
var errs []error
|
|
for _, check := range checks {
|
|
if err := check(se); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
}
|
|
|
|
// logSubstr returns a side effect checker func that checks
|
|
// whether a log statement was output containing substring sub.
|
|
func logSubstr(sub string) func(*sideEffects) error {
|
|
return func(se *sideEffects) error {
|
|
for _, log := range se.logs {
|
|
if strings.Contains(log, sub) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("expected log substring %q not found", sub)
|
|
}
|
|
}
|
|
|
|
// pkgSubstr returns a side effect checker func that checks whether an ethernet
|
|
// packet was received that, once decoded and stringified by gopacket, contains
|
|
// substring sub.
|
|
func pktSubstr(sub string) func(*sideEffects) error {
|
|
return func(se *sideEffects) error {
|
|
for _, pkt := range se.got {
|
|
pkt := gopacket.NewPacket(pkt.eth, layers.LayerTypeEthernet, gopacket.Lazy)
|
|
got := pkt.String()
|
|
if strings.Contains(got, sub) {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("packet summary with substring %q not found", sub)
|
|
}
|
|
}
|
|
|
|
// numPkts returns a side effect checker func that checks whether
|
|
// the received number of ethernet packets was the given number.
|
|
func numPkts(want int) func(*sideEffects) error {
|
|
return func(se *sideEffects) error {
|
|
if len(se.got) == want {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("got %d packets, want %d", len(se.got), want)
|
|
}
|
|
}
|
|
|
|
func clientIPv4(n int) netip.Addr {
|
|
return netip.AddrFrom4([4]byte{192, 168, 0, byte(100 + n)})
|
|
}
|
|
|
|
var wanSLAACBase = netip.MustParseAddr("2052::50cc:ccff:fecc:cc01")
|
|
|
|
// nodeLANIP6 returns a node number's Link Local SLAAC IPv6 address,
|
|
// such as fe80::50cc:ccff:fecc:cc03 for node 3.
|
|
func nodeWANIP6(n int) netip.Addr {
|
|
a := wanSLAACBase.As16()
|
|
a[15] = byte(n)
|
|
return netip.AddrFrom16(a)
|
|
}
|
|
|
|
func newTwoNodesSameNetwork() (*Server, error) {
|
|
var c Config
|
|
nw := c.AddNetwork("192.168.0.1/24", "2052::1/64")
|
|
c.AddNode(nw)
|
|
c.AddNode(nw)
|
|
for _, c := range c.Nodes() {
|
|
c.SetVerboseSyslog(true)
|
|
}
|
|
return New(&c)
|
|
}
|
|
|
|
func newTwoNodesSameV4Network() (*Server, error) {
|
|
var c Config
|
|
nw := c.AddNetwork("192.168.0.1/24")
|
|
c.AddNode(nw)
|
|
c.AddNode(nw)
|
|
for _, c := range c.Nodes() {
|
|
c.SetVerboseSyslog(true)
|
|
}
|
|
return New(&c)
|
|
}
|
|
|
|
// TestProtocolQEMU tests the protocol that qemu uses to connect to natlab's
|
|
// vnet. (uint32-length prefixed ethernet frames over a unix stream socket)
|
|
//
|
|
// This test makes two clients (as qemu would act) and has one send an ethernet
|
|
// packet to the other virtual LAN segment.
|
|
func TestProtocolQEMU(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skipf("skipping on %s", runtime.GOOS)
|
|
}
|
|
s := must.Get(newTwoNodesSameNetwork())
|
|
defer s.Close()
|
|
s.SetLoggerForTest(t.Logf)
|
|
|
|
td := t.TempDir()
|
|
serverSock := filepath.Join(td, "vnet.sock")
|
|
|
|
ln, err := net.Listen("unix", serverSock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
var clientc [2]*net.UnixConn
|
|
for i := range clientc {
|
|
c, err := net.Dial("unix", serverSock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
clientc[i] = c.(*net.UnixConn)
|
|
}
|
|
|
|
for range clientc {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.ServeUnixConn(conn.(*net.UnixConn), ProtocolQEMU)
|
|
}
|
|
|
|
sendBetweenClients(t, clientc, s, mkLenPrefixed)
|
|
}
|
|
|
|
// TestProtocolUnixDgram tests the protocol that macOS Virtualization.framework
|
|
// uses to connect to vnet. (unix datagram sockets)
|
|
//
|
|
// It is similar to TestProtocolQEMU but uses unix datagram sockets instead of
|
|
// streams.
|
|
func TestProtocolUnixDgram(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skipf("skipping on %s", runtime.GOOS)
|
|
}
|
|
s := must.Get(newTwoNodesSameNetwork())
|
|
defer s.Close()
|
|
s.SetLoggerForTest(t.Logf)
|
|
|
|
td := t.TempDir()
|
|
serverSock := filepath.Join(td, "vnet.sock")
|
|
serverAddr := must.Get(net.ResolveUnixAddr("unixgram", serverSock))
|
|
|
|
var clientSock [2]string
|
|
for i := range clientSock {
|
|
clientSock[i] = filepath.Join(td, fmt.Sprintf("c%d.sock", i))
|
|
}
|
|
|
|
uc, err := net.ListenUnixgram("unixgram", serverAddr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.ServeUnixConn(uc, ProtocolUnixDGRAM)
|
|
|
|
var clientc [2]*net.UnixConn
|
|
for i := range clientc {
|
|
c, err := net.DialUnix("unixgram",
|
|
must.Get(net.ResolveUnixAddr("unixgram", clientSock[i])),
|
|
serverAddr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
clientc[i] = c
|
|
}
|
|
|
|
sendBetweenClients(t, clientc, s, nil)
|
|
}
|
|
|
|
// sendBetweenClients is a test helper that tries to send an ethernet frame from
|
|
// one client to another.
|
|
//
|
|
// It first makes the two clients send a packet to a fictitious node 3, which
|
|
// forces their src MACs to be registered with a networkWriter internally so
|
|
// they can receive traffic.
|
|
//
|
|
// Normally a node starts up spamming DHCP + NDP but we don't get that as a side
|
|
// effect here, so this does it manually.
|
|
//
|
|
// It also then waits for them to be registered.
|
|
//
|
|
// wrap is an optional function that wraps the packet before sending it.
|
|
func sendBetweenClients(t testing.TB, clientc [2]*net.UnixConn, s *Server, wrap func([]byte) []byte) {
|
|
t.Helper()
|
|
if wrap == nil {
|
|
wrap = func(b []byte) []byte { return b }
|
|
}
|
|
for i, c := range clientc {
|
|
must.Get(c.Write(wrap(mkEth(nodeMac(3), nodeMac(i+1), testingEthertype, []byte("hello")))))
|
|
}
|
|
awaitCond(t, 5*time.Second, func() error {
|
|
if n := s.RegisteredWritersForTest(); n != 2 {
|
|
return fmt.Errorf("got %d registered writers, want 2", n)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now see if node1 can write to node2 and node2 receives it.
|
|
pkt := wrap(mkEth(nodeMac(2), nodeMac(1), testingEthertype, []byte("test-msg")))
|
|
t.Logf("writing % 02x", pkt)
|
|
must.Get(clientc[0].Write(pkt))
|
|
|
|
buf := make([]byte, len(pkt))
|
|
clientc[1].SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
n, err := clientc[1].Read(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := buf[:n]
|
|
if !bytes.Equal(got, pkt) {
|
|
t.Errorf("bad packet\n got: % 02x\nwant: % 02x", got, pkt)
|
|
}
|
|
}
|
|
|
|
func awaitCond(t testing.TB, timeout time.Duration, cond func() error) {
|
|
t.Helper()
|
|
t0 := time.Now()
|
|
for {
|
|
if err := cond(); err == nil {
|
|
return
|
|
}
|
|
if time.Since(t0) > timeout {
|
|
t.Fatalf("timed out after %v", timeout)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|