mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
44d634395b
Endpoint-indepedent Mapping with only Address (but not port) dependent filtering. Updates #13038 Change-Id: I1ec88301acafcb79bf878f9600a7286e8af0f173 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
92 lines
2.6 KiB
Go
92 lines
2.6 KiB
Go
// 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
|
|
}
|