Skip to content

Commit 94256af

Browse files
authored
Add IPv6/dual-stack support (#350)
## Flags Changes This adds the proxy-init flag `--iptables-mode` (with possible values `legacy` and `nft`), which supersedes `--firewal-bin-path` and `firewall-save-bin-path` (which still remain supported). Also the `--ipv6` flag has been added (default `true`). After the set of rules run via iptables are processed, if `--ipv6` is true (which is the default), the same set of rules will be run via ip6tables. Analog changes were applied to linkerd-cni as well. ## Backwards-Compatibility This is backwards-compatible with older control planes and upcoming control planes. If `--ipv6` is not passed (and thus defaults to true), this doesn't impact operation even if the cluster doesn't support IPv6; the ip6tables rules are applied but they're innocuous. OTOH if there's no kernel support for IPv6 (which is the case for github runners*) then the ip6tables command will fail but we'll just log the failure and not fail the linkerd-init container (nor the `add` command for linkerd-cni). This avoids having to explicitly set `--ipv6=false`, but it can be set if the user is aware of such limitations and wants to get rid of the errors. ## Testing Improvements The cni-plugin-integration workflow has been simplified by using a matrix strategy, and enhanced by parameterizing the iptables-mode config. ## Linkerd IPv6 Support This allows routing IPv6 traffic to the proxy, but is just the first step towards IPv6/dual-stack support. Control plane and proxy changes will come up next. ## (*) Github Runners IPv6 Support Even though `modinfo` signals support for IPv6, `ip6tables` commands throw modprobe errors. Indeed, according to actions/runner-images#668 support is not there yet. Also, according to actions/runner#3138 there are issues with hosted runners as well, but that might not affect us if we still expose an IPv4 interface to interact with github. Something to take into account when we get to IPv6 integration testing.
1 parent 6fbbc4c commit 94256af

File tree

10 files changed

+157
-45
lines changed

10 files changed

+157
-45
lines changed

.github/workflows/cni-plugin-integration.yml

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,33 +12,20 @@ on:
1212
- justfile*
1313

1414
jobs:
15-
cni-flannel-test:
16-
continue-on-error: true
17-
timeout-minutes: 15
18-
runs-on: ubuntu-latest
19-
steps:
20-
- uses: linkerd/dev/actions/setup-tools@v43
21-
- uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633
22-
- name: Run CNI integration tests
23-
run: just cni-plugin-test-integration-flannel
24-
cni-calico-test:
25-
continue-on-error: true
26-
timeout-minutes: 15
27-
runs-on: ubuntu-latest
28-
steps:
29-
- uses: linkerd/dev/actions/setup-tools@v43
30-
- uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633
31-
- name: Run CNI integration tests
32-
run: just cni-plugin-test-integration-calico
33-
cni-cilium-test:
34-
continue-on-error: true
15+
cni-test:
16+
strategy:
17+
matrix:
18+
cni: [flannel, calico, cilium]
19+
iptables-mode: [legacy, nft]
3520
timeout-minutes: 15
3621
runs-on: ubuntu-latest
3722
steps:
3823
- uses: linkerd/dev/actions/setup-tools@v43
3924
- uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633
4025
- name: Run CNI integration tests
41-
run: just cni-plugin-test-integration-cilium
26+
env:
27+
IPTABLES_MODE: ${{ matrix.iptables-mode }}
28+
run: just cni-plugin-test-integration-${{ matrix.cni }}
4229
ordering-test:
4330
continue-on-error: true
4431
timeout-minutes: 15

cni-plugin/integration/manifests/calico/linkerd-cni.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ data:
8080
"ports-to-redirect": [],
8181
"inbound-ports-to-ignore": ["4191","4190"],
8282
"simulate": false,
83-
"use-wait-flag": false
83+
"use-wait-flag": false,
84+
"iptables-mode": "$IPTABLES_MODE",
85+
"ipv6": true
8486
}
8587
}
8688
---

cni-plugin/integration/manifests/cilium/linkerd-cni.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ data:
8080
"ports-to-redirect": [],
8181
"inbound-ports-to-ignore": ["4191","4190"],
8282
"simulate": false,
83-
"use-wait-flag": false
83+
"use-wait-flag": false,
84+
"iptables-mode": "$IPTABLES_MODE",
85+
"ipv6": true
8486
}
8587
}
8688
---

cni-plugin/integration/manifests/flannel/linkerd-cni.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ data:
8484
"ports-to-redirect": [],
8585
"inbound-ports-to-ignore": ["4191","4190"],
8686
"simulate": false,
87-
"use-wait-flag": false
87+
"use-wait-flag": false,
88+
"iptables-mode": "$IPTABLES_MODE",
89+
"ipv6": true
8890
}
8991
}
9092
---

