You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/tstest/natlab/natlab_test.go

335 lines
8.3 KiB
Go

// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package natlab
import (
"context"
"fmt"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/tstest"
)
func TestAllocIPs(t *testing.T) {
n := NewInternet()
saw := map[netaddr.IP]bool{}
for i := 0; i < 255; i++ {
for _, f := range []func(*Interface) netaddr.IP{n.allocIPv4, n.allocIPv6} {
ip := f(nil)
if saw[ip] {
t.Fatalf("got duplicate %v", ip)
}
saw[ip] = true
}
}
// This should work:
n.allocIPv6(nil)
// But allocating another IPv4 should panic, exhausting the
// limited /24 range:
defer func() {
if e := recover(); fmt.Sprint(e) != "pool exhausted" {
t.Errorf("unexpected panic: %v", e)
}
}()
n.allocIPv4(nil)
t.Fatalf("expected panic from IPv4")
}
func TestSendPacket(t *testing.T) {
internet := NewInternet()
foo := &Machine{Name: "foo"}
bar := &Machine{Name: "bar"}
ifFoo := foo.Attach("eth0", internet)
ifBar := bar.Attach("enp0s1", internet)
fooAddr := netaddr.IPPort{IP: ifFoo.V4(), Port: 123}
barAddr := netaddr.IPPort{IP: ifBar.V4(), Port: 456}
ctx := context.Background()
fooPC, err := foo.ListenPacket(ctx, "udp4", fooAddr.String())
if err != nil {
t.Fatal(err)
}
barPC, err := bar.ListenPacket(ctx, "udp4", barAddr.String())
if err != nil {
t.Fatal(err)
}
const msg = "some message"
if _, err := fooPC.WriteTo([]byte(msg), barAddr.UDPAddr()); err != nil {
t.Fatal(err)
}
buf := make([]byte, 1500) // TODO: care about MTUs in the natlab package somewhere
n, addr, err := barPC.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
if string(buf) != msg {
t.Errorf("read %q; want %q", buf, msg)
}
if addr.String() != fooAddr.String() {
t.Errorf("addr = %q; want %q", addr, fooAddr)
}
}
func TestMultiNetwork(t *testing.T) {
lan := &Network{
Name: "lan",
Prefix4: mustPrefix("192.168.0.0/24"),
}
internet := NewInternet()
client := &Machine{Name: "client"}
nat := &Machine{Name: "nat"}
server := &Machine{Name: "server"}
ifClient := client.Attach("eth0", lan)
ifNATWAN := nat.Attach("ethwan", internet)
ifNATLAN := nat.Attach("ethlan", lan)
ifServer := server.Attach("eth0", internet)
ctx := context.Background()
clientPC, err := client.ListenPacket(ctx, "udp", ":123")
if err != nil {
t.Fatal(err)
}
natPC, err := nat.ListenPacket(ctx, "udp", ":456")
if err != nil {
t.Fatal(err)
}
serverPC, err := server.ListenPacket(ctx, "udp", ":789")
if err != nil {
t.Fatal(err)
}
clientAddr := netaddr.IPPort{IP: ifClient.V4(), Port: 123}
natLANAddr := netaddr.IPPort{IP: ifNATLAN.V4(), Port: 456}
natWANAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 456}
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 789}
const msg1, msg2 = "hello", "world"
if _, err := natPC.WriteTo([]byte(msg1), clientAddr.UDPAddr()); err != nil {
t.Fatal(err)
}
if _, err := natPC.WriteTo([]byte(msg2), serverAddr.UDPAddr()); err != nil {
t.Fatal(err)
}
buf := make([]byte, 1500)
n, addr, err := clientPC.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if string(buf[:n]) != msg1 {
t.Errorf("read %q; want %q", buf[:n], msg1)
}
if addr.String() != natLANAddr.String() {
t.Errorf("addr = %q; want %q", addr, natLANAddr)
}
n, addr, err = serverPC.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if string(buf[:n]) != msg2 {
t.Errorf("read %q; want %q", buf[:n], msg2)
}
if addr.String() != natWANAddr.String() {
t.Errorf("addr = %q; want %q", addr, natLANAddr)
}
}
func TestPacketHandler(t *testing.T) {
lan := &Network{
Name: "lan",
Prefix4: mustPrefix("192.168.0.0/24"),
Prefix6: mustPrefix("fd00:916::/64"),
}
internet := NewInternet()
client := &Machine{Name: "client"}
nat := &Machine{Name: "nat"}
server := &Machine{Name: "server"}
ifClient := client.Attach("eth0", lan)
ifNATWAN := nat.Attach("wan", internet)
ifNATLAN := nat.Attach("lan", lan)
ifServer := server.Attach("server", internet)
lan.SetDefaultGateway(ifNATLAN)
// This HandlePacket implements a basic (some might say "broken")
// 1:1 NAT, where client's IP gets replaced with the NAT's WAN IP,
// and vice versa.
//
// This NAT is not suitable for actual use, since it doesn't do
// port remappings or any other things that NATs usually to. But
// it works as a demonstrator for a single client behind the NAT,
// where the NAT box itself doesn't also make PacketConns.
nat.HandlePacket = func(p *Packet, iface *Interface) PacketVerdict {
switch {
case p.Dst.IP.Is6():
return Continue // no NAT for ipv6
case iface == ifNATLAN && p.Src.IP == ifClient.V4():
p.Src.IP = ifNATWAN.V4()
nat.Inject(p)
return Drop
case iface == ifNATWAN && p.Dst.IP == ifNATWAN.V4():
p.Dst.IP = ifClient.V4()
nat.Inject(p)
return Drop
default:
return Continue
}
}
ctx := context.Background()
clientPC, err := client.ListenPacket(ctx, "udp4", ":123")
if err != nil {
t.Fatal(err)
}
serverPC, err := server.ListenPacket(ctx, "udp4", ":456")
if err != nil {
t.Fatal(err)
}
const msg = "some message"
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 456}
if _, err := clientPC.WriteTo([]byte(msg), serverAddr.UDPAddr()); err != nil {
t.Fatal(err)
}
buf := make([]byte, 1500) // TODO: care about MTUs in the natlab package somewhere
n, addr, err := serverPC.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
buf = buf[:n]
if string(buf) != msg {
t.Errorf("read %q; want %q", buf, msg)
}
mappedAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 123}
if addr.String() != mappedAddr.String() {
t.Errorf("addr = %q; want %q", addr, mappedAddr)
}
}
func TestFirewall(t *testing.T) {
wan := NewInternet()
lan := &Network{
Name: "lan",
Prefix4: mustPrefix("10.0.0.0/8"),
}
m := &Machine{Name: "test"}
trust := m.Attach("trust", lan)
untrust := m.Attach("untrust", wan)
client := ipp("192.168.0.2:1234")
serverA := ipp("2.2.2.2:5678")
serverB1 := ipp("7.7.7.7:9012")
serverB2 := ipp("7.7.7.7:3456")
t.Run("ip_port_dependent", func(t *testing.T) {
f := &Firewall{
TrustedInterface: trust,
SessionTimeout: 30 * time.Second,
Type: AddressAndPortDependentFirewall,
}
testFirewall(t, f, []fwTest{
// client -> A authorizes A -> client
{trust, client, serverA, Continue},
{untrust, serverA, client, Continue},
{untrust, serverA, client, Continue},
// B1 -> client fails until client -> B1
{untrust, serverB1, client, Drop},
{trust, client, serverB1, Continue},
{untrust, serverB1, client, Continue},
// B2 -> client still fails
{untrust, serverB2, client, Drop},
})
})
t.Run("ip_dependent", func(t *testing.T) {
f := &Firewall{
TrustedInterface: trust,
SessionTimeout: 30 * time.Second,
Type: AddressDependentFirewall,
}
testFirewall(t, f, []fwTest{
// client -> A authorizes A -> client
{trust, client, serverA, Continue},
{untrust, serverA, client, Continue},
{untrust, serverA, client, Continue},
// B1 -> client fails until client -> B1
{untrust, serverB1, client, Drop},
{trust, client, serverB1, Continue},
{untrust, serverB1, client, Continue},
// B2 -> client also works now
{untrust, serverB2, client, Continue},
})
})
t.Run("endpoint_independent", func(t *testing.T) {
f := &Firewall{
TrustedInterface: trust,
SessionTimeout: 30 * time.Second,
Type: EndpointIndependentFirewall,
}
testFirewall(t, f, []fwTest{
// client -> A authorizes A -> client
{trust, client, serverA, Continue},
{untrust, serverA, client, Continue},
{untrust, serverA, client, Continue},
// B1 -> client also works
{untrust, serverB1, client, Continue},
// B2 -> client also works
{untrust, serverB2, client, Continue},
})
})
}
type fwTest struct {
iface *Interface
src, dst netaddr.IPPort
want PacketVerdict
}
func testFirewall(t *testing.T, f *Firewall, tests []fwTest) {
t.Helper()
clock := &tstest.Clock{}
f.TimeNow = clock.Now
for _, test := range tests {
clock.Advance(time.Second)
p := &Packet{
Src: test.src,
Dst: test.dst,
Payload: []byte{},
}
got := f.HandlePacket(p, test.iface)
if got != test.want {
t.Errorf("iface=%s src=%s dst=%s got %v, want %v", test.iface.name, test.src, test.dst, got, test.want)
}
}
}
func ipp(str string) netaddr.IPPort {
ipp, err := netaddr.ParseIPPort(str)
if err != nil {
panic(err)
}
return ipp
}