From 813c2c56facb7d02e0145b90404b0d1e2cf1aa58 Mon Sep 17 00:00:00 2001 From: sirzooro Date: Mon, 6 Jul 2026 19:34:49 +0200 Subject: [PATCH] Refactor NAT to support TCP, fix typos --- vnet/nat.go | 272 ++++++---- vnet/nat_test.go | 1299 +++++++++++++++++++--------------------------- 2 files changed, 711 insertions(+), 860 deletions(-) diff --git a/vnet/nat.go b/vnet/nat.go index dec9d657..4a2fb7bc 100644 --- a/vnet/nat.go +++ b/vnet/nat.go @@ -14,12 +14,12 @@ import ( ) var ( - errNATRequriesMapping = errors.New("1:1 NAT requires more than one mapping") - errMismatchLengthIP = errors.New("length mismtach between mappedIPs and localIPs") - errNonUDPTranslationNotSupported = errors.New("non-udp translation is not supported yet") - errNoAssociatedLocalAddress = errors.New("no associated local address") - errNoNATBindingFound = errors.New("no NAT binding found") - errHasNoPermission = errors.New("has no permission") + errNATRequiresMapping = errors.New("1:1 NAT requires at least one mapping") + errMismatchLengthIP = errors.New("length mismatch between mappedIPs and localIPs") + errTranslationNotSupported = errors.New("translation is not supported for this protocol") + errNoAssociatedLocalAddress = errors.New("no associated local address") + errNoNATBindingFound = errors.New("no NAT binding found") + errHasNoPermission = errors.New("has no permission") ) // EndpointDependencyType defines a type of behavioral dependendency on the @@ -92,6 +92,7 @@ type networkAddressTranslator struct { outboundMap map[string]*mapping // key: "::[:remote-ip[:remote-port]] inboundMap map[string]*mapping // key: "::" udpPortCounter int + tcpPortCounter int mutex sync.RWMutex log logging.LeveledLogger } @@ -107,7 +108,7 @@ func newNAT(config *natConfig) (*networkAddressTranslator, error) { natType.MappingLifeTime = 0 if len(config.mappedIPs) == 0 { - return nil, errNATRequriesMapping + return nil, errNATRequiresMapping } if len(config.mappedIPs) != len(config.localIPs) { return nil, errMismatchLengthIP @@ -151,13 +152,73 @@ func (n *networkAddressTranslator) getPairedLocalIP(mappedIP net.IP) net.IP { return nil } -func (n *networkAddressTranslator) translateOutbound(from Chunk) (Chunk, error) { //nolint:cyclop +func (n *networkAddressTranslator) translateOutbound(from Chunk) (Chunk, error) { //nolint:cyclop,gocognit n.mutex.Lock() defer n.mutex.Unlock() to := from.Clone() - if from.Network() == udp { //nolint:nestif + translateOutboundNAPT := func(proto string, portBase int, portCounter *int) (Chunk, error) { + var bound, filterKey string + switch n.natType.MappingBehavior { + case EndpointIndependent: + bound = "" + case EndpointAddrDependent: + bound = from.getDestinationIP().String() + case EndpointAddrPortDependent: + bound = from.DestinationAddr().String() + } + + switch n.natType.FilteringBehavior { + case EndpointIndependent: + filterKey = "" + case EndpointAddrDependent: + filterKey = from.getDestinationIP().String() + case EndpointAddrPortDependent: + filterKey = from.DestinationAddr().String() + } + + oKey := fmt.Sprintf("%s:%s:%s", proto, from.SourceAddr().String(), bound) + + mapp := n.findOutboundMapping(oKey) + if mapp == nil { + mappedPort := portBase + *portCounter + if mappedPort > 65535 { + return nil, errPortSpaceExhausted + } + (*portCounter)++ + + mapp = &mapping{ + proto: from.SourceAddr().Network(), + local: from.SourceAddr().String(), + bound: bound, + mapped: fmt.Sprintf("%s:%d", n.mappedIPs[0].String(), mappedPort), + filters: map[string]struct{}{}, + expires: time.Now().Add(n.natType.MappingLifeTime), + } + + n.outboundMap[oKey] = mapp + iKey := fmt.Sprintf("%s:%s", proto, mapp.mapped) + + n.log.Debugf("[%s] created a new NAT binding oKey=%s iKey=%s", n.name, oKey, iKey) + + mapp.filters[filterKey] = struct{}{} + n.log.Debugf("[%s] permit access from %s to %s", n.name, filterKey, mapp.mapped) + n.inboundMap[iKey] = mapp + } else if _, ok := mapp.filters[filterKey]; !ok { + n.log.Debugf("[%s] permit access from %s to %s", n.name, filterKey, mapp.mapped) + mapp.filters[filterKey] = struct{}{} + } + + if err := to.setSourceAddr(mapp.mapped); err != nil { + return nil, err + } + + return to, nil + } + + switch from.Network() { + case udp: if n.natType.Mode == NATModeNAT1To1 { // 1:1 NAT behavior srcAddr := from.SourceAddr().(*net.UDPAddr) //nolint:forcetypeassert @@ -172,61 +233,34 @@ func (n *networkAddressTranslator) translateOutbound(from Chunk) (Chunk, error) return nil, err } } else { - // Normal (NAPT) behavior - var bound, filterKey string - switch n.natType.MappingBehavior { - case EndpointIndependent: - bound = "" - case EndpointAddrDependent: - bound = from.getDestinationIP().String() - case EndpointAddrPortDependent: - bound = from.DestinationAddr().String() + var err error + to, err = translateOutboundNAPT("udp", 0xC000, &n.udpPortCounter) + if err != nil { + return nil, err } + } - switch n.natType.FilteringBehavior { - case EndpointIndependent: - filterKey = "" - case EndpointAddrDependent: - filterKey = from.getDestinationIP().String() - case EndpointAddrPortDependent: - filterKey = from.DestinationAddr().String() - } + n.log.Debugf("[%s] translate outbound chunk from %s to %s", n.name, from.String(), to.String()) - oKey := fmt.Sprintf("udp:%s:%s", from.SourceAddr().String(), bound) - - mapp := n.findOutboundMapping(oKey) - if mapp == nil { - // Create a new mapping - mappedPort := 0xC000 + n.udpPortCounter - n.udpPortCounter++ - - mapp = &mapping{ - proto: from.SourceAddr().Network(), - local: from.SourceAddr().String(), - bound: bound, - mapped: fmt.Sprintf("%s:%d", n.mappedIPs[0].String(), mappedPort), - filters: map[string]struct{}{}, - expires: time.Now().Add(n.natType.MappingLifeTime), - } - - n.outboundMap[oKey] = mapp - - iKey := fmt.Sprintf("udp:%s", mapp.mapped) - - n.log.Debugf("[%s] created a new NAT binding oKey=%s iKey=%s", - n.name, - oKey, - iKey) - - mapp.filters[filterKey] = struct{}{} - n.log.Debugf("[%s] permit access from %s to %s", n.name, filterKey, mapp.mapped) - n.inboundMap[iKey] = mapp - } else if _, ok := mapp.filters[filterKey]; !ok { - n.log.Debugf("[%s] permit access from %s to %s", n.name, filterKey, mapp.mapped) - mapp.filters[filterKey] = struct{}{} - } + return to, nil - if err := to.setSourceAddr(mapp.mapped); err != nil { + case tcp: + if n.natType.Mode == NATModeNAT1To1 { + srcAddr := from.SourceAddr().(*net.TCPAddr) //nolint:forcetypeassert + srcIP := n.getPairedMappedIP(srcAddr.IP) + if srcIP == nil { + n.log.Debugf("[%s] drop outbound chunk %s with not route", n.name, from.String()) + + return nil, nil // nolint:nilnil + } + srcPort := srcAddr.Port + if err := to.setSourceAddr(fmt.Sprintf("%s:%d", srcIP.String(), srcPort)); err != nil { + return nil, err + } + } else { + var err error + to, err = translateOutboundNAPT("tcp", 0x8000, &n.tcpPortCounter) + if err != nil { return nil, err } } @@ -234,60 +268,99 @@ func (n *networkAddressTranslator) translateOutbound(from Chunk) (Chunk, error) n.log.Debugf("[%s] translate outbound chunk from %s to %s", n.name, from.String(), to.String()) return to, nil - } - return nil, errNonUDPTranslationNotSupported + default: + return nil, errTranslationNotSupported + } } -func (n *networkAddressTranslator) translateInbound(from Chunk) (Chunk, error) { //nolint:cyclop +func (n *networkAddressTranslator) translateInbound(from Chunk) (Chunk, error) { //nolint:cyclop,gocognit n.mutex.Lock() defer n.mutex.Unlock() to := from.Clone() - if from.Network() == udp { //nolint:nestif + translateInboundNAT1To1 := func(dstPort int) (Chunk, error) { + dstIP := n.getPairedLocalIP(from.getDestinationIP()) + if dstIP == nil { + return nil, fmt.Errorf("drop %s as %w", from.String(), errNoAssociatedLocalAddress) + } + if err := to.setDestinationAddr(fmt.Sprintf("%s:%d", dstIP, dstPort)); err != nil { + return nil, err + } + + return to, nil + } + + translateInboundNAPT := func(proto string) (Chunk, error) { + iKey := fmt.Sprintf("%s:%s", proto, from.DestinationAddr().String()) + mapp := n.findInboundMapping(iKey) + if mapp == nil { + return nil, fmt.Errorf("drop %s as %w", from.String(), errNoNATBindingFound) + } + + var filterKey string + switch n.natType.FilteringBehavior { + case EndpointIndependent: + filterKey = "" + case EndpointAddrDependent: + filterKey = from.getSourceIP().String() + case EndpointAddrPortDependent: + filterKey = from.SourceAddr().String() + } + + if _, ok := mapp.filters[filterKey]; !ok { + return nil, fmt.Errorf("drop %s as the remote %s %w", from.String(), filterKey, errHasNoPermission) + } + + // See RFC 4847 Section 4.3. Mapping Refresh + // a) Inbound refresh may be useful for applications with no outgoing + // UDP traffic. However, allowing inbound refresh may allow an + // external attacker or misbehaving application to keep a mapping + // alive indefinitely. This may be a security risk. Also, if the + // process is repeated with different ports, over time, it could + // use up all the ports on the NAT. + + if err := to.setDestinationAddr(mapp.local); err != nil { + return nil, err + } + + return to, nil + } + + switch from.Network() { + case udp: if n.natType.Mode == NATModeNAT1To1 { - // 1:1 NAT behavior dstAddr := from.DestinationAddr().(*net.UDPAddr) //nolint:forcetypeassert - dstIP := n.getPairedLocalIP(dstAddr.IP) - if dstIP == nil { - return nil, fmt.Errorf("drop %s as %w", from.String(), errNoAssociatedLocalAddress) - } - dstPort := from.DestinationAddr().(*net.UDPAddr).Port //nolint:forcetypeassert - if err := to.setDestinationAddr(fmt.Sprintf("%s:%d", dstIP, dstPort)); err != nil { + var err error + to, err = translateInboundNAT1To1(dstAddr.Port) + if err != nil { return nil, err } } else { - // Normal (NAPT) behavior - iKey := fmt.Sprintf("udp:%s", from.DestinationAddr().String()) - mapping := n.findInboundMapping(iKey) - if mapping == nil { - return nil, fmt.Errorf("drop %s as %w", from.String(), errNoNATBindingFound) - } - - var filterKey string - switch n.natType.FilteringBehavior { - case EndpointIndependent: - filterKey = "" - case EndpointAddrDependent: - filterKey = from.getSourceIP().String() - case EndpointAddrPortDependent: - filterKey = from.SourceAddr().String() + var err error + to, err = translateInboundNAPT(udp) + if err != nil { + return nil, err } + } - if _, ok := mapping.filters[filterKey]; !ok { - return nil, fmt.Errorf("drop %s as the remote %s %w", from.String(), filterKey, errHasNoPermission) - } + n.log.Debugf("[%s] translate inbound chunk from %s to %s", n.name, from.String(), to.String()) - // See RFC 4847 Section 4.3. Mapping Refresh - // a) Inbound refresh may be useful for applications with no outgoing - // UDP traffic. However, allowing inbound refresh may allow an - // external attacker or misbehaving application to keep a mapping - // alive indefinitely. This may be a security risk. Also, if the - // process is repeated with different ports, over time, it could - // use up all the ports on the NAT. + return to, nil - if err := to.setDestinationAddr(mapping.local); err != nil { + case tcp: + if n.natType.Mode == NATModeNAT1To1 { + dstAddr := from.DestinationAddr().(*net.TCPAddr) //nolint:forcetypeassert + var err error + to, err = translateInboundNAT1To1(dstAddr.Port) + if err != nil { + return nil, err + } + } else { + var err error + to, err = translateInboundNAPT(tcp) + if err != nil { return nil, err } } @@ -295,9 +368,10 @@ func (n *networkAddressTranslator) translateInbound(from Chunk) (Chunk, error) { n.log.Debugf("[%s] translate inbound chunk from %s to %s", n.name, from.String(), to.String()) return to, nil - } - return nil, errNonUDPTranslationNotSupported + default: + return nil, errTranslationNotSupported + } } // caller must hold the mutex. diff --git a/vnet/nat_test.go b/vnet/nat_test.go index c5d8bf37..74d67518 100644 --- a/vnet/nat_test.go +++ b/vnet/nat_test.go @@ -19,6 +19,70 @@ import ( const demoIP = "1.2.3.4" +type natTestProto struct { + name string + newAddr func(ip net.IP, port int) net.Addr + newChunk func(t *testing.T, src, dst net.Addr) Chunk +} + +func natTestProtos() []natTestProto { + return []natTestProto{ + { + name: "udp", + newAddr: func(ip net.IP, port int) net.Addr { + return &net.UDPAddr{IP: ip, Port: port} + }, + newChunk: func(t *testing.T, src, dst net.Addr) Chunk { + t.Helper() + srcAddr, ok := src.(*net.UDPAddr) + if !ok { + assert.FailNow(t, "expected *net.UDPAddr src, got %T", src) + } + dstAddr, ok := dst.(*net.UDPAddr) + if !ok { + assert.FailNow(t, "expected *net.UDPAddr dst, got %T", dst) + } + + return newChunkUDP(srcAddr, dstAddr) + }, + }, + { + name: "tcp", + newAddr: func(ip net.IP, port int) net.Addr { + return &net.TCPAddr{IP: ip, Port: port} + }, + newChunk: func(t *testing.T, src, dst net.Addr) Chunk { + t.Helper() + srcAddr, ok := src.(*net.TCPAddr) + if !ok { + assert.FailNow(t, "expected *net.TCPAddr src, got %T", src) + } + dstAddr, ok := dst.(*net.TCPAddr) + if !ok { + assert.FailNow(t, "expected *net.TCPAddr dst, got %T", dst) + } + + return newChunkTCP(srcAddr, dstAddr, tcpACK) + }, + }, + } +} + +func natAddrIPPort(t *testing.T, addr net.Addr) (net.IP, int) { + t.Helper() + + switch a := addr.(type) { + case *net.UDPAddr: + return a.IP, a.Port + case *net.TCPAddr: + return a.IP, a.Port + default: + assert.FailNow(t, "unexpected addr type %T", addr) + + return nil, 0 + } +} + func TestNATTypeDefaults(t *testing.T) { loggerFactory := logging.NewDefaultLoggerFactory() nat, err := newNAT(&natConfig{ @@ -39,502 +103,298 @@ func TestNATMappingBehavior(t *testing.T) { //nolint:maintidx loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") - t.Run("full-cone NAT", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointIndependent, - FilteringBehavior: EndpointIndependent, - Hairpinning: false, - MappingLifeTime: 30 * time.Second, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") - - src := &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - } - - oic := newChunkUDP(src, dst) - - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - //nolint:forcetypeassert - iec := newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - log.Debugf("i-original : %s", iec.String()) - - iic, err := nat.translateInbound(iec) - assert.Nil(t, err, "should succeed") - - log.Debugf("i-translated: %s", iic.String()) - - //nolint:forcetypeassert - assert.Equal(t, - oic.SourceAddr().String(), - iic.(*chunkUDP).DestinationAddr().String(), - "should match") - - // packet with dest addr that does not exist in the mapping table - // will be dropped - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort + 1, - }, - ) - - _, err = nat.translateInbound(iec) - log.Debug(err.Error()) - assert.NotNil(t, err, "should fail (dropped)") - - // packet from any addr will be accepted (full-cone) - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: 7777, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - _, err = nat.translateInbound(iec) - assert.Nil(t, err, "should succeed") - }) - - t.Run("addr-restricted-cone NAT", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointIndependent, - FilteringBehavior: EndpointAddrDependent, - Hairpinning: false, - MappingLifeTime: 30 * time.Second, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") - - src := &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - } - - oic := newChunkUDP(src, dst) - log.Debugf("o-original : %s", oic.String()) - - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - log.Debugf("o-translated: %s", oec.String()) - - // sending different (IP: 5.6.7.9) won't create a new mapping - oic2 := newChunkUDP(&net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, &net.UDPAddr{ - IP: net.ParseIP("5.6.7.9"), - Port: 9000, - }) - oec2, err := nat.translateOutbound(oic2) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - log.Debugf("o-translated: %s", oec2.String()) - - //nolint:forcetypeassert - iec := newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - log.Debugf("i-original : %s", iec.String()) - - iic, err := nat.translateInbound(iec) - if !assert.NoError(t, err, "should succeed") { - return - } - - log.Debugf("i-translated: %s", iic.String()) - - //nolint:forcetypeassert - assert.Equal(t, - oic.SourceAddr().String(), - iic.(*chunkUDP).DestinationAddr().String(), - "should match") - - // packet with dest addr that does not exist in the mapping table - // will be dropped - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort + 1, - }, - ) - - _, err = nat.translateInbound(iec) - log.Debug(err.Error()) - assert.NotNil(t, err, "should fail (dropped)") - - // packet from any port will be accepted (restricted-cone) - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: 7777, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - _, err = nat.translateInbound(iec) - assert.Nil(t, err, "should succeed") - - // packet from different addr will be dropped (restricted-cone) - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("6.6.6.6"), - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - _, err = nat.translateInbound(iec) - log.Debug(err.Error()) - assert.NotNil(t, err, "should fail (dropped)") - }) - - t.Run("port-restricted-cone NAT", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointIndependent, - FilteringBehavior: EndpointAddrPortDependent, - Hairpinning: false, - MappingLifeTime: 30 * time.Second, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") - - src := &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - } - - oic := newChunkUDP(src, dst) - log.Debugf("o-original : %s", oic.String()) + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + t.Run("full-cone NAT", func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointIndependent, + FilteringBehavior: EndpointIndependent, + Hairpinning: false, + MappingLifeTime: 30 * time.Second, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) - log.Debugf("o-translated: %s", oec.String()) - - // sending different (IP: 5.6.7.9) won't create a new mapping - oic2 := newChunkUDP(&net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, &net.UDPAddr{ - IP: net.ParseIP("5.6.7.9"), - Port: 9000, - }) - oec2, err := nat.translateOutbound(oic2) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - log.Debugf("o-translated: %s", oec2.String()) - - //nolint:forcetypeassert - iec := newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") - log.Debugf("i-original : %s", iec.String()) + log.Debugf("o-original : %s", oic.String()) + log.Debugf("o-translated: %s", oec.String()) - iic, err := nat.translateInbound(iec) - assert.Nil(t, err, "should succeed") + oecIP, oecPort := natAddrIPPort(t, oec.SourceAddr()) + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort)) - log.Debugf("i-translated: %s", iic.String()) + log.Debugf("i-original : %s", iec.String()) - //nolint:forcetypeassert - assert.Equal(t, - oic.SourceAddr().String(), - iic.(*chunkUDP).DestinationAddr().String(), - "should match") + iic, err := nat.translateInbound(iec) + assert.Nil(t, err, "should succeed") - // packet with dest addr that does not exist in the mapping table - // will be dropped - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort + 1, - }, - ) + log.Debugf("i-translated: %s", iic.String()) - _, err = nat.translateInbound(iec) - assert.NotNil(t, err, "should fail (dropped)") + assert.Equal(t, oic.SourceAddr().String(), iic.DestinationAddr().String(), "should match") - // packet from different port will be dropped (port-restricted-cone) - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: 7777, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) + // packet with dest addr that does not exist in the mapping table + // will be dropped + iec = proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort+1)) - _, err = nat.translateInbound(iec) - assert.NotNil(t, err, "should fail (dropped)") + _, err = nat.translateInbound(iec) + log.Debug(err.Error()) + assert.NotNil(t, err, "should fail (dropped)") - // packet from different addr will be dropped (restricted-cone) - //nolint:forcetypeassert - iec = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("6.6.6.6"), - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) + // packet from any addr will be accepted (full-cone) + iec = proto.newChunk(t, proto.newAddr(dstIP, 7777), proto.newAddr(oecIP, oecPort)) - _, err = nat.translateInbound(iec) - assert.NotNil(t, err, "should fail (dropped)") - }) - - t.Run("symmetric NAT addr dependent mapping", func(t *testing.T) { //nolint:dupl - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointAddrDependent, - FilteringBehavior: EndpointAddrDependent, - Hairpinning: false, - MappingLifeTime: 30 * time.Second, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") + _, err = nat.translateInbound(iec) + assert.Nil(t, err, "should succeed") + }) - oic1 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }, - ) + t.Run("addr-restricted-cone NAT", func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointIndependent, + FilteringBehavior: EndpointAddrDependent, + Hairpinning: false, + MappingLifeTime: 30 * time.Second, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) + log.Debugf("o-original : %s", oic.String()) + + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + log.Debugf("o-translated: %s", oec.String()) + + // sending different (IP: 5.6.7.9) won't create a new mapping + oic2 := proto.newChunk(t, + proto.newAddr(srcIP, srcPort), + proto.newAddr(net.ParseIP("5.6.7.9"), 9000), + ) + oec2, err := nat.translateOutbound(oic2) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + log.Debugf("o-translated: %s", oec2.String()) + + oecIP, oecPort := natAddrIPPort(t, oec.SourceAddr()) + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort)) + + log.Debugf("i-original : %s", iec.String()) + + iic, err := nat.translateInbound(iec) + if !assert.NoError(t, err, "should succeed") { + return + } + + log.Debugf("i-translated: %s", iic.String()) + + assert.Equal(t, oic.SourceAddr().String(), iic.DestinationAddr().String(), "should match") + + // packet with dest addr that does not exist in the mapping table + // will be dropped + iec = proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort+1)) + + _, err = nat.translateInbound(iec) + log.Debug(err.Error()) + assert.NotNil(t, err, "should fail (dropped)") + + // packet from any port will be accepted (restricted-cone) + iec = proto.newChunk(t, proto.newAddr(dstIP, 7777), proto.newAddr(oecIP, oecPort)) + + _, err = nat.translateInbound(iec) + assert.Nil(t, err, "should succeed") + + // packet from different addr will be dropped (restricted-cone) + iec = proto.newChunk(t, proto.newAddr(net.ParseIP("6.6.6.6"), dstPort), proto.newAddr(oecIP, oecPort)) + + _, err = nat.translateInbound(iec) + log.Debug(err.Error()) + assert.NotNil(t, err, "should fail (dropped)") + }) - oic2 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.100"), - Port: 5678, - }, - ) + t.Run("port-restricted-cone NAT", func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointIndependent, + FilteringBehavior: EndpointAddrPortDependent, + Hairpinning: false, + MappingLifeTime: 30 * time.Second, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) + log.Debugf("o-original : %s", oic.String()) + + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + + log.Debugf("o-translated: %s", oec.String()) + + // sending different (IP: 5.6.7.9) won't create a new mapping + oic2 := proto.newChunk(t, + proto.newAddr(srcIP, srcPort), + proto.newAddr(net.ParseIP("5.6.7.9"), 9000), + ) + oec2, err := nat.translateOutbound(oic2) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + log.Debugf("o-translated: %s", oec2.String()) + + oecIP, oecPort := natAddrIPPort(t, oec.SourceAddr()) + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort)) + + log.Debugf("i-original : %s", iec.String()) + + iic, err := nat.translateInbound(iec) + assert.Nil(t, err, "should succeed") + + log.Debugf("i-translated: %s", iic.String()) + + assert.Equal(t, oic.SourceAddr().String(), iic.DestinationAddr().String(), "should match") + + // packet with dest addr that does not exist in the mapping table + // will be dropped + iec = proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort+1)) + + _, err = nat.translateInbound(iec) + assert.NotNil(t, err, "should fail (dropped)") + + // packet from different port will be dropped (port-restricted-cone) + iec = proto.newChunk(t, proto.newAddr(dstIP, 7777), proto.newAddr(oecIP, oecPort)) + + _, err = nat.translateInbound(iec) + assert.NotNil(t, err, "should fail (dropped)") + + // packet from different addr will be dropped (restricted-cone) + iec = proto.newChunk(t, proto.newAddr(net.ParseIP("6.6.6.6"), dstPort), proto.newAddr(oecIP, oecPort)) + + _, err = nat.translateInbound(iec) + assert.NotNil(t, err, "should fail (dropped)") + }) - oic3 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 6000, - }, - ) - - log.Debugf("o-original : %s", oic1.String()) - log.Debugf("o-original : %s", oic2.String()) - log.Debugf("o-original : %s", oic3.String()) - - oec1, err := nat.translateOutbound(oic1) - assert.Nil(t, err, "should succeed") - - oec2, err := nat.translateOutbound(oic2) - assert.Nil(t, err, "should succeed") - - oec3, err := nat.translateOutbound(oic3) - assert.Nil(t, err, "should succeed") - - assert.Equal(t, 2, len(nat.outboundMap), "should match") - assert.Equal(t, 2, len(nat.inboundMap), "should match") - - log.Debugf("o-translated: %s", oec1.String()) - log.Debugf("o-translated: %s", oec2.String()) - log.Debugf("o-translated: %s", oec3.String()) - - assert.NotEqual( - t, - oec1.(*chunkUDP).sourcePort, //nolint:forcetypeassert - oec2.(*chunkUDP).sourcePort, //nolint:forcetypeassert - "should not match", - ) - assert.Equal( - t, - oec1.(*chunkUDP).sourcePort, //nolint:forcetypeassert - oec3.(*chunkUDP).sourcePort, //nolint:forcetypeassert - "should match", - ) - }) + t.Run("symmetric NAT addr dependent mapping", func(t *testing.T) { //nolint:dupl + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointAddrDependent, + FilteringBehavior: EndpointAddrDependent, + Hairpinning: false, + MappingLifeTime: 30 * time.Second, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + oic1 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.8"), 5678)) + oic2 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.100"), 5678)) + oic3 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.8"), 6000)) + + log.Debugf("o-original : %s", oic1.String()) + log.Debugf("o-original : %s", oic2.String()) + log.Debugf("o-original : %s", oic3.String()) + + oec1, err := nat.translateOutbound(oic1) + assert.Nil(t, err, "should succeed") + + oec2, err := nat.translateOutbound(oic2) + assert.Nil(t, err, "should succeed") + + oec3, err := nat.translateOutbound(oic3) + assert.Nil(t, err, "should succeed") + + assert.Equal(t, 2, len(nat.outboundMap), "should match") + assert.Equal(t, 2, len(nat.inboundMap), "should match") + + log.Debugf("o-translated: %s", oec1.String()) + log.Debugf("o-translated: %s", oec2.String()) + log.Debugf("o-translated: %s", oec3.String()) + + _, p1 := natAddrIPPort(t, oec1.SourceAddr()) + _, p2 := natAddrIPPort(t, oec2.SourceAddr()) + _, p3 := natAddrIPPort(t, oec3.SourceAddr()) + assert.NotEqual(t, p1, p2, "should not match") + assert.Equal(t, p1, p3, "should match") + }) - t.Run("symmetric NAT port dependent mapping", func(t *testing.T) { //nolint:dupl - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointAddrPortDependent, - FilteringBehavior: EndpointAddrPortDependent, - Hairpinning: false, - MappingLifeTime: 30 * time.Second, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, + t.Run("symmetric NAT port dependent mapping", func(t *testing.T) { //nolint:dupl + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointAddrPortDependent, + FilteringBehavior: EndpointAddrPortDependent, + Hairpinning: false, + MappingLifeTime: 30 * time.Second, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + oic1 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.8"), 5678)) + oic2 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.100"), 5678)) + oic3 := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(net.ParseIP("5.6.7.8"), 6000)) + + log.Debugf("o-original : %s", oic1.String()) + log.Debugf("o-original : %s", oic2.String()) + log.Debugf("o-original : %s", oic3.String()) + + oec1, err := nat.translateOutbound(oic1) + assert.Nil(t, err, "should succeed") + + oec2, err := nat.translateOutbound(oic2) + assert.Nil(t, err, "should succeed") + + oec3, err := nat.translateOutbound(oic3) + assert.Nil(t, err, "should succeed") + + assert.Equal(t, 3, len(nat.outboundMap), "should match") + assert.Equal(t, 3, len(nat.inboundMap), "should match") + + log.Debugf("o-translated: %s", oec1.String()) + log.Debugf("o-translated: %s", oec2.String()) + log.Debugf("o-translated: %s", oec3.String()) + + _, p1 := natAddrIPPort(t, oec1.SourceAddr()) + _, p2 := natAddrIPPort(t, oec2.SourceAddr()) + _, p3 := natAddrIPPort(t, oec3.SourceAddr()) + assert.NotEqual(t, p1, p2, "should not match") + assert.NotEqual(t, p1, p3, "should match") + }) }) - assert.NoError(t, err, "should succeed") - - oic1 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }, - ) - - oic2 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.100"), - Port: 5678, - }, - ) - - oic3 := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 6000, - }, - ) - - log.Debugf("o-original : %s", oic1.String()) - log.Debugf("o-original : %s", oic2.String()) - log.Debugf("o-original : %s", oic3.String()) - - oec1, err := nat.translateOutbound(oic1) - assert.Nil(t, err, "should succeed") - - oec2, err := nat.translateOutbound(oic2) - assert.Nil(t, err, "should succeed") - - oec3, err := nat.translateOutbound(oic3) - assert.Nil(t, err, "should succeed") - - assert.Equal(t, 3, len(nat.outboundMap), "should match") - assert.Equal(t, 3, len(nat.inboundMap), "should match") - - log.Debugf("o-translated: %s", oec1.String()) - log.Debugf("o-translated: %s", oec2.String()) - log.Debugf("o-translated: %s", oec3.String()) - - assert.NotEqual( - t, - oec1.(*chunkUDP).sourcePort, //nolint:forcetypeassert - oec2.(*chunkUDP).sourcePort, //nolint:forcetypeassert - "should not match", - ) - assert.NotEqual( - t, - oec1.(*chunkUDP).sourcePort, //nolint:forcetypeassert - oec3.(*chunkUDP).sourcePort, //nolint:forcetypeassert - "should match", - ) - }) + } } func TestNATMappingTimeout(t *testing.T) { @@ -542,278 +402,209 @@ func TestNATMappingTimeout(t *testing.T) { log := loggerFactory.NewLogger("test") t.Run("refresh on outbound", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointIndependent, - FilteringBehavior: EndpointIndependent, - Hairpinning: false, - MappingLifeTime: 100 * time.Millisecond, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") - - src := &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointIndependent, + FilteringBehavior: EndpointIndependent, + Hairpinning: false, + MappingLifeTime: 100 * time.Millisecond, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) + + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + + log.Debugf("o-original : %s", oic.String()) + log.Debugf("o-translated: %s", oec.String()) + + mapped := oec.SourceAddr().String() + + time.Sleep(75 * time.Millisecond) + + // refresh + oec, err = nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + + log.Debugf("o-original : %s", oic.String()) + log.Debugf("o-translated: %s", oec.String()) + + assert.Equal(t, mapped, oec.SourceAddr().String(), "mapped addr should match") + + // sleep long enough for the mapping to expire + time.Sleep(125 * time.Millisecond) + + // refresh after expiration + oec, err = nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + assert.NotEqual(t, mapped, oec.SourceAddr().String(), "mapped addr should not match") + }) } - - oic := newChunkUDP(src, dst) - - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - // record mapped addr - mapped := oec.(*chunkUDP).SourceAddr().String() //nolint:forcetypeassert - - time.Sleep(75 * time.Millisecond) - - // refresh - oec, err = nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - assert.Equal(t, mapped, oec.(*chunkUDP).SourceAddr().String(), "mapped addr should match") //nolint:forcetypeassert - - // sleep long enough for the mapping to expire - time.Sleep(125 * time.Millisecond) - - // refresh after expiration - oec, err = nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - assert.NotEqual( - t, - mapped, - oec.(*chunkUDP).SourceAddr().String(), //nolint:forcetypeassert - "mapped addr should not match", - ) }) t.Run("outbound detects timeout", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - MappingBehavior: EndpointIndependent, - FilteringBehavior: EndpointIndependent, - Hairpinning: false, - MappingLifeTime: 100 * time.Millisecond, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - loggerFactory: loggerFactory, - }) - assert.NoError(t, err, "should succeed") - - src := &net.UDPAddr{ - IP: net.ParseIP("192.168.0.2"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + MappingBehavior: EndpointIndependent, + FilteringBehavior: EndpointIndependent, + Hairpinning: false, + MappingLifeTime: 100 * time.Millisecond, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + loggerFactory: loggerFactory, + }) + assert.NoError(t, err, "should succeed") + + srcIP := net.ParseIP("192.168.0.2") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) + + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Equal(t, 1, len(nat.outboundMap), "should match") + assert.Equal(t, 1, len(nat.inboundMap), "should match") + + log.Debugf("o-original : %s", oic.String()) + log.Debugf("o-translated: %s", oec.String()) + + // sleep long enough for the mapping to expire + time.Sleep(125 * time.Millisecond) + + oecIP, oecPort := natAddrIPPort(t, oec.SourceAddr()) + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort)) + log.Debugf("i-original : %s", iec.String()) + + iic, err := nat.translateInbound(iec) + assert.NotNil(t, err, "should drop") + assert.Nil(t, iic, "should be nil") + assert.Empty(t, nat.outboundMap, "should have no binding") + assert.Empty(t, nat.inboundMap, "should have no binding") + }) } - - oic := newChunkUDP(src, dst) - - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Equal(t, 1, len(nat.outboundMap), "should match") - assert.Equal(t, 1, len(nat.inboundMap), "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - // sleep long enough for the mapping to expire - time.Sleep(125 * time.Millisecond) - - //nolint:forcetypeassert - iec := newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - log.Debugf("i-original : %s", iec.String()) - - _, err = nat.translateInbound(iec) - assert.NotNil(t, err, "should drop") - assert.Empty(t, nat.outboundMap, "should have no binding") - assert.Empty(t, nat.inboundMap, "should have no binding") }) } -func TestNAT1To1Behavior(t *testing.T) { +func TestNAT1To1Behavior(t *testing.T) { // nolint:cyclop loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") t.Run("1:1 NAT with one mapping", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - Mode: NATModeNAT1To1, - }, - mappedIPs: []net.IP{net.ParseIP(demoIP)}, - localIPs: []net.IP{net.ParseIP("10.0.0.1")}, - loggerFactory: loggerFactory, - }) - if !assert.NoError(t, err, "should succeed") { - return - } - - src := &net.UDPAddr{ - IP: net.ParseIP("10.0.0.1"), - Port: 1234, - } - dst := &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + Mode: NATModeNAT1To1, + }, + mappedIPs: []net.IP{net.ParseIP(demoIP)}, + localIPs: []net.IP{net.ParseIP("10.0.0.1")}, + loggerFactory: loggerFactory, + }) + if !assert.NoError(t, err, "should succeed") { + return + } + + srcIP := net.ParseIP("10.0.0.1") + srcPort := 1234 + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(srcIP, srcPort), proto.newAddr(dstIP, dstPort)) + + oec, err := nat.translateOutbound(oic) + assert.Nil(t, err, "should succeed") + assert.Empty(t, nat.outboundMap, "should match") + assert.Empty(t, nat.inboundMap, "should match") + + log.Debugf("o-original : %s", oic.String()) + log.Debugf("o-translated: %s", oec.String()) + assert.Equal(t, "1.2.3.4:1234", oec.SourceAddr().String(), "should match") + + oecIP, oecPort := natAddrIPPort(t, oec.SourceAddr()) + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(oecIP, oecPort)) + log.Debugf("i-original : %s", iec.String()) + + iic, err := nat.translateInbound(iec) + assert.Nil(t, err, "should succeed") + log.Debugf("i-translated: %s", iic.String()) + assert.Equal(t, oic.SourceAddr().String(), iic.DestinationAddr().String(), "should match") + }) } - - oic := newChunkUDP(src, dst) - - oec, err := nat.translateOutbound(oic) - assert.Nil(t, err, "should succeed") - assert.Empty(t, nat.outboundMap, "should match") - assert.Empty(t, nat.inboundMap, "should match") - - log.Debugf("o-original : %s", oic.String()) - log.Debugf("o-translated: %s", oec.String()) - - assert.Equal(t, "1.2.3.4:1234", oec.SourceAddr().String(), "should match") - - //nolint:forcetypeassert - iec := newChunkUDP( - &net.UDPAddr{ - IP: dst.IP, - Port: dst.Port, - }, - &net.UDPAddr{ - IP: oec.(*chunkUDP).sourceIP, - Port: oec.(*chunkUDP).sourcePort, - }, - ) - - log.Debugf("i-original : %s", iec.String()) - - iic, err := nat.translateInbound(iec) - assert.Nil(t, err, "should succeed") - - log.Debugf("i-translated: %s", iic.String()) - - assert.Equal(t, - oic.SourceAddr().String(), - iic.DestinationAddr().String(), - "should match") }) t.Run("1:1 NAT with more than one mapping", func(t *testing.T) { - nat, err := newNAT(&natConfig{ - natType: NATType{ - Mode: NATModeNAT1To1, - }, - mappedIPs: []net.IP{ - net.ParseIP(demoIP), - net.ParseIP("1.2.3.5"), - }, - localIPs: []net.IP{ - net.ParseIP("10.0.0.1"), - net.ParseIP("10.0.0.2"), - }, - loggerFactory: loggerFactory, - }) - if !assert.NoError(t, err, "should succeed") { - return - } - - // outbound translation - - before := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("10.0.0.1"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }) - - after, err := nat.translateOutbound(before) - if !assert.NoError(t, err, "should succeed") { - return - } - assert.Equal(t, "1.2.3.4:1234", after.SourceAddr().String(), "should match") - - before = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("10.0.0.2"), - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + nat, err := newNAT(&natConfig{ + natType: NATType{ + Mode: NATModeNAT1To1, + }, + mappedIPs: []net.IP{ + net.ParseIP(demoIP), + net.ParseIP("1.2.3.5"), + }, + localIPs: []net.IP{ + net.ParseIP("10.0.0.1"), + net.ParseIP("10.0.0.2"), + }, + loggerFactory: loggerFactory, + }) + if !assert.NoError(t, err, "should succeed") { + return + } + + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + + // outbound translation + oic := proto.newChunk(t, proto.newAddr(net.ParseIP("10.0.0.1"), 1234), proto.newAddr(dstIP, dstPort)) + oec, err := nat.translateOutbound(oic) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, "1.2.3.4:1234", oec.SourceAddr().String(), "should match") + + oic = proto.newChunk(t, proto.newAddr(net.ParseIP("10.0.0.2"), 1234), proto.newAddr(dstIP, dstPort)) + oec, err = nat.translateOutbound(oic) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, "1.2.3.5:1234", oec.SourceAddr().String(), "should match") + + // inbound translation + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(net.ParseIP(demoIP), 2525)) + iic, err := nat.translateInbound(iec) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, "10.0.0.1:2525", iic.DestinationAddr().String(), "should match") + + iec = proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(net.ParseIP("1.2.3.5"), 9847)) + iic, err = nat.translateInbound(iec) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Equal(t, "10.0.0.2:9847", iic.DestinationAddr().String(), "should match") }) - - after, err = nat.translateOutbound(before) - if !assert.NoError(t, err, "should succeed") { - return - } - assert.Equal(t, "1.2.3.5:1234", after.SourceAddr().String(), "should match") - - // inbound translation - - before = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }, - &net.UDPAddr{ - IP: net.ParseIP(demoIP), - Port: 2525, - }) - - after, err = nat.translateInbound(before) - if !assert.NoError(t, err, "should succeed") { - return } - assert.Equal(t, "10.0.0.1:2525", after.DestinationAddr().String(), "should match") - - before = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }, - &net.UDPAddr{ - IP: net.ParseIP("1.2.3.5"), - Port: 9847, - }) - - after, err = nat.translateInbound(before) - if !assert.NoError(t, err, "should succeed") { - return - } - assert.Equal(t, "10.0.0.2:9847", after.DestinationAddr().String(), "should match") }) t.Run("1:1 NAT failure", func(t *testing.T) { @@ -858,35 +649,21 @@ func TestNAT1To1Behavior(t *testing.T) { }) assert.NoError(t, err, "should succeed") - before := newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("10.0.0.2"), // no external mapping for this - Port: 1234, - }, - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, + for _, proto := range natTestProtos() { + t.Run(proto.name, func(t *testing.T) { + dstIP := net.ParseIP("5.6.7.8") + dstPort := 5678 + oic := proto.newChunk(t, proto.newAddr(net.ParseIP("10.0.0.2"), 1234), proto.newAddr(dstIP, dstPort)) + oec, err := nat.translateOutbound(oic) + if !assert.NoError(t, err, "should succeed") { + return + } + assert.Nil(t, oec, "should be nil") + + iec := proto.newChunk(t, proto.newAddr(dstIP, dstPort), proto.newAddr(net.ParseIP("10.0.0.2"), 1234)) + _, err = nat.translateInbound(iec) + assert.Error(t, err, "should fail") }) - - after, err := nat.translateOutbound(before) - if !assert.NoError(t, err, "should succeed") { - return } - if !assert.Nil(t, after, "should be nil") { - return - } - - before = newChunkUDP( - &net.UDPAddr{ - IP: net.ParseIP("5.6.7.8"), - Port: 5678, - }, - &net.UDPAddr{ - IP: net.ParseIP("10.0.0.2"), // no local mapping for this - Port: 1234, - }) - - _, err = nat.translateInbound(before) - assert.Error(t, err, "should fail") }) }