cni-plugin/integration/run.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ set -euxo pipefail
44

55
cd "${BASH_SOURCE[0]%/*}"
66

7-
# Integration tests to run. Scenario is passed in as an environment variable.
8-
# Default is 'flannel'
97
SCENARIO=${CNI_TEST_SCENARIO:-flannel}
8+
IPTABLES_MODE=${IPTABLES_MODE:-legacy}
109

1110
# Run kubectl with the correct context.
1211
function k() {
@@ -25,7 +24,10 @@ function create_test_lab() {
2524
# can enable a testing matrix?
2625
# Apply all files in scenario directory. For non-flannel CNIs, this will
2726
# include the CNI manifest itself.
28-
k apply -f "manifests/$SCENARIO/"
27+
for f in ./manifests/"$SCENARIO"/*.yaml
28+
do
29+
envsubst < "$f" | k apply -f -
30+
done
2931
}
3032

3133
function cleanup() {

cni-plugin/integration/testutil/test_util.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ type ProxyInit struct {
4949
PortsToRedirect []int `json:"ports-to-redirect"`
5050
InboundPortsToIgnore []string `json:"inbound-ports-to-ignore"`
5151
OutboundPortsToIgnore []string `json:"outbound-ports-to-ignore"`
52+
SubnetsToIgnore []string `json:"subnets-to-ignore"`
5253
Simulate bool `json:"simulate"`
5354
UseWaitFlag bool `json:"use-wait-flag"`
55+
IPTablesMode string `json:"iptables-mode"`
56+
IPv6 bool `json:"ipv6"`
5457
}
5558

5659
// LinkerdPlugin is what we use for CNI configuration in the plugins section

cni-plugin/main.go

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ type ProxyInit struct {
5252
SubnetsToIgnore []string `json:"subnets-to-ignore"`
5353
Simulate bool `json:"simulate"`
5454
UseWaitFlag bool `json:"use-wait-flag"`
55+
IPTablesMode string `json:"iptables-mode"`
56+
IPv6 bool `json:"ipv6"`
5557
}
5658

5759
// Kubernetes a K8s specific struct to hold config
@@ -219,8 +221,8 @@ func cmdAdd(args *skel.CmdArgs) error {
219221
SimulateOnly: conf.ProxyInit.Simulate,
220222
NetNs: args.Netns,
221223
UseWaitFlag: conf.ProxyInit.UseWaitFlag,
222-
FirewallBinPath: "iptables-legacy",
223-
FirewallSaveBinPath: "iptables-legacy-save",
224+
IPTablesMode: conf.ProxyInit.IPTablesMode,
225+
IPv6: conf.ProxyInit.IPv6,
224226
}
225227

226228
// Check if there are any overridden ports to be skipped
@@ -292,17 +294,24 @@ func cmdAdd(args *skel.CmdArgs) error {
292294
options.OutboundPortsToIgnore = append(options.OutboundPortsToIgnore, skippedPorts...)
293295
}
294296

295-
firewallConfiguration, err := cmd.BuildFirewallConfiguration(&options)
296-
if err != nil {
297-
logEntry.Errorf("linkerd-cni: could not create a Firewall Configuration from the options: %v", options)
298-
return err
297+
// This ensures BC against linkerd2-cni older versions not yet passing this flag
298+
if options.IPTablesMode == "" {
299+
options.IPTablesMode = cmd.IPTablesModeLegacy
299300
}
300301

301-
err = iptables.ConfigureFirewall(*firewallConfiguration)
302-
if err != nil {
303-
logEntry.Errorf("linkerd-cni: could not configure firewall: %s", err)
302+
// always trigger the IPv4 rules
303+
optIPv4 := options
304+
optIPv4.IPv6 = false
305+
if err := buildAndConfigure(logEntry, &optIPv4); err != nil {
304306
return err
305307
}
308+
309+
// trigger the IPv6 rules
310+
if options.IPv6 {
311+
if err := buildAndConfigure(logEntry, &options); err != nil {
312+
return err
313+
}
314+
}
306315
} else {
307316
if containsInitContainer {
308317
logEntry.Debug("linkerd-cni: linkerd-init initContainer is present, skipping.")
@@ -353,6 +362,24 @@ func getAPIServerPorts(ctx context.Context, api *kubernetes.Clientset) ([]string
353362
return ports, nil
354363
}
355364

365+
func buildAndConfigure(logEntry *logrus.Entry, options *cmd.RootOptions) error {
366+
firewallConfiguration, err := cmd.BuildFirewallConfiguration(options)
367+
if err != nil {
368+
logEntry.Errorf("linkerd-cni: could not create a Firewall Configuration from the options: %v", options)
369+
return err
370+
}
371+
372+
err = iptables.ConfigureFirewall(*firewallConfiguration)
373+
// We couldn't find a robust way of checking IPv6 support besides trying to just call ip6tables-save.
374+
// If IPv4 rules worked but not IPv6, let's not fail the container (the actual problem will get logged).
375+
if !options.IPv6 && err != nil {
376+
logEntry.Errorf("linkerd-cni: could not configure firewall: %s", err)
377+
return err
378+
}
379+
380+
return nil
381+
}
382+
356383
func getAnnotationOverride(ctx context.Context, api *kubernetes.Clientset, pod *v1.Pod, key string) (string, error) {
357384
// Check if the annotation is present on the pod
358385
if override := pod.GetObjectMeta().GetAnnotations()[key]; override != "" {

justfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ _cni-plugin-setup-cilium:
224224
echo "Mounted /sys/fs/bpf to cilium-test-server cluster"
225225
helm repo add cilium https://helm.cilium.io/
226226
helm install cilium cilium/cilium --version 1.13.0 \
227+
--kube-context k3d-l5d-cilium-test \
227228
--namespace kube-system \
228229
--set kubeProxyReplacement=partial \
229230
--set hostServices.enabled=false \

proxy-init/cmd/root.go

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ import (
1313
"github.com/linkerd/linkerd2-proxy-init/internal/util"
1414
)
1515

16+
const (
17+
// IPTablesModeLegacy signals the usage of the iptables-legacy commands
18+
IPTablesModeLegacy = "legacy"
19+
// IPTablesModeNFT signals the usage of the iptables-nft commands
20+
IPTablesModeNFT = "nft"
21+
22+
cmdLegacy = "iptables-legacy"
23+
cmdLegacySave = "iptables-legacy-save"
24+
cmdLegacyIPv6 = "ip6tables-legacy"
25+
cmdLegacyIPv6Save = "ip6tables-legacy-save"
26+
cmdNFT = "iptables-nft"
27+
cmdNFTSave = "iptables-nft-save"
28+
cmdNFTIPv6 = "ip6tables-nft"
29+
cmdNFTIPv6Save = "ip6tables-nft-save"
30+
)
31+
1632
// RootOptions provides the information that will be used to build a firewall configuration.
1733
type RootOptions struct {
1834
IncomingProxyPort int
@@ -30,6 +46,8 @@ type RootOptions struct {
3046
LogLevel string
3147
FirewallBinPath string
3248
FirewallSaveBinPath string
49+
IPTablesMode string
50+
IPv6 bool
3351
}
3452

3553
func newRootOptions() *RootOptions {
@@ -47,8 +65,10 @@ func newRootOptions() *RootOptions {
4765
TimeoutCloseWaitSecs: 0,
4866
LogFormat: "plain",
4967
LogLevel: "info",
50-
FirewallBinPath: "iptables-legacy",
51-
FirewallSaveBinPath: "iptables-legacy-save",
68+
FirewallBinPath: "",
69+
FirewallSaveBinPath: "",
70+
IPTablesMode: "",
71+
IPv6: true,
5272
}
5373
}
5474

@@ -61,7 +81,7 @@ func NewRootCmd() *cobra.Command {
6181
Use: "proxy-init",
6282
Short: "proxy-init adds a Kubernetes pod to the Linkerd service mesh",
6383
Long: "proxy-init adds a Kubernetes pod to the Linkerd service mesh.",
64-
RunE: func(cmd *cobra.Command, args []string) error {
84+
RunE: func(_ *cobra.Command, _ []string) error {
6585

6686
if options.TimeoutCloseWaitSecs != 0 {
6787
sysctl := exec.Command("sysctl", "-w",
@@ -75,16 +95,39 @@ func NewRootCmd() *cobra.Command {
7595
log.Info(string(out))
7696
}
7797

78-
config, err := BuildFirewallConfiguration(options)
98+
log.SetFormatter(getFormatter(options.LogFormat))
99+
err := setLogLevel(options.LogLevel)
79100
if err != nil {
80101
return err
81102
}
82-
log.SetFormatter(getFormatter(options.LogFormat))
83-
err = setLogLevel(options.LogLevel)
103+
104+
// always trigger the IPv4 rules
105+
optIPv4 := *options
106+
optIPv4.IPv6 = false
107+
config, err := BuildFirewallConfiguration(&optIPv4)
84108
if err != nil {
85109
return err
86110
}
87-
return iptables.ConfigureFirewall(*config)
111+
112+
if err = iptables.ConfigureFirewall(*config); err != nil {
113+
return err
114+
}
115+
116+
if !options.IPv6 {
117+
return nil
118+
}
119+
120+
// trigger the IPv6 rules
121+
config, err = BuildFirewallConfiguration(options)
122+
if err != nil {
123+
return err
124+
}
125+
126+
// We couldn't find a robust way of checking IPv6 support besides trying to just call ip6tables-save.
127+
// If IPv4 rules worked but not IPv6, let's not fail the container (the actual problem will get logged).
128+
_ = iptables.ConfigureFirewall(*config)
129+
130+
return nil
88131
},
89132
}
90133

@@ -101,13 +144,32 @@ func NewRootCmd() *cobra.Command {
101144
cmd.PersistentFlags().IntVar(&options.TimeoutCloseWaitSecs, "timeout-close-wait-secs", options.TimeoutCloseWaitSecs, "Sets nf_conntrack_tcp_timeout_close_wait")
102145
cmd.PersistentFlags().StringVar(&options.LogFormat, "log-format", options.LogFormat, "Configure log format ('plain' or 'json')")
103146
cmd.PersistentFlags().StringVar(&options.LogLevel, "log-level", options.LogLevel, "Configure log level")
147+
cmd.PersistentFlags().StringVar(&options.IPTablesMode, "iptables-mode", options.IPTablesMode, "Variant of iptables command to use (\"legacy\" or \"nft\"); overrides --firewall-bin-path and --firewall-save-bin-path")
148+
cmd.PersistentFlags().BoolVar(&options.IPv6, "ipv6", options.IPv6, "Set rules both via iptables and ip6tables to support dual-stack networking")
149+
150+
// these two flags are kept for backwards-compatibility, but --iptables-mode is preferred
104151
cmd.PersistentFlags().StringVar(&options.FirewallBinPath, "firewall-bin-path", options.FirewallBinPath, "Path to iptables binary")
105152
cmd.PersistentFlags().StringVar(&options.FirewallSaveBinPath, "firewall-save-bin-path", options.FirewallSaveBinPath, "Path to iptables-save binary")
106153
return cmd
107154
}
108155

109156
// BuildFirewallConfiguration returns an iptables FirewallConfiguration suitable to use to configure iptables.
110157
func BuildFirewallConfiguration(options *RootOptions) (*iptables.FirewallConfiguration, error) {
158+
if options.IPTablesMode != "" && options.IPTablesMode != IPTablesModeLegacy && options.IPTablesMode != IPTablesModeNFT {
159+
return nil, fmt.Errorf("--iptables-mode valid values are only \"%s\" and \"%s\"", IPTablesModeLegacy, IPTablesModeNFT)
160+
}
161+
162+
if options.IPTablesMode == "" {
163+
switch options.FirewallBinPath {
164+
case "", cmdLegacy:
165+
options.IPTablesMode = IPTablesModeLegacy
166+
case cmdNFT:
167+
options.IPTablesMode = IPTablesModeNFT
168+
default:
169+
return nil, fmt.Errorf("--firewall-bin-path valid values are only \"%s\" and \"%s\"", cmdLegacy, cmdNFT)
170+
}
171+
}
172+
111173
if !util.IsValidPort(options.IncomingProxyPort) {
112174
return nil, fmt.Errorf("--incoming-proxy-port must be a valid TCP port number")
113175
}
@@ -116,6 +178,8 @@ func BuildFirewallConfiguration(options *RootOptions) (*iptables.FirewallConfigu
116178
return nil, fmt.Errorf("--outgoing-proxy-port must be a valid TCP port number")
117179
}
118180

181+
cmd, cmdSave := getCommands(options)
182+
119183
sanitizedSubnets := []string{}
120184
for _, subnet := range options.SubnetsToIgnore {
121185
subnet := strings.TrimSpace(subnet)
@@ -138,8 +202,8 @@ func BuildFirewallConfiguration(options *RootOptions) (*iptables.FirewallConfigu
138202
SimulateOnly: options.SimulateOnly,
139203
NetNs: options.NetNs,
140204
UseWaitFlag: options.UseWaitFlag,
141-
BinPath: options.FirewallBinPath,
142-
SaveBinPath: options.FirewallSaveBinPath,
205+
BinPath: cmd,
206+
SaveBinPath: cmdSave,
143207
}
144208

145209
if len(options.PortsToRedirect) > 0 {
@@ -160,6 +224,21 @@ func getFormatter(format string) log.Formatter {
160224
}
161225
}
162226

227+
func getCommands(options *RootOptions) (string, string) {
228+
if options.IPTablesMode == IPTablesModeLegacy {
229+
if options.IPv6 {
230+
return cmdLegacyIPv6, cmdLegacyIPv6Save
231+
}
232+
return cmdLegacy, cmdLegacySave
233+
}
234+
235+
if options.IPv6 {
236+
return cmdNFTIPv6, cmdNFTIPv6Save
237+
}
238+
239+
return cmdNFT, cmdNFTSave
240+
}
241+
163242
func setLogLevel(logLevel string) error {
164243
level, err := log.ParseLevel(logLevel)
165244
if err != nil {

0 commit comments

Comments
 (0)