diff --git a/pkg/tcpip/header/icmpv4.go b/pkg/tcpip/header/icmpv4.go index 91c1c3cd2..a7715aefe 100644 --- a/pkg/tcpip/header/icmpv4.go +++ b/pkg/tcpip/header/icmpv4.go @@ -185,6 +185,13 @@ func (b ICMPv4) SetIdent(ident uint16) { binary.BigEndian.PutUint16(b[icmpv4IdentOffset:], ident) } +// SetIdentWithChecksumUpdate sets the Ident field and updates the checksum. +func (b ICMPv4) SetIdentWithChecksumUpdate(new uint16) { + old := b.Ident() + b.SetIdent(new) + b.SetChecksum(^checksumUpdate2ByteAlignedUint16(^b.Checksum(), old, new)) +} + // Sequence retrieves the Sequence field from an ICMPv4 message. func (b ICMPv4) Sequence() uint16 { return binary.BigEndian.Uint16(b[icmpv4SequenceOffset:]) diff --git a/pkg/tcpip/stack/conntrack.go b/pkg/tcpip/stack/conntrack.go index c80d95a5a..caa846565 100644 --- a/pkg/tcpip/stack/conntrack.go +++ b/pkg/tcpip/stack/conntrack.go @@ -17,6 +17,7 @@ package stack import ( "encoding/binary" "fmt" + "math" "math/rand" "sync" "sync/atomic" @@ -70,29 +71,38 @@ func (t *tuple) id() tupleID { return t.tupleID } -// tupleID uniquely identifies a connection in one direction. It currently -// contains enough information to distinguish between any TCP or UDP -// connection, and will need to be extended to support other protocols. +// tupleID uniquely identifies a trackable connection in one direction. // // +stateify savable type tupleID struct { - srcAddr tcpip.Address - srcPort uint16 - dstAddr tcpip.Address - dstPort uint16 - transProto tcpip.TransportProtocolNumber - netProto tcpip.NetworkProtocolNumber + srcAddr tcpip.Address + // The source port of a packet in the original direction is overloaded with + // the ident of an Echo Request packet. + // + // This also matches the behaviour of sending packets on Linux where the + // socket's source port value is used for the source port of outgoing packets + // for TCP/UDP and the ident field for outgoing Echo Requests on Ping sockets: + // + // IPv4: https://github.com/torvalds/linux/blob/c5c17547b778975b3d83a73c8d84e8fb5ecf3ba5/net/ipv4/ping.c#L810 + // IPv6: https://github.com/torvalds/linux/blob/c5c17547b778975b3d83a73c8d84e8fb5ecf3ba5/net/ipv6/ping.c#L133 + srcPortOrEchoRequestIdent uint16 + dstAddr tcpip.Address + // The opposite of srcPortOrEchoRequestIdent; the destination port of a packet + // in the reply direction is overloaded with the ident of an Echo Reply. + dstPortOrEchoReplyIdent uint16 + transProto tcpip.TransportProtocolNumber + netProto tcpip.NetworkProtocolNumber } // reply creates the reply tupleID. func (ti tupleID) reply() tupleID { return tupleID{ - srcAddr: ti.dstAddr, - srcPort: ti.dstPort, - dstAddr: ti.srcAddr, - dstPort: ti.srcPort, - transProto: ti.transProto, - netProto: ti.netProto, + srcAddr: ti.dstAddr, + srcPortOrEchoRequestIdent: ti.dstPortOrEchoReplyIdent, + dstAddr: ti.srcAddr, + dstPortOrEchoReplyIdent: ti.srcPortOrEchoRequestIdent, + transProto: ti.transProto, + netProto: ti.netProto, } } @@ -284,7 +294,7 @@ func getEmbeddedNetAndTransHeaders(pkt *PacketBuffer, netHdrLength int, getNetAn return nil, nil, false } -func getHeaders(pkt *PacketBuffer) (netHdr header.Network, transHdr header.ChecksummableTransport, isICMPError bool, ok bool) { +func getHeaders(pkt *PacketBuffer) (netHdr header.Network, transHdr header.Transport, isICMPError bool, ok bool) { switch pkt.TransportProtocolNumber { case header.TCPProtocolNumber: if tcpHeader := header.TCP(pkt.TransportHeader().View()); len(tcpHeader) >= header.TCPMinimumSize { @@ -295,6 +305,19 @@ func getHeaders(pkt *PacketBuffer) (netHdr header.Network, transHdr header.Check return pkt.Network(), udpHeader, false, true } case header.ICMPv4ProtocolNumber: + icmpHeader := header.ICMPv4(pkt.TransportHeader().View()) + if len(icmpHeader) < header.ICMPv4MinimumSize { + break + } + + switch icmpType := icmpHeader.Type(); icmpType { + case header.ICMPv4Echo, header.ICMPv4EchoReply: + return pkt.Network(), icmpHeader, false, true + case header.ICMPv4DstUnreachable, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem: + default: + panic(fmt.Sprintf("unexpected ICMPv4 type = %d", icmpType)) + } + h, ok := pkt.Data().PullUp(header.IPv4MinimumSize) if !ok { panic(fmt.Sprintf("should have a valid IPv4 packet; only have %d bytes, want at least %d bytes", pkt.Data().Size(), header.IPv4MinimumSize)) @@ -333,24 +356,24 @@ func getHeaders(pkt *PacketBuffer) (netHdr header.Network, transHdr header.Check func getTupleIDForRegularPacket(netHdr header.Network, netProto tcpip.NetworkProtocolNumber, transHdr header.Transport, transProto tcpip.TransportProtocolNumber) tupleID { return tupleID{ - srcAddr: netHdr.SourceAddress(), - srcPort: transHdr.SourcePort(), - dstAddr: netHdr.DestinationAddress(), - dstPort: transHdr.DestinationPort(), - transProto: transProto, - netProto: netProto, + srcAddr: netHdr.SourceAddress(), + srcPortOrEchoRequestIdent: transHdr.SourcePort(), + dstAddr: netHdr.DestinationAddress(), + dstPortOrEchoReplyIdent: transHdr.DestinationPort(), + transProto: transProto, + netProto: netProto, } } func getTupleIDForPacketInICMPError(pkt *PacketBuffer, getNetAndTransHdr netAndTransHeadersFunc, netProto tcpip.NetworkProtocolNumber, netLen int, transProto tcpip.TransportProtocolNumber) (tupleID, bool) { if netHdr, transHdr, ok := getEmbeddedNetAndTransHeaders(pkt, netLen, getNetAndTransHdr, transProto); ok { return tupleID{ - srcAddr: netHdr.DestinationAddress(), - srcPort: transHdr.DestinationPort(), - dstAddr: netHdr.SourceAddress(), - dstPort: transHdr.SourcePort(), - transProto: transProto, - netProto: netProto, + srcAddr: netHdr.DestinationAddress(), + srcPortOrEchoRequestIdent: transHdr.DestinationPort(), + dstAddr: netHdr.SourceAddress(), + dstPortOrEchoReplyIdent: transHdr.SourcePort(), + transProto: transProto, + netProto: netProto, }, true } @@ -365,6 +388,24 @@ const ( getTupleIDOKAndDontAllowNewConn ) +func getTupleIDForEchoPacket(pkt *PacketBuffer, ident uint16, request bool) tupleID { + netHdr := pkt.Network() + tid := tupleID{ + srcAddr: netHdr.SourceAddress(), + dstAddr: netHdr.DestinationAddress(), + transProto: pkt.TransportProtocolNumber, + netProto: pkt.NetworkProtocolNumber, + } + + if request { + tid.srcPortOrEchoRequestIdent = ident + } else { + tid.dstPortOrEchoReplyIdent = ident + } + + return tid +} + func getTupleID(pkt *PacketBuffer) (tupleID, getTupleIDDisposition) { switch pkt.TransportProtocolNumber { case header.TCPProtocolNumber: @@ -382,8 +423,16 @@ func getTupleID(pkt *PacketBuffer) (tupleID, getTupleIDDisposition) { } switch icmp.Type() { + case header.ICMPv4Echo: + return getTupleIDForEchoPacket(pkt, icmp.Ident(), true /* request */), getTupleIDOKAndAllowNewConn + case header.ICMPv4EchoReply: + // Do not create a new connection in response to a reply packet as only + // the first packet of a connection should create a conntrack entry but + // a reply is never the first packet sent for a connection. + return getTupleIDForEchoPacket(pkt, icmp.Ident(), false /* request */), getTupleIDOKAndDontAllowNewConn case header.ICMPv4DstUnreachable, header.ICMPv4TimeExceeded, header.ICMPv4ParamProblem: default: + // Unsupported ICMP type for NAT-ing. return tupleID{}, getTupleIDNotOK } @@ -614,9 +663,9 @@ func (cn *conn) maybePerformNoopNAT(dnat bool) { } } -type portRange struct { +type portOrIdentRange struct { start uint16 - size uint16 + size uint32 } // performNAT setups up the connection for the specified NAT and rewrites the @@ -628,7 +677,15 @@ type portRange struct { // // Generally, only the first packet of a connection reaches this method; other // packets will be manipulated without needing to modify the connection. -func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, ports portRange, natAddress tcpip.Address, dnat bool) { +func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, portsOrIdents portOrIdentRange, natAddress tcpip.Address, dnat bool) { + lastPortOrIdent := func() uint16 { + lastPortOrIdent := uint32(portsOrIdents.start) + portsOrIdents.size - 1 + if lastPortOrIdent > math.MaxUint16 { + panic(fmt.Sprintf("got lastPortOrIdent = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", lastPortOrIdent, math.MaxUint16, portsOrIdents)) + } + return uint16(lastPortOrIdent) + }() + // Make sure the packet is re-written after performing NAT. defer func() { // handlePacket returns true if the packet may skip the NAT table as the @@ -645,15 +702,15 @@ func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, ports portRan var manip *manipType var address *tcpip.Address - var port *uint16 + var portOrIdent *uint16 if dnat { manip = &cn.destinationManip address = &cn.reply.tupleID.srcAddr - port = &cn.reply.tupleID.srcPort + portOrIdent = &cn.reply.tupleID.srcPortOrEchoRequestIdent } else { manip = &cn.sourceManip address = &cn.reply.tupleID.dstAddr - port = &cn.reply.tupleID.dstPort + portOrIdent = &cn.reply.tupleID.dstPortOrEchoReplyIdent } if *manip != manipNotPerformed { @@ -662,8 +719,8 @@ func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, ports portRan *manip = manipPerformed *address = natAddress - // Does the current port fit in the range? - if end := ports.start + ports.size - 1; *port >= ports.start && *port <= end { + // Does the current port/ident fit in the range? + if portsOrIdents.start <= *portOrIdent && *portOrIdent <= lastPortOrIdent { // Yes, is the current reply tuple unique? if other := cn.ct.connForTID(cn.reply.tupleID); other == nil { // Yes! No need to change the port. @@ -671,27 +728,32 @@ func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, ports portRan } } - // Try our best to find a port that results in a unique reply tuple. + // Try our best to find a port/ident that results in a unique reply tuple. // // We limit the number of attempts to find a unique tuple to not waste a lot // of time looking for a unique tuple. // // Matches linux behaviour introduced in // https://github.com/torvalds/linux/commit/a504b703bb1da526a01593da0e4be2af9d9f5fa8. - const maxAttemptsForInitialRound uint16 = 128 + const maxAttemptsForInitialRound uint32 = 128 const minAttemptsToContinue = 16 allowedInitialAttempts := maxAttemptsForInitialRound - if allowedInitialAttempts > ports.size { - allowedInitialAttempts = ports.size + if allowedInitialAttempts > portsOrIdents.size { + allowedInitialAttempts = portsOrIdents.size } for maxAttempts := allowedInitialAttempts; ; maxAttempts /= 2 { - // Start reach round with a random initial port in the range. - initial := ports.start + uint16(cn.ct.rand.Uint32())%ports.size + // Start reach round with a random initial port/ident offset. + randOffset := cn.ct.rand.Uint32() - for i := uint16(0); i < maxAttempts; i++ { - *port = initial + i%ports.size + for i := uint32(0); i < maxAttempts; i++ { + newPortOrIdentU32 := uint32(portsOrIdents.start) + (randOffset+i)%portsOrIdents.size + if newPortOrIdentU32 > math.MaxUint16 { + panic(fmt.Sprintf("got newPortOrIdentU32 = %d, want <= MaxUint16(=%d); portsOrIdents=%#v, randOffset=%d", newPortOrIdentU32, math.MaxUint16, portsOrIdents, randOffset)) + } + + *portOrIdent = uint16(newPortOrIdentU32) if other := cn.ct.connForTID(cn.reply.tupleID); other == nil { // We found a unique tuple! @@ -699,8 +761,9 @@ func (cn *conn) performNAT(pkt *PacketBuffer, hook Hook, r *Route, ports portRan } } - if maxAttempts == ports.size { - // We already tried all the ports in the range so no need to keep trying. + if maxAttempts == portsOrIdents.size { + // We already tried all the ports/idents in the range so no need to keep + // trying. return } @@ -795,10 +858,10 @@ func (cn *conn) handlePacket(pkt *PacketBuffer, hook Hook, rt *Route) bool { panic(fmt.Sprintf("unhandled manip = %d", manip)) } - newPort := tid.dstPort + newPort := tid.dstPortOrEchoReplyIdent newAddr := tid.dstAddr if dnat { - newPort = tid.srcPort + newPort = tid.srcPortOrEchoRequestIdent newAddr = tid.srcAddr } @@ -871,9 +934,9 @@ func (ct *ConnTrack) bucket(id tupleID) int { h.Write([]byte(id.srcAddr)) h.Write([]byte(id.dstAddr)) shortBuf := make([]byte, 2) - binary.LittleEndian.PutUint16(shortBuf, id.srcPort) + binary.LittleEndian.PutUint16(shortBuf, id.srcPortOrEchoRequestIdent) h.Write([]byte(shortBuf)) - binary.LittleEndian.PutUint16(shortBuf, id.dstPort) + binary.LittleEndian.PutUint16(shortBuf, id.dstPortOrEchoReplyIdent) h.Write([]byte(shortBuf)) binary.LittleEndian.PutUint16(shortBuf, uint16(id.transProto)) h.Write([]byte(shortBuf)) @@ -998,12 +1061,12 @@ func (ct *ConnTrack) originalDst(epID TransportEndpointID, netProto tcpip.Networ // Lookup the connection. The reply's original destination // describes the original address. tid := tupleID{ - srcAddr: epID.LocalAddress, - srcPort: epID.LocalPort, - dstAddr: epID.RemoteAddress, - dstPort: epID.RemotePort, - transProto: transProto, - netProto: netProto, + srcAddr: epID.LocalAddress, + srcPortOrEchoRequestIdent: epID.LocalPort, + dstAddr: epID.RemoteAddress, + dstPortOrEchoReplyIdent: epID.RemotePort, + transProto: transProto, + netProto: netProto, } t := ct.connForTID(tid) if t == nil { @@ -1019,5 +1082,5 @@ func (ct *ConnTrack) originalDst(epID TransportEndpointID, netProto tcpip.Networ } id := t.conn.original.id() - return id.dstAddr, id.dstPort, nil + return id.dstAddr, id.dstPortOrEchoReplyIdent, nil } diff --git a/pkg/tcpip/stack/iptables_targets.go b/pkg/tcpip/stack/iptables_targets.go index 1ad1ff40f..23b16c94c 100644 --- a/pkg/tcpip/stack/iptables_targets.go +++ b/pkg/tcpip/stack/iptables_targets.go @@ -176,48 +176,57 @@ type SNATTarget struct { } func dnatAction(pkt *PacketBuffer, hook Hook, r *Route, port uint16, address tcpip.Address) (RuleVerdict, int) { - return natAction(pkt, hook, r, portRange{start: port, size: 1}, address, true /* dnat */) + return natAction(pkt, hook, r, portOrIdentRange{start: port, size: 1}, address, true /* dnat */) +} + +func targetPortRangeForTCPAndUDP(originalSrcPort uint16) portOrIdentRange { + // As per iptables(8), + // + // If no port range is specified, then source ports below 512 will be + // mapped to other ports below 512: those between 512 and 1023 inclusive + // will be mapped to ports below 1024, and other ports will be mapped to + // 1024 or above. + switch { + case originalSrcPort < 512: + return portOrIdentRange{start: 1, size: 511} + case originalSrcPort < 1024: + return portOrIdentRange{start: 1, size: 1023} + default: + return portOrIdentRange{start: 1024, size: math.MaxUint16 - 1023} + } } func snatAction(pkt *PacketBuffer, hook Hook, r *Route, port uint16, address tcpip.Address) (RuleVerdict, int) { - ports := portRange{start: port, size: 1} - if port == 0 { - // As per iptables(8), - // - // If no port range is specified, then source ports below 512 will be - // mapped to other ports below 512: those between 512 and 1023 inclusive - // will be mapped to ports below 1024, and other ports will be mapped to - // 1024 or above. - switch protocol := pkt.TransportProtocolNumber; protocol { - case header.UDPProtocolNumber: - port = header.UDP(pkt.TransportHeader().View()).SourcePort() - case header.TCPProtocolNumber: - port = header.TCP(pkt.TransportHeader().View()).SourcePort() - default: - panic(fmt.Sprintf("unsupported transport protocol = %d", pkt.TransportProtocolNumber)) - } + portsOrIdents := portOrIdentRange{start: port, size: 1} - switch { - case port < 512: - ports = portRange{start: 1, size: 511} - case port < 1024: - ports = portRange{start: 1, size: 1023} - default: - ports = portRange{start: 1024, size: math.MaxUint16 - 1023} + switch pkt.TransportProtocolNumber { + case header.UDPProtocolNumber: + if port == 0 { + portsOrIdents = targetPortRangeForTCPAndUDP(header.UDP(pkt.TransportHeader().View()).SourcePort()) } + case header.TCPProtocolNumber: + if port == 0 { + portsOrIdents = targetPortRangeForTCPAndUDP(header.TCP(pkt.TransportHeader().View()).SourcePort()) + } + case header.ICMPv4ProtocolNumber: + // Allow NAT-ing to any 16-bit value for ICMP's Ident field to match Linux + // behaviour. + // + // https://github.com/torvalds/linux/blob/58e1100fdc5990b0cc0d4beaf2562a92e621ac7d/net/netfilter/nf_nat_core.c#L391 + portsOrIdents = portOrIdentRange{start: 0, size: math.MaxUint16 + 1} } - return natAction(pkt, hook, r, ports, address, false /* dnat */) + return natAction(pkt, hook, r, portsOrIdents, address, false /* dnat */) } -func natAction(pkt *PacketBuffer, hook Hook, r *Route, ports portRange, address tcpip.Address, dnat bool) (RuleVerdict, int) { +func natAction(pkt *PacketBuffer, hook Hook, r *Route, portsOrIdents portOrIdentRange, address tcpip.Address, dnat bool) (RuleVerdict, int) { // Drop the packet if network and transport header are not set. if pkt.NetworkHeader().View().IsEmpty() || pkt.TransportHeader().View().IsEmpty() { return RuleDrop, 0 } if t := pkt.tuple; t != nil { - t.conn.performNAT(pkt, hook, r, ports, address, dnat) + t.conn.performNAT(pkt, hook, r, portsOrIdents, address, dnat) return RuleAccept, 0 } @@ -280,30 +289,48 @@ func (mt *MasqueradeTarget) Action(pkt *PacketBuffer, hook Hook, r *Route, addre return snatAction(pkt, hook, r, 0 /* port */, address) } -func rewritePacket(n header.Network, t header.ChecksummableTransport, updateSRCFields, fullChecksum, updatePseudoHeader bool, newPort uint16, newAddr tcpip.Address) { - if updateSRCFields { - if fullChecksum { - t.SetSourcePortWithChecksumUpdate(newPort) - } else { - t.SetSourcePort(newPort) - } - } else { - if fullChecksum { - t.SetDestinationPortWithChecksumUpdate(newPort) - } else { - t.SetDestinationPort(newPort) - } - } - - if updatePseudoHeader { - var oldAddr tcpip.Address +func rewritePacket(n header.Network, t header.Transport, updateSRCFields, fullChecksum, updatePseudoHeader bool, newPort uint16, newAddr tcpip.Address) { + switch t := t.(type) { + case header.ChecksummableTransport: if updateSRCFields { - oldAddr = n.SourceAddress() + if fullChecksum { + t.SetSourcePortWithChecksumUpdate(newPort) + } else { + t.SetSourcePort(newPort) + } } else { - oldAddr = n.DestinationAddress() + if fullChecksum { + t.SetDestinationPortWithChecksumUpdate(newPort) + } else { + t.SetDestinationPort(newPort) + } } - t.UpdateChecksumPseudoHeaderAddress(oldAddr, newAddr, fullChecksum) + if updatePseudoHeader { + var oldAddr tcpip.Address + if updateSRCFields { + oldAddr = n.SourceAddress() + } else { + oldAddr = n.DestinationAddress() + } + + t.UpdateChecksumPseudoHeaderAddress(oldAddr, newAddr, fullChecksum) + } + case header.ICMPv4: + switch icmpType := t.Type(); icmpType { + case header.ICMPv4Echo: + if updateSRCFields { + t.SetIdentWithChecksumUpdate(newPort) + } + case header.ICMPv4EchoReply: + if !updateSRCFields { + t.SetIdentWithChecksumUpdate(newPort) + } + default: + panic(fmt.Sprintf("unexpected ICMPv4 type = %d", icmpType)) + } + default: + panic(fmt.Sprintf("unhandled transport = %#v", t)) } if checksummableNetHeader, ok := n.(header.ChecksummableNetwork); ok { diff --git a/pkg/tcpip/tests/integration/BUILD b/pkg/tcpip/tests/integration/BUILD index 99f4d4d0e..4336ce601 100644 --- a/pkg/tcpip/tests/integration/BUILD +++ b/pkg/tcpip/tests/integration/BUILD @@ -40,6 +40,7 @@ go_test( "//pkg/tcpip/stack", "//pkg/tcpip/tests/utils", "//pkg/tcpip/testutil", + "//pkg/tcpip/transport/icmp", "//pkg/tcpip/transport/tcp", "//pkg/tcpip/transport/udp", "//pkg/waiter", diff --git a/pkg/tcpip/tests/integration/iptables_test.go b/pkg/tcpip/tests/integration/iptables_test.go index f7333f30b..0cfa25885 100644 --- a/pkg/tcpip/tests/integration/iptables_test.go +++ b/pkg/tcpip/tests/integration/iptables_test.go @@ -32,6 +32,7 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/tests/utils" "gvisor.dev/gvisor/pkg/tcpip/testutil" + "gvisor.dev/gvisor/pkg/tcpip/transport/icmp" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/waiter" @@ -1300,6 +1301,15 @@ var ( }, } + dnatTarget = natType{ + name: "DNAT", + setupNAT: func(t *testing.T, s *stack.Stack, netProto tcpip.NetworkProtocolNumber, transProto tcpip.TransportProtocolNumber, _, dnatAddr tcpip.Address, dnatPort uint16) { + t.Helper() + + setupDNAT(t, s, netProto, transProto, &stack.DNATTarget{NetworkProtocol: netProto, Addr: dnatAddr, Port: dnatPort}) + }, + } + dnatTypes = []natType{ { name: "Redirect", @@ -1309,14 +1319,7 @@ var ( setupDNAT(t, s, netProto, transProto, &stack.RedirectTarget{NetworkProtocol: netProto, Port: dnatPort}) }, }, - { - name: "DNAT", - setupNAT: func(t *testing.T, s *stack.Stack, netProto tcpip.NetworkProtocolNumber, transProto tcpip.TransportProtocolNumber, _, dnatAddr tcpip.Address, dnatPort uint16) { - t.Helper() - - setupDNAT(t, s, netProto, transProto, &stack.DNATTarget{NetworkProtocol: netProto, Addr: dnatAddr, Port: dnatPort}) - }, - }, + dnatTarget, } twiceNATTypes = []natType{ @@ -1339,6 +1342,140 @@ var ( } ) +func TestNATEcho(t *testing.T) { + const ident = 1 + + v4EchoPkt := func(srcAddr, dstAddr tcpip.Address, reply bool) buffer.View { + icmpType := header.ICMPv4Echo + if reply { + icmpType = header.ICMPv4EchoReply + } + + return icmpv4Packet(srcAddr, dstAddr, icmpType, ident) + } + + checkV4EchoPkt := func(t *testing.T, v buffer.View, srcAddr, dstAddr tcpip.Address, reply bool) { + t.Helper() + + icmpType := header.ICMPv4Echo + if reply { + icmpType = header.ICMPv4EchoReply + } + + checker.IPv4(t, v, + checker.SrcAddr(srcAddr), + checker.DstAddr(dstAddr), + checker.ICMPv4( + checker.ICMPv4Type(icmpType), + checker.ICMPv4Checksum(), + ), + ) + } + + type natTypeTest struct { + name string + natTypes []natType + requestSrc, requestDst tcpip.Address + expectedRequestSrc, expectedRequestDst tcpip.Address + } + + tests := []struct { + name string + netProto tcpip.NetworkProtocolNumber + transProto tcpip.TransportProtocolNumber + echoPkt func(srcAddr, dstAddr tcpip.Address, reply bool) buffer.View + checkEchoPkt func(t *testing.T, v buffer.View, srcAddr, dstAddr tcpip.Address, reply bool) + + natTypes []natTypeTest + }{ + { + name: "IPv4", + netProto: header.IPv4ProtocolNumber, + transProto: header.ICMPv4ProtocolNumber, + echoPkt: v4EchoPkt, + checkEchoPkt: checkV4EchoPkt, + + natTypes: []natTypeTest{ + { + name: "SNAT", + natTypes: snatTypes, + requestSrc: utils.Host2IPv4Addr.AddressWithPrefix.Address, + requestDst: utils.Host1IPv4Addr.AddressWithPrefix.Address, + expectedRequestSrc: utils.RouterNIC1IPv4Addr.AddressWithPrefix.Address, + expectedRequestDst: utils.Host1IPv4Addr.AddressWithPrefix.Address, + }, + { + name: "DNAT", + natTypes: []natType{dnatTarget}, + requestSrc: utils.Host2IPv4Addr.AddressWithPrefix.Address, + requestDst: utils.RouterNIC2IPv4Addr.AddressWithPrefix.Address, + expectedRequestSrc: utils.Host2IPv4Addr.AddressWithPrefix.Address, + expectedRequestDst: utils.Host1IPv4Addr.AddressWithPrefix.Address, + }, + { + name: "Twice-NAT", + natTypes: twiceNATTypes, + requestSrc: utils.Host2IPv4Addr.AddressWithPrefix.Address, + requestDst: utils.RouterNIC2IPv4Addr.AddressWithPrefix.Address, + expectedRequestSrc: utils.RouterNIC1IPv4Addr.AddressWithPrefix.Address, + expectedRequestDst: utils.Host1IPv4Addr.AddressWithPrefix.Address, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, natTypeTest := range test.natTypes { + t.Run(natTypeTest.name, func(t *testing.T) { + for _, natType := range natTypeTest.natTypes { + t.Run(natType.name, func(t *testing.T) { + s := stack.New(stack.Options{ + NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, + TransportProtocols: []stack.TransportProtocolFactory{icmp.NewProtocol4, icmp.NewProtocol6}, + }) + + ep1 := channel.New(1, header.IPv6MinimumMTU, "") + ep2 := channel.New(1, header.IPv6MinimumMTU, "") + utils.SetupRouterStack(t, s, ep1, ep2) + + natType.setupNAT(t, s, test.netProto, test.transProto, natTypeTest.expectedRequestSrc, natTypeTest.expectedRequestDst, 0 /* dnatPort */) + + // Send and check the Echo Request. + { + ep2.InjectInbound(test.netProto, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Data: test.echoPkt(natTypeTest.requestSrc, natTypeTest.requestDst, false /* reply */).ToVectorisedView(), + })) + pkt, ok := ep1.Read() + if !ok { + t.Fatal("expected to read a packet on ep1") + } + test.checkEchoPkt(t, stack.PayloadSince(pkt.Pkt.NetworkHeader()), natTypeTest.expectedRequestSrc, natTypeTest.expectedRequestDst, false /* reply */) + } + + if t.Failed() { + t.FailNow() + } + + // Send and check the Echo Reply. + { + ep1.InjectInbound(test.netProto, stack.NewPacketBuffer(stack.PacketBufferOptions{ + Data: test.echoPkt(natTypeTest.expectedRequestDst, natTypeTest.expectedRequestSrc, true /* reply */).ToVectorisedView(), + })) + pkt, ok := ep2.Read() + if !ok { + t.Fatal("expected to read a packet on ep2") + } + test.checkEchoPkt(t, stack.PayloadSince(pkt.Pkt.NetworkHeader()), natTypeTest.requestDst, natTypeTest.requestSrc, true /* reply */) + } + }) + } + }) + } + }) + } +} + func TestNAT(t *testing.T) { const listenPort uint16 = 8080 @@ -1856,6 +1993,23 @@ func tcpv4Packet(srcAddr, dstAddr tcpip.Address, srcPort, dstPort uint16, dataSi return hdr.View() } +func icmpv4Packet(srcAddr, dstAddr tcpip.Address, icmpType header.ICMPv4Type, ident uint16) buffer.View { + hdr := buffer.NewPrependable(header.IPv4MinimumSize + header.ICMPv4MinimumSize) + icmp := header.ICMPv4(hdr.Prepend(header.ICMPv4MinimumSize)) + icmp.SetType(icmpType) + icmp.SetIdent(ident) + icmp.SetChecksum(0) + icmp.SetChecksum(^header.Checksum(icmp, 0)) + encodeIPv4Header( + hdr.Prepend(header.IPv4MinimumSize), + hdr.UsedLength(), + header.ICMPv4ProtocolNumber, + srcAddr, + dstAddr, + ) + return hdr.View() +} + func udpv6Packet(srcAddr, dstAddr tcpip.Address, srcPort, dstPort uint16, dataSize int) buffer.View { udpSize := header.UDPMinimumSize + dataSize hdr := buffer.NewPrependable(header.IPv6MinimumSize + udpSize) @@ -2292,33 +2446,77 @@ func TestNATICMPError(t *testing.T) { } } -func TestSNATHandlePortConflicts(t *testing.T) { +func TestSNATHandlePortOrIdentConflicts(t *testing.T) { const dstPort = 5432 - type portRange struct { + type portOrIdentRange struct { first uint16 last uint16 } - type transportTypeTest struct { - name string - proto tcpip.TransportProtocolNumber - buf func(tcpip.Address, uint16) buffer.View - checkNATed func(*testing.T, buffer.View, uint16, bool, portRange) + type srcPortOrIdentRangeTest struct { + name string + originalRange portOrIdentRange + targetRange portOrIdentRange } - compareSrcPort := func(t *testing.T, gotPort uint16, originalSrcPort uint16, firstPacket bool, expectedRange portRange) { + srcPortRanges := []srcPortOrIdentRangeTest{ + { + name: "Less than 512", + originalRange: portOrIdentRange{first: 1, last: 511}, + targetRange: portOrIdentRange{first: 1, last: 511}, + }, + { + name: "Greater than or equal to 512 but less than 1024", + originalRange: portOrIdentRange{first: 512, last: 1023}, + targetRange: portOrIdentRange{first: 1, last: 1023}, + }, + { + name: "Greater than or equal to 1024", + originalRange: portOrIdentRange{first: 1024, last: math.MaxUint16}, + targetRange: portOrIdentRange{first: 1024, last: math.MaxUint16}, + }, + } + + // Unlike TCP/UDP, the Ident may be mapped to any 16-bit value. + identRanges := []srcPortOrIdentRangeTest{ + { + name: "Less than 512", + originalRange: portOrIdentRange{first: 0, last: 511}, + targetRange: portOrIdentRange{first: 0, last: math.MaxUint16}, + }, + { + name: "Greater than or equal to 512 but less than 1024", + originalRange: portOrIdentRange{first: 512, last: 1023}, + targetRange: portOrIdentRange{first: 0, last: math.MaxUint16}, + }, + { + name: "Greater than or equal to 1024", + originalRange: portOrIdentRange{first: 1024, last: math.MaxUint16}, + targetRange: portOrIdentRange{first: 0, last: math.MaxUint16}, + }, + } + + type transportTypeTest struct { + name string + proto tcpip.TransportProtocolNumber + buf func(tcpip.Address, uint16) buffer.View + checkNATed func(*testing.T, buffer.View, uint16, bool, portOrIdentRange) + srcPortOrIdentRanges []srcPortOrIdentRangeTest + } + + compareSrcPortOrIdent := func(t *testing.T, gotPort uint16, originalSrcPort uint16, firstPacket bool, expectedRange portOrIdentRange) { t.Helper() if firstPacket { if gotPort != originalSrcPort { - t.Errorf("got port = %d, want = %d", gotPort, originalSrcPort) + t.Errorf("got port/ident = %d, want = %d", gotPort, originalSrcPort) } return } if gotPort < expectedRange.first || gotPort > expectedRange.last { - t.Errorf("got port = %d, want in range [%d, %d]", gotPort, expectedRange.first, expectedRange.last) + t.Errorf("got port/ident = %d, want in range [%d, %d]", gotPort, expectedRange.first, expectedRange.last) } } @@ -2345,7 +2543,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { buf: func(srcAddr tcpip.Address, srcPort uint16) buffer.View { return udpv4Packet(srcAddr, utils.Host1IPv4Addr.AddressWithPrefix.Address, srcPort, dstPort, 0 /* dataSize */) }, - checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portRange) { + checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portOrIdentRange) { checker.IPv4(t, v, checker.SrcAddr(utils.RouterNIC1IPv4Addr.AddressWithPrefix.Address), checker.DstAddr(utils.Host1IPv4Addr.AddressWithPrefix.Address), @@ -2355,9 +2553,10 @@ func TestSNATHandlePortConflicts(t *testing.T) { ) if !t.Failed() { - compareSrcPort(t, header.UDP(header.IPv4(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) + compareSrcPortOrIdent(t, header.UDP(header.IPv4(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) } }, + srcPortOrIdentRanges: srcPortRanges, }, { name: "TCP", @@ -2365,7 +2564,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { buf: func(srcAddr tcpip.Address, srcPort uint16) buffer.View { return tcpv4Packet(srcAddr, utils.Host1IPv4Addr.AddressWithPrefix.Address, srcPort, dstPort, 0 /* dataSize */) }, - checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portRange) { + checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portOrIdentRange) { checker.IPv4(t, v, checker.SrcAddr(utils.RouterNIC1IPv4Addr.AddressWithPrefix.Address), checker.DstAddr(utils.Host1IPv4Addr.AddressWithPrefix.Address), @@ -2375,9 +2574,32 @@ func TestSNATHandlePortConflicts(t *testing.T) { ) if !t.Failed() { - compareSrcPort(t, header.TCP(header.IPv4(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) + compareSrcPortOrIdent(t, header.TCP(header.IPv4(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) } }, + srcPortOrIdentRanges: srcPortRanges, + }, + { + name: "ICMP Echo", + proto: header.ICMPv4ProtocolNumber, + buf: func(srcAddr tcpip.Address, ident uint16) buffer.View { + return icmpv4Packet(srcAddr, utils.Host1IPv4Addr.AddressWithPrefix.Address, header.ICMPv4Echo, ident) + }, + checkNATed: func(t *testing.T, v buffer.View, originalIdent uint16, firstPacket bool, expectedRange portOrIdentRange) { + checker.IPv4(t, v, + checker.SrcAddr(utils.RouterNIC1IPv4Addr.AddressWithPrefix.Address), + checker.DstAddr(utils.Host1IPv4Addr.AddressWithPrefix.Address), + checker.ICMPv4( + checker.ICMPv4Type(header.ICMPv4Echo), + checker.ICMPv4Checksum(), + ), + ) + + if !t.Failed() { + compareSrcPortOrIdent(t, header.ICMPv4(header.IPv4(v).Payload()).Ident(), originalIdent, firstPacket, expectedRange) + } + }, + srcPortOrIdentRanges: identRanges, }, }, }, @@ -2397,7 +2619,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { buf: func(srcAddr tcpip.Address, srcPort uint16) buffer.View { return udpv6Packet(srcAddr, utils.Host1IPv6Addr.AddressWithPrefix.Address, srcPort, dstPort, 0 /* dataSize */) }, - checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portRange) { + checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portOrIdentRange) { checker.IPv6(t, v, checker.SrcAddr(utils.RouterNIC1IPv6Addr.AddressWithPrefix.Address), checker.DstAddr(utils.Host1IPv6Addr.AddressWithPrefix.Address), @@ -2407,9 +2629,10 @@ func TestSNATHandlePortConflicts(t *testing.T) { ) if !t.Failed() { - compareSrcPort(t, header.UDP(header.IPv6(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) + compareSrcPortOrIdent(t, header.UDP(header.IPv6(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) } }, + srcPortOrIdentRanges: srcPortRanges, }, { name: "TCP", @@ -2417,7 +2640,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { buf: func(srcAddr tcpip.Address, srcPort uint16) buffer.View { return tcpv6Packet(srcAddr, utils.Host1IPv6Addr.AddressWithPrefix.Address, srcPort, dstPort, 0 /* dataSize */) }, - checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portRange) { + checkNATed: func(t *testing.T, v buffer.View, originalSrcPort uint16, firstPacket bool, expectedRange portOrIdentRange) { checker.IPv6(t, v, checker.SrcAddr(utils.RouterNIC1IPv6Addr.AddressWithPrefix.Address), checker.DstAddr(utils.Host1IPv6Addr.AddressWithPrefix.Address), @@ -2427,9 +2650,10 @@ func TestSNATHandlePortConflicts(t *testing.T) { ) if !t.Failed() { - compareSrcPort(t, header.TCP(header.IPv6(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) + compareSrcPortOrIdent(t, header.TCP(header.IPv6(v).Payload()).SourcePort(), originalSrcPort, firstPacket, expectedRange) } }, + srcPortOrIdentRanges: srcPortRanges, }, }, }, @@ -2453,38 +2677,16 @@ func TestSNATHandlePortConflicts(t *testing.T) { }, } - srcPortRanges := []struct { - name string - originalRange portRange - targetRange portRange - }{ - { - name: "Less than 512", - originalRange: portRange{first: 1, last: 511}, - targetRange: portRange{first: 1, last: 511}, - }, - { - name: "Greater than or equal to 512 but less than 1024", - originalRange: portRange{first: 512, last: 1023}, - targetRange: portRange{first: 1, last: 1023}, - }, - { - name: "Greater than or equal to 1024", - originalRange: portRange{first: 1024, last: math.MaxUint16}, - targetRange: portRange{first: 1024, last: math.MaxUint16}, - }, - } - for _, test := range tests { t.Run(test.name, func(t *testing.T) { for _, transportType := range test.transportTypes { t.Run(transportType.name, func(t *testing.T) { for _, natType := range natTypes { t.Run(natType.name, func(t *testing.T) { - for _, srcPortRange := range srcPortRanges { - t.Run(srcPortRange.name, func(t *testing.T) { - for _, srcPort := range [2]uint16{srcPortRange.originalRange.first, srcPortRange.originalRange.last} { - t.Run(fmt.Sprintf("OriginalSrcPort=%d", srcPort), func(t *testing.T) { + for _, srcPortOrIdentRange := range transportType.srcPortOrIdentRanges { + t.Run(srcPortOrIdentRange.name, func(t *testing.T) { + for _, srcPortOrIdent := range [2]uint16{srcPortOrIdentRange.originalRange.first, srcPortOrIdentRange.originalRange.last} { + t.Run(fmt.Sprintf("OriginalSrcPortOrIdent=%d", srcPortOrIdent), func(t *testing.T) { s := stack.New(stack.Options{ NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, TransportProtocols: []stack.TransportProtocolFactory{udp.NewProtocol, tcp.NewProtocol}, @@ -2548,7 +2750,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { for i, srcAddr := range test.srcAddrs { t.Run(fmt.Sprintf("Packet#%d", i), func(t *testing.T) { ep2.InjectInbound(test.netProto, stack.NewPacketBuffer(stack.PacketBufferOptions{ - Data: transportType.buf(srcAddr, srcPort).ToVectorisedView(), + Data: transportType.buf(srcAddr, srcPortOrIdent).ToVectorisedView(), })) pkt, ok := ep1.Read() @@ -2556,7 +2758,7 @@ func TestSNATHandlePortConflicts(t *testing.T) { t.Fatal("expected to read a packet on ep1") } pktView := stack.PayloadSince(pkt.Pkt.NetworkHeader()) - transportType.checkNATed(t, pktView, srcPort, i == 0, srcPortRange.targetRange) + transportType.checkNATed(t, pktView, srcPortOrIdent, i == 0, srcPortOrIdentRange.targetRange) }) } })