Skip to content

Commit d22b65e

Browse files
committed
Better NAT mapping
Deal separately with SRC vs DST mapping. Includes detailed rationale.
1 parent ceafa6c commit d22b65e

File tree

2 files changed

+109
-44
lines changed

2 files changed

+109
-44
lines changed

probe/endpoint/nat.go

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,45 +30,102 @@ func makeNATMapper(fw flowWalker) natMapper {
3030
return natMapper{fw}
3131
}
3232

33-
func toMapping(f conntrack.Conn) *endpointMapping {
34-
var mapping endpointMapping
35-
if f.Orig.Src.Equal(f.Reply.Dst) {
36-
mapping = endpointMapping{
37-
originalIP: f.Reply.Src,
38-
originalPort: f.Reply.SrcPort,
39-
rewrittenIP: f.Orig.Dst,
40-
rewrittenPort: f.Orig.DstPort,
41-
}
42-
} else {
43-
mapping = endpointMapping{
44-
originalIP: f.Orig.Src,
45-
originalPort: f.Orig.SrcPort,
46-
rewrittenIP: f.Reply.Dst,
47-
rewrittenPort: f.Reply.DstPort,
48-
}
49-
}
50-
51-
return &mapping
33+
func endpointNodeID(scope string, ip net.IP, port uint16) string {
34+
return report.MakeEndpointNodeID(scope, "", ip.String(), strconv.Itoa(int(port)))
5235
}
5336

54-
// applyNAT duplicates Nodes in the endpoint topology of a report, based on
37+
/*
38+
39+
Some examples of connections with NAT:
40+
41+
Pod to pod via Kubernetes service
42+
picked up by ebpf as 10.32.0.16:47600->10.105.173.176:5432 and 10.32.0.6:5432 (??)
43+
NAT IPS_DST_NAT orig: 10.32.0.16:47600->10.105.173.176:5432, reply: 10.32.0.6:5432->10.32.0.16:47600
44+
We want: 10.32.0.16:47600->10.32.0.6:5432
45+
- replace the destination (== NAT orig dst) with the NAT reply source
46+
47+
Incoming from outside the cluster to a NodePort:
48+
picked up by ebpf as 10.32.0.1:13488->10.32.0.7:80
49+
NAT: IPS_SRC_NAT IPS_DST_NAT orig: 37.157.33.76:13488->172.31.2.17:30081, reply: 10.32.0.7:80->10.32.0.1:13488
50+
We want: 37.157.33.76:13488->10.32.0.7:80
51+
- replace the source (== NAT reply dst) with the NAT original source
52+
53+
Outgoing from a pod:
54+
picked up by ebpf as 10.32.0.7:36078->18.221.99.178:443
55+
NAT: IPS_SRC_NAT orig: 10.32.0.7:36078->18.221.99.178:443, reply: 18.221.99.178:443->172.31.2.17:36078
56+
We want: 10.32.0.7:36078->18.221.99.178:443
57+
- leave it alone.
58+
59+
Docker container exposing port to similar on different host
60+
host1:
61+
picked up by ebpf as ip-172-31-5-80;172.17.0.2:43042->172.31.2.17:8080
62+
NAT: IPS_SRC_NAT orig: 172.17.0.2:43042->172.31.2.17:8080, reply: 172.31.2.17:8080-> 172.31.5.80:43042
63+
applying standard rule: ip-172-31-5-80;172.17.0.2:43042->172.31.2.17:8080 (i.e. no change)
64+
we could add 172.31.5.80:43042 (nat reply destination) as a copy of ip-172-31-5-80;172.17.0.2:43042 (nat orig source)
65+
host2:
66+
picked up by ebpf as 172.31.5.80:43042->ip-172-31-2-17;172.17.0.2:80
67+
NAT: IPS_DST_NAT orig: 172.31.5.80:43042->172.31.2.17:8080, reply: 172.17.0.2:80->172.31.5.80:43042
68+
Ideally we might want: ip-172-31-5-80;172.17.0.2:43042->ip-172-31-2-17;172.17.0.2:80
69+
applying standard rule: 172.31.5.80:43042->ip-172-31-2-17;172.17.0.2:80 (i.e. no change)
70+
we could add 172.31.2.17:8080 (nat original destination) as a copy of ip-172-31-2-17;172.17.0.2:80 (nat reply source)
71+
72+
All of the above can be satisfied by these rules:
73+
For SRC_NAT either add NAT orig source as a copy of NAT reply destination
74+
or add NAT reply destination as a copy of NAT original source
75+
For DST_NAT replace the destination in adjacencies with the NAT reply source
76+
and add nat original destination as a copy of nat reply source
77+
*/
78+
79+
// applyNAT modifies Nodes in the endpoint topology of a report, based on
5580
// the NAT table.
5681
func (n natMapper) applyNAT(rpt report.Report, scope string) {
5782
n.flowWalker.walkFlows(func(f conntrack.Conn, _ bool) {
58-
mapping := toMapping(f)
83+
replyDstID := endpointNodeID(scope, f.Reply.Dst, f.Reply.DstPort)
84+
origSrcID := endpointNodeID(scope, f.Orig.Src, f.Orig.SrcPort)
5985

60-
realEndpointPort := strconv.Itoa(int(mapping.originalPort))
61-
copyEndpointPort := strconv.Itoa(int(mapping.rewrittenPort))
62-
realEndpointID := report.MakeEndpointNodeID(scope, "", mapping.originalIP.String(), realEndpointPort)
63-
copyEndpointID := report.MakeEndpointNodeID(scope, "", mapping.rewrittenIP.String(), copyEndpointPort)
64-
65-
node, ok := rpt.Endpoint.Nodes[realEndpointID]
66-
if !ok {
67-
return
86+
if (f.Status & conntrack.IPS_SRC_NAT) != 0 {
87+
if replyDstID != origSrcID {
88+
// either add NAT orig source as a copy of NAT reply destination
89+
if replyDstNode, ok := rpt.Endpoint.Nodes[replyDstID]; ok {
90+
newNode := replyDstNode.WithID(origSrcID).WithLatests(map[string]string{
91+
CopyOf: replyDstID,
92+
})
93+
rpt.Endpoint.AddNode(newNode)
94+
} else if origSrcNode, ok := rpt.Endpoint.Nodes[origSrcID]; ok {
95+
// or add NAT reply destination as a copy of NAT original source
96+
newNode := origSrcNode.WithID(replyDstID).WithLatests(map[string]string{
97+
CopyOf: origSrcID,
98+
})
99+
rpt.Endpoint.AddNode(newNode)
100+
}
101+
}
68102
}
69103

70-
rpt.Endpoint.AddNode(node.WithID(copyEndpointID).WithLatests(map[string]string{
71-
CopyOf: realEndpointID,
72-
}))
104+
if (f.Status & conntrack.IPS_DST_NAT) != 0 {
105+
fromID := endpointNodeID(scope, f.Reply.Dst, f.Reply.DstPort)
106+
fromNode, ok := rpt.Endpoint.Nodes[fromID]
107+
if !ok {
108+
return
109+
}
110+
toID := endpointNodeID(scope, f.Orig.Dst, f.Orig.DstPort)
111+
112+
// replace destination with reply source
113+
replySrcID := endpointNodeID(scope, f.Reply.Src, f.Reply.SrcPort)
114+
if replySrcID != toID {
115+
fromNode.Adjacency = fromNode.Adjacency.Minus(toID)
116+
fromNode = fromNode.WithAdjacent(replySrcID)
117+
118+
// add nat original destination as a copy of nat reply source
119+
replySrcNode, ok := rpt.Endpoint.Nodes[replySrcID]
120+
if !ok {
121+
replySrcNode = report.MakeNode(replySrcID)
122+
}
123+
newNode := replySrcNode.WithID(toID).WithLatests(map[string]string{
124+
CopyOf: replySrcID,
125+
})
126+
rpt.Endpoint.AddNode(newNode)
127+
}
128+
129+
}
73130
})
74131
}

probe/endpoint/nat_internal_test.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,19 @@ func TestNat(t *testing.T) {
4747
{
4848
f := conntrack.Conn{
4949
MsgType: conntrack.NfctMsgUpdate,
50+
Status: conntrack.IPS_DST_NAT,
5051
Orig: conntrack.Tuple{
5152
Src: host2,
5253
Dst: host1,
53-
SrcPort: 22222,
54+
SrcPort: 22223,
5455
DstPort: 80,
5556
Proto: syscall.IPPROTO_TCP,
5657
},
5758
Reply: conntrack.Tuple{
5859
Src: c1,
5960
Dst: host2,
6061
SrcPort: 80,
61-
DstPort: 22222,
62+
DstPort: 22223,
6263
Proto: syscall.IPPROTO_TCP,
6364
},
6465
CtId: 1,
@@ -70,15 +71,18 @@ func TestNat(t *testing.T) {
7071

7172
have := report.MakeReport()
7273
originalID := report.MakeEndpointNodeID("host1", "", "10.0.47.1", "80")
73-
have.Endpoint.AddNode(report.MakeNodeWith(originalID, map[string]string{
74+
originalNode := report.MakeNodeWith(originalID, map[string]string{
7475
"foo": "bar",
75-
}))
76+
})
77+
have.Endpoint.AddNode(originalNode)
78+
fromID := report.MakeEndpointNodeID("host2", "", "2.3.4.5", "22223")
79+
have.Endpoint.AddNode(report.MakeNodeWith(fromID, nil).WithAdjacent(originalID))
7680

7781
want := have.Copy()
78-
wantID := report.MakeEndpointNodeID("host1", "", "1.2.3.4", "80")
79-
want.Endpoint.AddNode(report.MakeNodeWith(wantID, map[string]string{
82+
// add nat original destination as a copy of nat reply source
83+
origDstID := report.MakeEndpointNodeID("host1", "", "1.2.3.4", "80")
84+
want.Endpoint.AddNode(originalNode.WithID(origDstID).WithLatests(map[string]string{
8085
CopyOf: originalID,
81-
"foo": "bar",
8286
}))
8387

8488
makeNATMapper(ct).applyNAT(have, "host1")
@@ -91,6 +95,7 @@ func TestNat(t *testing.T) {
9195
{
9296
f := conntrack.Conn{
9397
MsgType: conntrack.NfctMsgUpdate,
98+
Status: conntrack.IPS_SRC_NAT,
9499
Orig: conntrack.Tuple{
95100
Src: c2,
96101
Dst: host1,
@@ -112,16 +117,19 @@ func TestNat(t *testing.T) {
112117
}
113118

114119
have := report.MakeReport()
115-
originalID := report.MakeEndpointNodeID("host2", "", "10.0.47.2", "22222")
116-
have.Endpoint.AddNode(report.MakeNodeWith(originalID, map[string]string{
120+
fromID := report.MakeEndpointNodeID("host2", "", "10.0.47.2", "22222")
121+
toID := report.MakeEndpointNodeID("host1", "", "1.2.3.4", "80")
122+
have.Endpoint.AddNode(report.MakeNodeWith(toID, nil))
123+
have.Endpoint.AddNode(report.MakeNodeWith(fromID, map[string]string{
117124
"foo": "baz",
118-
}))
125+
}).WithAdjacent(toID))
119126

127+
// add NAT reply destination as a copy of NAT original source
120128
want := have.Copy()
121129
want.Endpoint.AddNode(report.MakeNodeWith(report.MakeEndpointNodeID("host2", "", "2.3.4.5", "22223"), map[string]string{
122-
CopyOf: originalID,
130+
CopyOf: report.MakeEndpointNodeID("host1", "", "10.0.47.2", "22222"),
123131
"foo": "baz",
124-
}))
132+
}).WithAdjacent(toID))
125133

126134
makeNATMapper(ct).applyNAT(have, "host1")
127135
if !reflect.DeepEqual(want, have) {

0 commit comments

Comments
 (0)