Merge pull request #2895 from kevinGC:outbound-conntrack

PiperOrigin-RevId: 322803359
This commit is contained in:
gVisor bot
2020-07-23 09:38:51 -07:00
5 changed files with 189 additions and 29 deletions
+109 -27
View File
@@ -49,7 +49,8 @@ const (
type manipType int
const (
manipDstPrerouting manipType = iota
manipNone manipType = iota
manipDstPrerouting
manipDstOutput
)
@@ -113,13 +114,11 @@ type conn struct {
// update the state of tcb. It is immutable.
tcbHook Hook
// mu protects tcb.
// mu protects all mutable state.
mu sync.Mutex `state:"nosave"`
// tcb is TCB control block. It is used to keep track of states
// of tcp connection and is protected by mu.
tcb tcpconntrack.TCB
// lastUsed is the last time the connection saw a relevant packet, and
// is updated by each packet on the connection. It is protected by mu.
lastUsed time.Time `state:".(unixTime)"`
@@ -141,8 +140,26 @@ func (cn *conn) timedOut(now time.Time) bool {
return now.Sub(cn.lastUsed) > defaultTimeout
}
// update the connection tracking state.
//
// Precondition: ct.mu must be held.
func (ct *conn) updateLocked(tcpHeader header.TCP, hook Hook) {
// Update the state of tcb. tcb assumes it's always initialized on the
// client. However, we only need to know whether the connection is
// established or not, so the client/server distinction isn't important.
// TODO(gvisor.dev/issue/170): Add support in tcpconntrack to handle
// other tcp states.
if ct.tcb.IsEmpty() {
ct.tcb.Init(tcpHeader)
} else if hook == ct.tcbHook {
ct.tcb.UpdateStateOutbound(tcpHeader)
} else {
ct.tcb.UpdateStateInbound(tcpHeader)
}
}
// ConnTrack tracks all connections created for NAT rules. Most users are
// expected to only call handlePacket and createConnFor.
// expected to only call handlePacket, insertRedirectConn, and maybeInsertNoop.
//
// ConnTrack keeps all connections in a slice of buckets, each of which holds a
// linked list of tuples. This gives us some desirable properties:
@@ -248,8 +265,7 @@ func (ct *ConnTrack) connFor(pkt *PacketBuffer) (*conn, direction) {
return nil, dirOriginal
}
// createConnFor creates a new conn for pkt.
func (ct *ConnTrack) createConnFor(pkt *PacketBuffer, hook Hook, rt RedirectTarget) *conn {
func (ct *ConnTrack) insertRedirectConn(pkt *PacketBuffer, hook Hook, rt RedirectTarget) *conn {
tid, err := packetToTupleID(pkt)
if err != nil {
return nil
@@ -272,10 +288,15 @@ func (ct *ConnTrack) createConnFor(pkt *PacketBuffer, hook Hook, rt RedirectTarg
manip = manipDstOutput
}
conn := newConn(tid, replyTID, manip, hook)
ct.insertConn(conn)
return conn
}
// insertConn inserts conn into the appropriate table bucket.
func (ct *ConnTrack) insertConn(conn *conn) {
// Lock the buckets in the correct order.
tupleBucket := ct.bucket(tid)
replyBucket := ct.bucket(replyTID)
tupleBucket := ct.bucket(conn.original.tupleID)
replyBucket := ct.bucket(conn.reply.tupleID)
ct.mu.RLock()
defer ct.mu.RUnlock()
if tupleBucket < replyBucket {
@@ -289,22 +310,37 @@ func (ct *ConnTrack) createConnFor(pkt *PacketBuffer, hook Hook, rt RedirectTarg
ct.buckets[tupleBucket].mu.Lock()
}
// Add the tuple to the map.
ct.buckets[tupleBucket].tuples.PushFront(&conn.original)
ct.buckets[replyBucket].tuples.PushFront(&conn.reply)
// Now that we hold the locks, ensure the tuple hasn't been inserted by
// another thread.
alreadyInserted := false
for other := ct.buckets[tupleBucket].tuples.Front(); other != nil; other = other.Next() {
if other.tupleID == conn.original.tupleID {
alreadyInserted = true
break
}
}
if !alreadyInserted {
// Add the tuple to the map.
ct.buckets[tupleBucket].tuples.PushFront(&conn.original)
ct.buckets[replyBucket].tuples.PushFront(&conn.reply)
}
// Unlocking can happen in any order.
ct.buckets[tupleBucket].mu.Unlock()
if tupleBucket != replyBucket {
ct.buckets[replyBucket].mu.Unlock()
}
return conn
}
// handlePacketPrerouting manipulates ports for packets in Prerouting hook.
// TODO(gvisor.dev/issue/170): Change address for Prerouting hook.
func handlePacketPrerouting(pkt *PacketBuffer, conn *conn, dir direction) {
// If this is a noop entry, don't do anything.
if conn.manip == manipNone {
return
}
netHeader := header.IPv4(pkt.NetworkHeader)
tcpHeader := header.TCP(pkt.TransportHeader)
@@ -322,12 +358,22 @@ func handlePacketPrerouting(pkt *PacketBuffer, conn *conn, dir direction) {
netHeader.SetSourceAddress(conn.original.dstAddr)
}
// TODO(gvisor.dev/issue/170): TCP checksums aren't usually validated
// on inbound packets, so we don't recalculate them. However, we should
// support cases when they are validated, e.g. when we can't offload
// receive checksumming.
netHeader.SetChecksum(0)
netHeader.SetChecksum(^netHeader.CalculateChecksum())
}
// handlePacketOutput manipulates ports for packets in Output hook.
func handlePacketOutput(pkt *PacketBuffer, conn *conn, gso *GSO, r *Route, dir direction) {
// If this is a noop entry, don't do anything.
if conn.manip == manipNone {
return
}
netHeader := header.IPv4(pkt.NetworkHeader)
tcpHeader := header.TCP(pkt.TransportHeader)
@@ -362,20 +408,31 @@ func handlePacketOutput(pkt *PacketBuffer, conn *conn, gso *GSO, r *Route, dir d
}
// handlePacket will manipulate the port and address of the packet if the
// connection exists.
func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, gso *GSO, r *Route) {
// connection exists. Returns whether, after the packet traverses the tables,
// it should create a new entry in the table.
func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, gso *GSO, r *Route) bool {
if pkt.NatDone {
return
return false
}
if hook != Prerouting && hook != Output {
return
return false
}
// TODO(gvisor.dev/issue/170): Support other transport protocols.
if pkt.NetworkHeader == nil || header.IPv4(pkt.NetworkHeader).TransportProtocol() != header.TCPProtocolNumber {
return false
}
conn, dir := ct.connFor(pkt)
// Connection or Rule not found for the packet.
if conn == nil {
// Connection not found for the packet or the packet is invalid.
return
return true
}
tcpHeader := header.TCP(pkt.TransportHeader)
if tcpHeader == nil {
return false
}
switch hook {
@@ -395,14 +452,39 @@ func (ct *ConnTrack) handlePacket(pkt *PacketBuffer, hook Hook, gso *GSO, r *Rou
// Mark the connection as having been used recently so it isn't reaped.
conn.lastUsed = time.Now()
// Update connection state.
if tcpHeader := header.TCP(pkt.TransportHeader); conn.tcb.IsEmpty() {
conn.tcb.Init(tcpHeader)
conn.tcbHook = hook
} else if hook == conn.tcbHook {
conn.tcb.UpdateStateOutbound(tcpHeader)
} else {
conn.tcb.UpdateStateInbound(tcpHeader)
conn.updateLocked(header.TCP(pkt.TransportHeader), hook)
return false
}
// maybeInsertNoop tries to insert a no-op connection entry to keep connections
// from getting clobbered when replies arrive. It only inserts if there isn't
// already a connection for pkt.
//
// This should be called after traversing iptables rules only, to ensure that
// pkt.NatDone is set correctly.
func (ct *ConnTrack) maybeInsertNoop(pkt *PacketBuffer, hook Hook) {
// If there were a rule applying to this packet, it would be marked
// with NatDone.
if pkt.NatDone {
return
}
// We only track TCP connections.
if pkt.NetworkHeader == nil || header.IPv4(pkt.NetworkHeader).TransportProtocol() != header.TCPProtocolNumber {
return
}
// This is the first packet we're seeing for the TCP connection. Insert
// the noop entry (an identity mapping) so that the response doesn't
// get NATed, breaking the connection.
tid, err := packetToTupleID(pkt)
if err != nil {
return
}
conn := newConn(tid, tid.reply(), manipNone, hook)
conn.updateLocked(header.TCP(pkt.TransportHeader), hook)
ct.insertConn(conn)
}
// bucket gets the conntrack bucket for a tupleID.
+20 -1
View File
@@ -245,13 +245,18 @@ func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, addr
// Packets are manipulated only if connection and matching
// NAT rule exists.
it.connections.handlePacket(pkt, hook, gso, r)
shouldTrack := it.connections.handlePacket(pkt, hook, gso, r)
// Go through each table containing the hook.
it.mu.RLock()
defer it.mu.RUnlock()
priorities := it.priorities[hook]
for _, tableID := range priorities {
// If handlePacket already NATed the packet, we don't need to
// check the NAT table.
if tableID == natID && pkt.NatDone {
continue
}
table := it.tables[tableID]
ruleIdx := table.BuiltinChains[hook]
switch verdict := it.checkChain(hook, pkt, table, ruleIdx, gso, r, address, nicName); verdict {
@@ -281,6 +286,20 @@ func (it *IPTables) Check(hook Hook, pkt *PacketBuffer, gso *GSO, r *Route, addr
}
}
// If this connection should be tracked, try to add an entry for it. If
// traversing the nat table didn't end in adding an entry,
// maybeInsertNoop will add a no-op entry for the connection. This is
// needeed when establishing connections so that the SYN/ACK reply to an
// outgoing SYN is delivered to the correct endpoint rather than being
// redirected by a prerouting rule.
//
// From the iptables documentation: "If there is no rule, a `null'
// binding is created: this usually does not map the packet, but exists
// to ensure we don't map another stream over an existing one."
if shouldTrack {
it.connections.maybeInsertNoop(pkt, hook)
}
// Every table returned Accept.
return true
}
+1 -1
View File
@@ -153,7 +153,7 @@ func (rt RedirectTarget) Action(pkt *PacketBuffer, ct *ConnTrack, hook Hook, gso
// Set up conection for matching NAT rule. Only the first
// packet of the connection comes here. Other packets will be
// manipulated in connection tracking.
if conn := ct.createConnFor(pkt, hook, rt); conn != nil {
if conn := ct.insertRedirectConn(pkt, hook, rt); conn != nil {
ct.handlePacket(pkt, hook, gso, r)
}
default:
+7
View File
@@ -263,6 +263,13 @@ func TestNATPreRedirectTCPPort(t *testing.T) {
singleTest(t, NATPreRedirectTCPPort{})
}
func TestNATPreRedirectTCPOutgoing(t *testing.T) {
singleTest(t, NATPreRedirectTCPOutgoing{})
}
func TestNATOutRedirectTCPIncoming(t *testing.T) {
singleTest(t, NATOutRedirectTCPIncoming{})
}
func TestNATOutRedirectUDPPort(t *testing.T) {
singleTest(t, NATOutRedirectUDPPort{})
}
+52
View File
@@ -28,6 +28,8 @@ const (
func init() {
RegisterTestCase(NATPreRedirectUDPPort{})
RegisterTestCase(NATPreRedirectTCPPort{})
RegisterTestCase(NATPreRedirectTCPOutgoing{})
RegisterTestCase(NATOutRedirectTCPIncoming{})
RegisterTestCase(NATOutRedirectUDPPort{})
RegisterTestCase(NATOutRedirectTCPPort{})
RegisterTestCase(NATDropUDP{})
@@ -91,6 +93,56 @@ func (NATPreRedirectTCPPort) LocalAction(ip net.IP) error {
return connectTCP(ip, dropPort, sendloopDuration)
}
// NATPreRedirectTCPOutgoing verifies that outgoing TCP connections aren't
// affected by PREROUTING connection tracking.
type NATPreRedirectTCPOutgoing struct{}
// Name implements TestCase.Name.
func (NATPreRedirectTCPOutgoing) Name() string {
return "NATPreRedirectTCPOutgoing"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATPreRedirectTCPOutgoing) ContainerAction(ip net.IP) error {
// Redirect all incoming TCP traffic to a closed port.
if err := natTable("-A", "PREROUTING", "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", dropPort)); err != nil {
return err
}
// Establish a connection to the host process.
return connectTCP(ip, acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATPreRedirectTCPOutgoing) LocalAction(ip net.IP) error {
return listenTCP(acceptPort, sendloopDuration)
}
// NATOutRedirectTCPIncoming verifies that incoming TCP connections aren't
// affected by OUTPUT connection tracking.
type NATOutRedirectTCPIncoming struct{}
// Name implements TestCase.Name.
func (NATOutRedirectTCPIncoming) Name() string {
return "NATOutRedirectTCPIncoming"
}
// ContainerAction implements TestCase.ContainerAction.
func (NATOutRedirectTCPIncoming) ContainerAction(ip net.IP) error {
// Redirect all outgoing TCP traffic to a closed port.
if err := natTable("-A", "OUTPUT", "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", dropPort)); err != nil {
return err
}
// Establish a connection to the host process.
return listenTCP(acceptPort, sendloopDuration)
}
// LocalAction implements TestCase.LocalAction.
func (NATOutRedirectTCPIncoming) LocalAction(ip net.IP) error {
return connectTCP(ip, acceptPort, sendloopDuration)
}
// NATOutRedirectUDPPort tests that packets are redirected to different port.
type NATOutRedirectUDPPort struct{}