// Copyright 2020 The gVisor Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stack import ( "fmt" "math" "github.com/sagernet/gvisor/pkg/log" "github.com/sagernet/gvisor/pkg/tcpip" "github.com/sagernet/gvisor/pkg/tcpip/header" ) // NATType represents the type of NAT. type NATType int const ( // SNAT is source NAT. SNAT NATType = iota // DNAT is destination NAT. DNAT // NATUnknown is unknown NAT type. NATUnknown ) // ToNATType converts a uint8 to a NATType. func ToNATType(t uint8) NATType { switch t { case 0: return SNAT case 1: return DNAT } return NATUnknown } func (natType NATType) String() string { switch natType { case SNAT: return "SNAT" case DNAT: return "DNAT" default: return "NATUnknown" } } // NfNATPriority returns the priority of the NAT hook. // Check `ipv4/ipv6_nat_ops` in nf_nat_proto.c. func NfNATPriority(hook NFHook) (int, bool) { switch hook { case NFPrerouting: // NF_IP_PRI_NAT_DST return -100, true case NFPostrouting: // NF_IP_PRI_NAT_SRC return 100, true case NFOutput: // NF_IP_PRI_NAT_DST return -100, true case NFInput: // NF_IP_PRI_NAT_SRC return 100, true } // NAT is not supported for other hooks. return 0, false } // NfHookToNATType returns the applicable NAT type // for the given netfilter hook. func NfHookToNATType(hook NFHook) NATType { switch hook { case NFPrerouting, NFOutput: return DNAT case NFInput, NFPostrouting: return SNAT } return NATUnknown } // handlePacketOpts contains the options for handlePacket. type handlePacketOpts struct { fullChecksum bool updatePseudoHeader bool natType NATType } // handlePacket attempts to handle a packet and perform NAT if the connection // has had NAT performed on it. // // Returns true if the packet can skip the NAT table. func handlePacket(pkt *PacketBuffer, opts *handlePacketOpts) bool { if opts == nil || opts.natType == NATUnknown { return false } netHdr, transHdr, isICMPError, ok := pkt.GetHeaders() if !ok { return false } natDone := &pkt.snatDone dnat := false if opts.natType == DNAT { natDone = &pkt.dnatDone dnat = true } if *natDone { panic(fmt.Sprintf("packet already had NAT: %s performed; pkt=%#v", opts.natType, pkt)) } // TODO(gvisor.dev/issue/5748): TCP checksums on inbound packets should be // validated if checksum offloading is off. It may require IP defrag if the // packets are fragmented. reply := pkt.tuple.reply cn := pkt.tuple.conn tid, manip := func() (tupleID, manipType) { cn.mu.RLock() defer cn.mu.RUnlock() if reply { tid := cn.original.tupleID if dnat { return tid, cn.sourceManip } return tid, cn.destinationManip } tid := cn.reply.tupleID if dnat { return tid, cn.destinationManip } return tid, cn.sourceManip }() switch manip { case manipNotPerformed: return false case manipPerformedNoop: *natDone = true return true case manipPerformed: default: panic(fmt.Sprintf("unhandled manip = %d", manip)) } newPort := tid.dstPortOrEchoReplyIdent newAddr := tid.dstAddr if dnat { newPort = tid.srcPortOrEchoRequestIdent newAddr = tid.srcAddr } UpdateHeaders( netHdr, transHdr, !dnat != isICMPError, opts.fullChecksum, opts.updatePseudoHeader, newPort, newAddr, ) *natDone = true if !isICMPError { return true } // We performed NAT on (erroneous) packet that triggered an ICMP response, but // not the ICMP packet itself. switch pkt.TransportProtocolNumber { case header.ICMPv4ProtocolNumber: icmp := header.ICMPv4(pkt.TransportHeader().Slice()) // TODO(https://gvisor.dev/issue/6788): Incrementally update ICMP checksum. icmp.SetChecksum(0) icmp.SetChecksum(header.ICMPv4Checksum(icmp, pkt.Data().Checksum())) network := header.IPv4(pkt.NetworkHeader().Slice()) if dnat { network.SetDestinationAddressWithChecksumUpdate(tid.srcAddr) } else { network.SetSourceAddressWithChecksumUpdate(tid.dstAddr) } case header.ICMPv6ProtocolNumber: network := header.IPv6(pkt.NetworkHeader().Slice()) srcAddr := network.SourceAddress() dstAddr := network.DestinationAddress() if dnat { dstAddr = tid.srcAddr } else { srcAddr = tid.dstAddr } icmp := header.ICMPv6(pkt.TransportHeader().Slice()) // TODO(https://gvisor.dev/issue/6788): Incrementally update ICMP checksum. icmp.SetChecksum(0) payload := pkt.Data() icmp.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{ Header: icmp, Src: srcAddr, Dst: dstAddr, PayloadCsum: payload.Checksum(), PayloadLen: payload.Size(), })) if dnat { network.SetDestinationAddress(dstAddr) } else { network.SetSourceAddress(srcAddr) } } return true } // IPTHandlePacket handles and applies NAT to the packet if required. func IPTHandlePacket(pkt *PacketBuffer, hook Hook, r *Route) bool { opts := handlePacketOpts{ fullChecksum: false, updatePseudoHeader: false, natType: SNAT, } requiresTXTransportChecksum := false if r != nil { requiresTXTransportChecksum = r.RequiresTXTransportChecksum() } switch hook { case Prerouting: opts.fullChecksum = true opts.updatePseudoHeader = true opts.natType = DNAT case Input: case Forward: panic("should not handle packet in the forwarding hook") case Output: opts.natType = DNAT fallthrough case Postrouting: if pkt.TransportProtocolNumber == header.TCPProtocolNumber && pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum { opts.updatePseudoHeader = true } else if requiresTXTransportChecksum { opts.fullChecksum = true opts.updatePseudoHeader = true } default: panic(fmt.Sprintf("unrecognized hook = %d", hook)) } return handlePacket(pkt, &opts) } // PortOrIdentRange represents a range of ports or idents // range to use for NAT. type PortOrIdentRange struct { Start uint16 Size uint32 } // ConfigureNAT setups up the connection for the specified NAT and rewrites the // packet. // // If NAT has already been performed on the connection, then the packet will // be rewritten with the NAT performed on the connection, ignoring the passed // address and port range. // // Generally, only the first packet of a connection reaches this method; other // packets will be manipulated without needing to modify the connection. // // Returns whether the NAT was configured or not. func (cn *conn) ConfigureNAT(portsOrIdents PortOrIdentRange, natAddress tcpip.Address, natType NATType, changePort, changeAddress bool) bool { lastPortOrIdentU32 := uint32(portsOrIdents.Start) + portsOrIdents.Size - 1 if lastPortOrIdentU32 > math.MaxUint16 { log.Warningf("got lastPortOrIdent = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", lastPortOrIdentU32, math.MaxUint16, portsOrIdents) return false } lastPortOrIdent := uint16(lastPortOrIdentU32) cn.mu.Lock() defer cn.mu.Unlock() var manip *manipType var address *tcpip.Address var portOrIdent *uint16 if natType == DNAT { manip = &cn.destinationManip address = &cn.reply.tupleID.srcAddr portOrIdent = &cn.reply.tupleID.srcPortOrEchoRequestIdent } else { manip = &cn.sourceManip address = &cn.reply.tupleID.dstAddr portOrIdent = &cn.reply.tupleID.dstPortOrEchoReplyIdent } if *manip != manipNotPerformed { return true } *manip = manipPerformed if changeAddress { *address = natAddress } // Everything below here is port-fiddling. if !changePort { return true } // Does the current port/ident fit in the range? if portsOrIdents.Start <= *portOrIdent && *portOrIdent <= lastPortOrIdent { // Yes, is the current reply tuple unique? // // Or, does the reply tuple refer to the same connection as the current one that // we are NATing? This would apply, for example, to a self-connected socket, // where the original and reply tuples are identical. other := cn.ct.connForTID(cn.reply.tupleID) if other == nil || other.conn == cn { // Yes! No need to change the port. return true } } // 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 uint32 = 128 const minAttemptsToContinue = 16 allowedInitialAttempts := maxAttemptsForInitialRound if allowedInitialAttempts > portsOrIdents.Size { allowedInitialAttempts = portsOrIdents.Size } for maxAttempts := allowedInitialAttempts; ; maxAttempts /= 2 { // Start reach round with a random initial port/ident offset. randOffset := cn.ct.rng.Uint32() for i := uint32(0); i < maxAttempts; i++ { newPortOrIdentU32 := uint32(portsOrIdents.Start) + (randOffset+i)%portsOrIdents.Size if newPortOrIdentU32 > math.MaxUint16 { log.Warningf("got newPortOrIdentU32 = %d, want <= MaxUint16(=%d); portsOrIdents=%#v", newPortOrIdentU32, math.MaxUint16, portsOrIdents) continue } *portOrIdent = uint16(newPortOrIdentU32) if other := cn.ct.connForTID(cn.reply.tupleID); other == nil { // We found a unique tuple! return true } } if maxAttempts == portsOrIdents.Size { // We already tried all the ports/idents in the range so no need to keep // trying. return false } if maxAttempts < minAttemptsToContinue { return false } } // We did not find a unique tuple, use the last used port anyways. // TODO(https://gvisor.dev/issue/6850): Handle not finding a unique tuple // better (e.g. remove the connection and drop the packet). } // IPTPerformNAT performs NAT on the packet and updates the connection. // Used by IPTables. func IPTPerformNAT(pkt *PacketBuffer, hook Hook, r *Route, portsOrIdents PortOrIdentRange, natAddress tcpip.Address, dnat, changePort, changeAddress bool) { // 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 // connection is already NATed, but if we reach this point we must be in the // NAT table, so the return value is useless for us. _ = IPTHandlePacket(pkt, hook, r) }() cn := pkt.tuple.conn natType := SNAT if dnat { natType = DNAT } _ = cn.ConfigureNAT(portsOrIdents, natAddress, natType, changePort, changeAddress) } // IPTMaybePerformNoopNAT can apply NAT or configure a no-op NAT. // If NAT has not been configured for this connection, either mark the // connection as configured for "no-op NAT", in the case of DNAT, or, in the // case of SNAT, perform source port remapping so that source ports used by // locally-generated traffic do not conflict with ports occupied by existing NAT // bindings. // // Note that in the typical case this is also a no-op, because `snatAction` // will do nothing if the original tuple is already unique. func IPTMaybePerformNoopNAT(pkt *PacketBuffer, hook Hook, r *Route, dnat bool) { cn := pkt.tuple.conn cn.mu.Lock() var manip *manipType if dnat { manip = &cn.destinationManip } else { manip = &cn.sourceManip } if *manip != manipNotPerformed { cn.mu.Unlock() _ = IPTHandlePacket(pkt, hook, r) return } if dnat { *manip = manipPerformedNoop cn.mu.Unlock() _ = IPTHandlePacket(pkt, hook, r) return } cn.mu.Unlock() // At this point, we know that NAT has not yet been performed on this // connection, and the DNAT case has been handled with a no-op. For SNAT, we // simply perform source port remapping to ensure that source ports for // locally generated traffic do not clash with ports used by existing NAT // bindings. _, _ = snatAction(pkt, hook, r, 0, tcpip.Address{}, true /* changePort */, false /* changeAddress */) } // NFTApplyNAT applies NAT to the packet and updates the connection. // Similar to IPTHandlePacket but for NFTables hooks. func NFTApplyNAT(pkt *PacketBuffer, hook NFHook, rt *Route) bool { requiresTXTransportChecksum := false if rt != nil { requiresTXTransportChecksum = rt.RequiresTXTransportChecksum() } opts := handlePacketOpts{ fullChecksum: false, updatePseudoHeader: false, natType: SNAT, } switch hook { case NFPrerouting: opts.fullChecksum = true opts.updatePseudoHeader = true opts.natType = DNAT case NFInput: case NFForward: panic("should not handle packet in the forwarding hook") case NFOutput: opts.natType = DNAT fallthrough case NFPostrouting: if pkt.TransportProtocolNumber == header.TCPProtocolNumber && pkt.GSOOptions.Type != GSONone && pkt.GSOOptions.NeedsCsum { opts.updatePseudoHeader = true } else if requiresTXTransportChecksum { opts.fullChecksum = true opts.updatePseudoHeader = true } default: panic(fmt.Sprintf("unrecognized hook = %d", hook)) } return handlePacket(pkt, &opts) } // IsNATConfigured returns whether NAT has been configured for the given NAT type. func (cn *conn) IsNATConfigured(natType NATType) bool { cn.mu.RLock() defer cn.mu.RUnlock() switch natType { case SNAT: return cn.sourceManip != manipNotPerformed case DNAT: return cn.destinationManip != manipNotPerformed } return false } // ConfigureNoopNAT configures the connection for no-op NAT. // Similar to the func `IPTMaybePerformNoopNAT` except that this one only configures NO-OP NAT and is independent of IPTables. func (cn *conn) ConfigureNoopNAT(pkt *PacketBuffer, natType NATType) bool { cn.mu.Lock() var manip *manipType if natType == DNAT { manip = &cn.destinationManip } else { manip = &cn.sourceManip } if *manip != manipNotPerformed { cn.mu.Unlock() return true } if natType == DNAT { *manip = manipPerformedNoop cn.mu.Unlock() return true } cn.mu.Unlock() // At this point, we know that NAT has not yet been performed on this // connection, and the DNAT case has been handled with a no-op. For SNAT, we // simply perform source port remapping to ensure that source ports for // locally generated traffic do not clash with ports used by existing NAT // bindings. portsOrIdents := PortOrIdentRange{Start: 0, Size: math.MaxUint16 + 1} // However, we need to extract the port from packet. var port uint16 switch pkt.TransportProtocolNumber { case header.UDPProtocolNumber: port = header.UDP(pkt.TransportHeader().Slice()).SourcePort() case header.TCPProtocolNumber: port = header.TCP(pkt.TransportHeader().Slice()).SourcePort() } if port != 0 { portsOrIdents = targetPortRangeForTCPAndUDP(port) } return cn.ConfigureNAT(portsOrIdents, tcpip.Address{}, natType, true /* changePort */, false /* changeAddress */) } // ConfigureMasquerade configures the connection for masquerade. func (cn *conn) configureMasquerade(pkt *PacketBuffer, route *Route, stk *Stack, ports PortOrIdentRange, changePort bool) bool { srcAddr := pkt.Network().SourceAddress() if srcAddr == header.IPv4Any || srcAddr == header.IPv6Any { return false } // Masquerade is only supported for postrouting. if route == nil { return false } // Get the network endpoint for the outgoing interface to find its primary address. netEP, err := stk.GetNetworkEndpoint(route.NICID(), route.NetProto()) if err != nil { return false } addressEP, ok := netEP.(AddressableEndpoint) if !ok { return false } // Ref: net/netfilter/nf_nat_masquerade.c:nf_nat_masquerade_ipv[4|6]() // Use the next hop address as the destination address if it is set. nh := route.NextHop() if nh.Len() == 0 { nh = pkt.Network().DestinationAddress() } // addressEP is expected to be set for the postrouting hook. // Find the outgoing primary address for the destination address. ep := addressEP.AcquireOutgoingPrimaryAddress(nh, tcpip.Address{} /* srcHint */, false /* allowExpired */) if ep == nil { // No address exists that we can use as a source address. return false } address := ep.AddressWithPrefix().Address ep.DecRef() // Configure NAT for the packet to change the source address. return cn.ConfigureNAT(ports, address, SNAT, changePort, true /* changeAddress */) }