net/tstun: add inital support for NAT v4

This adds support in tstun to utitilize the SelfNodeV4MasqAddrForThisPeer and
perform the necessary modifications to the packet as it passes through tstun.

Currently this only handles ICMP, UDP and TCP traffic.
Subnet routers and Exit Nodes are also unsupported.

Updates tailscale/corp#8020

Co-authored-by: Melanie Warrick <warrick@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali
2022-12-10 01:13:41 +05:00
committed by Maisem Ali
parent 535fad16f8
commit bb31fd7d1c
4 changed files with 384 additions and 7 deletions

View File

@ -440,6 +440,40 @@ func (q *Parsed) IsEchoResponse() bool {
}
}
// UpdateSrcAddr updates the source address in the packet buffer (e.g. during
// SNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
// over IPv4 is supported. It panics if called with IPv6 addr.
func (q *Parsed) UpdateSrcAddr(src netip.Addr) {
if q.IPVersion != 4 || src.Is6() {
panic("UpdateSrcAddr: only IPv4 is supported")
}
old := q.Src.Addr()
q.Src = netip.AddrPortFrom(src, q.Src.Port())
b := q.Buffer()
v4 := src.As4()
copy(b[12:16], v4[:])
updateV4PacketChecksums(q, old, src)
}
// UpdateDstAddr updates the source address in the packet buffer (e.g. during
// DNAT). It also updates the checksum. Currently (2022-12-10) only TCP/UDP/ICMP
// over IPv4 is supported. It panics if called with IPv6 addr.
func (q *Parsed) UpdateDstAddr(dst netip.Addr) {
if q.IPVersion != 4 || dst.Is6() {
panic("UpdateDstAddr: only IPv4 is supported")
}
old := q.Dst.Addr()
q.Dst = netip.AddrPortFrom(dst, q.Dst.Port())
b := q.Buffer()
v4 := dst.As4()
copy(b[16:20], v4[:])
updateV4PacketChecksums(q, old, dst)
}
// EchoIDSeq extracts the identifier/sequence bytes from an ICMP Echo response,
// and returns them as a uint32, used to lookup internally routed ICMP echo
// responses. This function is intentionally lightweight as it is called on
@ -502,3 +536,69 @@ func withIP(ap netip.AddrPort, ip netip.Addr) netip.AddrPort {
func withPort(ap netip.AddrPort, port uint16) netip.AddrPort {
return netip.AddrPortFrom(ap.Addr(), port)
}
// updateV4PacketChecksums updates the checksums in the packet buffer.
// Currently (2023-03-01) only TCP/UDP/ICMP over IPv4 is supported.
// p is modified in place.
// If p.IPProto is unknown, only the IP header checksum is updated.
// TODO(maisem): more protocols (sctp, gre, dccp)
func updateV4PacketChecksums(p *Parsed, old, new netip.Addr) {
o4, n4 := old.As4(), new.As4()
updateV4Checksum(p.Buffer()[10:12], o4[:], n4[:]) // header
switch p.IPProto {
case ipproto.UDP:
updateV4Checksum(p.Transport()[6:8], o4[:], n4[:])
case ipproto.TCP:
updateV4Checksum(p.Transport()[16:18], o4[:], n4[:])
case ipproto.ICMPv4:
// Nothing to do.
}
// TODO(maisem): more protocols (sctp, gre, dccp)
}
// updateV4Checksum calculates and updates the checksum in the packet buffer
// for a change between old and new. The checksum is updated in place.
func updateV4Checksum(oldSum, old, new []byte) {
if len(old) != len(new) {
panic("old and new must be the same length")
}
if len(old)%2 != 0 {
panic("old and new must be even length")
}
/*
RFC 1624
Given the following notation:
HC - old checksum in header
C - one's complement sum of old header
HC' - new checksum in header
C' - one's complement sum of new header
m - old value of a 16-bit field
m' - new value of a 16-bit field
HC' = ~(C + (-m) + m') -- [Eqn. 3]
HC' = ~(~HC + ~m + m')
This can be simplified to:
HC' = ~(C + ~m + m') -- [Eqn. 3]
HC' = ~C'
C' = C + ~m + m'
*/
c := uint32(^binary.BigEndian.Uint16(oldSum))
cPrime := c
for len(new) > 0 {
mNot := uint32(^binary.BigEndian.Uint16(old[:2]))
mPrime := uint32(binary.BigEndian.Uint16(new[:2]))
cPrime += mPrime + mNot
new, old = new[2:], old[2:]
}
// Account for overflows by adding the carry bits back into the sum.
for (cPrime >> 16) > 0 {
cPrime = cPrime&0xFFFF + cPrime>>16
}
hcPrime := ^uint16(cPrime)
binary.BigEndian.PutUint16(oldSum, hcPrime)
}