forked from projectcalico/felix
-
Notifications
You must be signed in to change notification settings - Fork 0
/
floating_ip_mgr.go
167 lines (153 loc) · 6.49 KB
/
floating_ip_mgr.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Copyright (c) 2017 Tigera, Inc. All rights reserved.
//
// 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 intdataplane
import (
"reflect"
log "github.com/sirupsen/logrus"
"github.com/projectcalico/felix/iptables"
"github.com/projectcalico/felix/proto"
"github.com/projectcalico/felix/rules"
)
// A floating IP is an IP that can be used to reach a particular workload endpoint, but that the
// endpoint itself is not aware of. The 'floating IP' terminology comes from OpenStack, but the
// concept can be useful with workload orchestration platforms more generally. OpenStack
// installations use floating IPs for two reasons: (1) IP mobility, aka as a 'service IP' - i.e. to
// have an IP that can be moved from time to time to target a different workload, for example,
// depending on which of those workloads is currently the 'active' one for some HA service; (2) to
// allow a particular workload to be reachable (inbound) from the Internet, when the networking
// driver normally doesn't allow an Internet-routable IP to be assigned as a workload's primary IP.
// In Calico (2) is irrelevant, because a workload can have an Internet-routable IP as its primary
// IP, but the idea of service IP mobility is still useful, and floating IPs are one way of
// providing mobile service IPs.
//
// Implementation-wise, a floating IP is simply a destination IP address - the 'external' IP - that
// gets DNAT'd by the compute node to the workload's normal IP address - the 'internal' IP - on
// packets that are sent to the workload.
//
// There generally _isn't_ a corresponding SNAT for the other direction; in other words, packets
// sent _from_ a workload don't have their source address changing to a floating IP, if the workload
// has floating IPs associated with it. This is because people don't expect floating IPs to do
// that, and its faster in the datapath to avoid NATs if we can, especially when sending to another
// workload nearby. (And for sending outbound to the Internet, there will be an SNAT if needed at
// the data center's border gateway.) However SNAT is needed in the loopback case where a workload
// sends to a floating IP that maps back to itself; for this case the datapath processing must be as
// follows:
//
// 1. Workload sends to <floating IP>, so packet has SRC=<workload IP> DST=<floating IP>.
//
// 2. Compute node does DNAT for the floating IP, so now packet has SRC=<workload IP> DST=<workload IP>.
//
// 3. Compute node does SNAT, so that packet has SRC=<floating IP> DST=<workload IP>.
//
// 4. Workload receives packet again, with SRC=<floating IP> DST=<workload IP>.
//
// If (3) was omitted, the workload would receive a packet from a non-loopback interface with
// SRC=<my own IP> DST=<my own IP>, and so would probably drop it.
// floatingIPManager programs the 'cali-fip-dnat' and 'cali-fip-snat' chains in the iptables 'nat'
// table with DNAT and SNAT rules for the floating IPs associated with local workload endpoints.
// The cali-fip-dnat chain is statically linked from cali-OUTPUT and cali-PREROUTING, and
// cali-fip-snat from cali-POSTROUTING.
type floatingIPManager struct {
ipVersion uint8
// Our dependencies.
natTable iptablesTable
ruleRenderer rules.RuleRenderer
// Internal state.
activeDNATChains []*iptables.Chain
activeSNATChains []*iptables.Chain
natInfo map[proto.WorkloadEndpointID][]*proto.NatInfo
dirtyNATInfo bool
}
func newFloatingIPManager(
natTable iptablesTable,
ruleRenderer rules.RuleRenderer,
ipVersion uint8,
) *floatingIPManager {
return &floatingIPManager{
natTable: natTable,
ruleRenderer: ruleRenderer,
ipVersion: ipVersion,
activeDNATChains: []*iptables.Chain{},
activeSNATChains: []*iptables.Chain{},
natInfo: map[proto.WorkloadEndpointID][]*proto.NatInfo{},
dirtyNATInfo: true,
}
}
func (m *floatingIPManager) OnUpdate(protoBufMsg interface{}) {
switch msg := protoBufMsg.(type) {
case *proto.WorkloadEndpointUpdate:
if m.ipVersion == 4 {
m.natInfo[*msg.Id] = msg.Endpoint.Ipv4Nat
} else {
m.natInfo[*msg.Id] = msg.Endpoint.Ipv6Nat
}
m.dirtyNATInfo = true
case *proto.WorkloadEndpointRemove:
delete(m.natInfo, *msg.Id)
m.dirtyNATInfo = true
}
}
func (m *floatingIPManager) CompleteDeferredWork() error {
if m.dirtyNATInfo {
// Collate required DNATs as a map from external IP to internal IP.
dnats := map[string]string{}
for _, natInfos := range m.natInfo {
for _, natInfo := range natInfos {
log.WithFields(log.Fields{
"ExtIP": natInfo.ExtIp,
"IntIP": natInfo.IntIp,
}).Debug("NAT mapping")
// We shouldn't ever have the same floating IP mapping to multiple
// workload IPs, but if we do we'll program the mapping to the
// alphabetically earlier one.
existingIntIP := dnats[natInfo.ExtIp]
if existingIntIP == "" || natInfo.IntIp < existingIntIP {
log.Debug("Wanted NAT mapping")
dnats[natInfo.ExtIp] = natInfo.IntIp
}
}
}
// Collate required SNATs as a map from internal IP to external IP.
snats := map[string]string{}
for extIP, intIP := range dnats {
log.WithFields(log.Fields{
"ExtIP": extIP,
"IntIP": intIP,
}).Debug("Reverse mapping")
// For the reverse SNATs, if multiple floating IPs map to the same workload
// IP, use the alphabetically earliest floating IP.
existingExtIP := snats[intIP]
if existingExtIP == "" || extIP < existingExtIP {
log.Debug("Wanted reverse mapping")
snats[intIP] = extIP
}
}
// Render chains for those NATs.
dnatChains := m.ruleRenderer.DNATsToIptablesChains(dnats)
snatChains := m.ruleRenderer.SNATsToIptablesChains(snats)
// Update iptables if they have changed.
if !reflect.DeepEqual(m.activeDNATChains, dnatChains) {
m.natTable.RemoveChains(m.activeDNATChains)
m.natTable.UpdateChains(dnatChains)
m.activeDNATChains = dnatChains
}
if !reflect.DeepEqual(m.activeSNATChains, snatChains) {
m.natTable.RemoveChains(m.activeSNATChains)
m.natTable.UpdateChains(snatChains)
m.activeSNATChains = snatChains
}
m.dirtyNATInfo = false
}
return nil
}