tstest/natlab/vnet: add easyAF
Endpoint-indepedent Mapping with only Address (but not port) dependent filtering. Updates #13038 Change-Id: I1ec88301acafcb79bf878f9600a7286e8af0f173 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
d4cc074187
commit
44d634395b
|
@ -102,6 +102,13 @@ func easy(c *vnet.Config) *vnet.Node {
|
|||
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyNAT))
|
||||
}
|
||||
|
||||
func easyAF(c *vnet.Config) *vnet.Node {
|
||||
n := c.NumNodes() + 1
|
||||
return c.AddNode(c.AddNetwork(
|
||||
fmt.Sprintf("2.%d.%d.%d", n, n, n), // public IP
|
||||
fmt.Sprintf("192.168.%d.1/24", n), vnet.EasyAFNAT))
|
||||
}
|
||||
|
||||
func sameLAN(c *vnet.Config) *vnet.Node {
|
||||
nw := c.FirstNetwork()
|
||||
if nw == nil {
|
||||
|
@ -391,6 +398,11 @@ func TestEasyHard(t *testing.T) {
|
|||
nt.runTest(easy, hard)
|
||||
}
|
||||
|
||||
func TestEasyAFHard(t *testing.T) {
|
||||
nt := newNatTest(t)
|
||||
nt.runTest(easyAF, hard)
|
||||
}
|
||||
|
||||
func TestEasyHardPMP(t *testing.T) {
|
||||
nt := newNatTest(t)
|
||||
nt.runTest(easy, hardPMP)
|
||||
|
@ -430,6 +442,7 @@ func TestGrid(t *testing.T) {
|
|||
}
|
||||
types := []nodeType{
|
||||
{"easy", easy},
|
||||
{"easyAF", easyAF},
|
||||
{"hard", hard},
|
||||
{"easyPMP", easyPMP},
|
||||
{"hardPMP", hardPMP},
|
||||
|
@ -483,10 +496,13 @@ func TestGrid(t *testing.T) {
|
|||
pf := func(format string, args ...any) {
|
||||
fmt.Fprintf(&hb, format, args...)
|
||||
}
|
||||
rewrite := func(s string) string {
|
||||
return strings.ReplaceAll(s, "PMP", "+pm")
|
||||
}
|
||||
pf("<html><table border=1 cellpadding=5>")
|
||||
pf("<tr><td></td>")
|
||||
for _, a := range types {
|
||||
pf("<td><b>%s</b></td>", a.name)
|
||||
pf("<td><b>%s</b></td>", rewrite(a.name))
|
||||
}
|
||||
pf("</tr>\n")
|
||||
|
||||
|
@ -494,7 +510,7 @@ func TestGrid(t *testing.T) {
|
|||
if a.name == "sameLAN" {
|
||||
continue
|
||||
}
|
||||
pf("<tr><td><b>%s</b></td>", a.name)
|
||||
pf("<tr><td><b>%s</b></td>", rewrite(a.name))
|
||||
for _, b := range types {
|
||||
key := a.name + "-" + b.name
|
||||
key2 := b.name + "-" + a.name
|
||||
|
@ -509,7 +525,14 @@ func TestGrid(t *testing.T) {
|
|||
}
|
||||
pf("</tr>\n")
|
||||
}
|
||||
pf("</table></html>")
|
||||
pf("</table>")
|
||||
pf("<b>easy</b>: Endpoint-Independent Mapping, Address and Port-Dependent Filtering (e.g. Linux, Google Wifi, Unifi, eero)<br>")
|
||||
pf("<b>easyAF</b>: Endpoint-Independent Mapping, Address-Dependent Filtering (James says telephony things or Zyxel type things)<br>")
|
||||
pf("<b>hard</b>: Address and Port-Dependent Mapping, Address and Port-Dependent Filtering (FreeBSD, OPNSense, pfSense)<br>")
|
||||
pf("<b>one2one</b>: One-to-One NAT (e.g. an EC2 instance with a public IPv4)<br>")
|
||||
pf("<b>x+pm</b>: x, with port mapping (NAT-PMP, PCP, UPnP, etc)<br>")
|
||||
pf("<b>sameLAN</b>: a second node in the same LAN as the first<br>")
|
||||
pf("</html>")
|
||||
|
||||
if err := os.WriteFile("grid.html", hb.Bytes(), 0666); err != nil {
|
||||
t.Fatalf("writeFile: %v", err)
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// easyAFNAT is an "Endpoint Independent" NAT, like Linux and most home routers
|
||||
// (many of which are Linux), but with only address filtering, not address+port
|
||||
// filtering.
|
||||
//
|
||||
// James says these are used by "anyone with “voip helpers” turned on"
|
||||
// "which is a lot of home modem routers" ... "probably like most of the zyxel
|
||||
// type things".
|
||||
type easyAFNAT struct {
|
||||
pool IPPool
|
||||
wanIP netip.Addr
|
||||
out map[netip.Addr]portMappingAndTime
|
||||
in map[uint16]lanAddrAndTime
|
||||
lastOut map[srcAPDstAddrTuple]time.Time // (lan:port, wan:port) => last packet out time
|
||||
}
|
||||
|
||||
type srcAPDstAddrTuple struct {
|
||||
src netip.AddrPort
|
||||
dst netip.Addr
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerNATType(EasyAFNAT, func(p IPPool) (NATTable, error) {
|
||||
return &easyAFNAT{pool: p, wanIP: p.WANIP()}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (n *easyAFNAT) IsPublicPortUsed(ap netip.AddrPort) bool {
|
||||
if ap.Addr() != n.wanIP {
|
||||
return false
|
||||
}
|
||||
_, ok := n.in[ap.Port()]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (n *easyAFNAT) PickOutgoingSrc(src, dst netip.AddrPort, at time.Time) (wanSrc netip.AddrPort) {
|
||||
mak.Set(&n.lastOut, srcAPDstAddrTuple{src, dst.Addr()}, at)
|
||||
if pm, ok := n.out[src.Addr()]; ok {
|
||||
// Existing flow.
|
||||
// TODO: bump timestamp
|
||||
return netip.AddrPortFrom(n.wanIP, pm.port)
|
||||
}
|
||||
|
||||
// Loop through all 32k high (ephemeral) ports, starting at a random
|
||||
// position and looping back around to the start.
|
||||
start := rand.N(uint16(32 << 10))
|
||||
for off := range uint16(32 << 10) {
|
||||
port := 32<<10 + (start+off)%(32<<10)
|
||||
if _, ok := n.in[port]; !ok {
|
||||
wanAddr := netip.AddrPortFrom(n.wanIP, port)
|
||||
if n.pool.IsPublicPortUsed(wanAddr) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a free port.
|
||||
mak.Set(&n.out, src.Addr(), portMappingAndTime{port: port, at: at})
|
||||
mak.Set(&n.in, port, lanAddrAndTime{lanAddr: src, at: at})
|
||||
return wanAddr
|
||||
}
|
||||
}
|
||||
return netip.AddrPort{} // failed to allocate a mapping; TODO: fire an alert?
|
||||
}
|
||||
|
||||
func (n *easyAFNAT) PickIncomingDst(src, dst netip.AddrPort, at time.Time) (lanDst netip.AddrPort) {
|
||||
if dst.Addr() != n.wanIP {
|
||||
return netip.AddrPort{} // drop; not for us. shouldn't happen if natlabd routing isn't broken.
|
||||
}
|
||||
lanDst = n.in[dst.Port()].lanAddr
|
||||
|
||||
// Stateful firewall: drop incoming packets that don't have traffic out.
|
||||
// TODO(bradfitz): verify Linux does this in the router code, not in the NAT code.
|
||||
if t, ok := n.lastOut[srcAPDstAddrTuple{lanDst, src.Addr()}]; !ok || at.Sub(t) > 300*time.Second {
|
||||
log.Printf("Drop incoming packet from %v to %v; no recent outgoing packet", src, dst)
|
||||
return netip.AddrPort{}
|
||||
}
|
||||
|
||||
return lanDst
|
||||
}
|
|
@ -15,7 +15,8 @@ import (
|
|||
|
||||
const (
|
||||
One2OneNAT NAT = "one2one"
|
||||
EasyNAT NAT = "easy"
|
||||
EasyNAT NAT = "easy" // address+port filtering
|
||||
EasyAFNAT NAT = "easyaf" // address filtering (not port)
|
||||
HardNAT NAT = "hard"
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue