// Copyright (c) 2020-2025 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 fv_test import ( "bufio" "context" "errors" "fmt" "io" "maps" "os" "os/exec" "regexp" "strconv" "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/types" api "github.com/projectcalico/api/pkg/apis/projectcalico/v3" "github.com/projectcalico/api/pkg/lib/numorstring" log "github.com/sirupsen/logrus" "github.com/projectcalico/calico/felix/fv/connectivity" "github.com/projectcalico/calico/felix/fv/containers" "github.com/projectcalico/calico/felix/fv/infrastructure" "github.com/projectcalico/calico/felix/fv/tcpdump" "github.com/projectcalico/calico/felix/fv/utils" "github.com/projectcalico/calico/felix/fv/workload" "github.com/projectcalico/calico/libcalico-go/lib/apiconfig" "github.com/projectcalico/calico/libcalico-go/lib/apis/internalapi" "github.com/projectcalico/calico/libcalico-go/lib/clientv3" "github.com/projectcalico/calico/libcalico-go/lib/net" "github.com/projectcalico/calico/libcalico-go/lib/options" ) const ( wireguardInterfaceNameDefault = "wireguard.cali" wireguardInterfaceNameV6Default = "wg-v6.cali" wireguardMTUDefault = 1440 // Wireguard needs an overhead of 60 bytes for IPv4. wireguardMTUV6Default = 1420 // Wireguard needs an overhead of 80 bytes for IPv6. wireguardRoutingRulePriorityDefault = "99" wireguardListeningPortDefault = 51820 wireguardListeningPortV6Default = 51821 defaultWorkloadPort = "8055" fakeWireguardPubKeyV4 = "jlkVyQYooZYzI2wFfNhSZez5eWh44yfq1wKVjLvSXgY=" fakeWireguardPubKeyV6 = "wTRvGspB+MEP36UJ69K5krtFndRYKJZn3YfKOCf59Es=" ) var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ WireGuard-Supported", []apiconfig.DatastoreType{apiconfig.EtcdV3, apiconfig.Kubernetes}, func(getInfra infrastructure.InfraFactory) { const nodeCount = 2 var ( infra infrastructure.DatastoreInfra topologyContainers infrastructure.TopologyContainers client clientv3.Interface tcpdumps []*tcpdump.TCPDump wlsV4 [nodeCount]*workload.Workload // simulated host workloads wlsV6 [nodeCount]*workload.Workload // simulated host workloads cc *connectivity.Checker routeEntriesV4 [nodeCount]string routeEntriesV6 [nodeCount]string dmesgCmd *exec.Cmd dmesgKill func() wgBootstrapEvents chan struct{} ) type testConf struct { WireguardEnabledV4 bool WireguardEnabledV6 bool } for _, testConfig := range []testConf{ {true, false}, {false, true}, {true, true}, } { wireguardEnabledV4 := testConfig.WireguardEnabledV4 wireguardEnabledV6 := testConfig.WireguardEnabledV6 JustBeforeEach(func() { if BPFMode() { ensureAllNodesBPFProgramsAttached(topologyContainers.Felixes) } }) Describe(fmt.Sprintf("wireguardEnabledV4: %v, wireguardEnabledV6: %v", wireguardEnabledV4, wireguardEnabledV6), func() { BeforeEach(func() { // Run these tests only when the Host has Wireguard kernel module installed. if os.Getenv("FELIX_FV_WIREGUARD_AVAILABLE") != "true" { Skip("Skipping Wireguard supported tests.") } // Enable Wireguard module debugging. utils.Run("sudo", "sh", "-c", "echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control") // Start a process tailing the dmesg log. ctx, cancel := context.WithCancel(context.Background()) dmesgCmd = exec.CommandContext(ctx, "sudo", "dmesg", "-WH") dmesgCmd.WaitDelay = time.Second dmesgIn, err := dmesgCmd.StdinPipe() Expect(err).NotTo(HaveOccurred()) dmesgOut, err := dmesgCmd.StdoutPipe() Expect(err).NotTo(HaveOccurred()) dmesgErr, err := dmesgCmd.StderrPipe() Expect(err).NotTo(HaveOccurred()) err = dmesgCmd.Start() Expect(err).NotTo(HaveOccurred()) copyOutputToLog := func(name string, pipe io.ReadCloser) { scanner := bufio.NewScanner(pipe) scanner.Buffer(nil, 10*1024*1024) // Increase maximum buffer size (but don't pre-alloc). for scanner.Scan() { line := scanner.Text() line = strings.TrimRight(line, " \n") _, _ = fmt.Fprintf(GinkgoWriter, "dmesg[%v] %v\n", name, line) } err := scanner.Err() if err != nil && !errors.Is(err, io.EOF) { log.WithError(err).Errorf("Error reading %v", name) } } go copyOutputToLog("out", dmesgOut) go copyOutputToLog("err", dmesgErr) dmesgKill = cancel // close stdin to make sure sudo fails fast if it's asking for // password or something. Expect(dmesgIn.Close()).NotTo(HaveOccurred()) log.Info("Started dmesg log capture") infra = getInfra() ipipEnabled := !BPFMode() || !wireguardEnabledV6 topologyOptions := wireguardTopologyOptions( "CalicoIPAM", ipipEnabled, wireguardEnabledV4, wireguardEnabledV6, false, map[string]string{ "FELIX_DebugDisableLogDropping": "true", "FELIX_DBG_WGBOOTSTRAP": "true", }, ) topologyContainers, client = infrastructure.StartNNodeTopology(nodeCount, topologyOptions, infra) // To allow all ingress and egress, in absence of any Policy. infra.AddDefaultAllow() if wireguardEnabledV4 { for i := range wlsV4 { wlsV4[i] = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, fmt.Sprintf("10.65.%d.2", i), fmt.Sprintf("wlv4-%d", i), topologyContainers.Felixes[i]) // Prepare route entry. routeEntriesV4[i] = fmt.Sprintf("10.65.%d.0/26 dev %s scope link", i, wireguardInterfaceNameDefault) } // Swap route entry to match between workloads. routeEntriesV4[0], routeEntriesV4[1] = routeEntriesV4[1], routeEntriesV4[0] } if wireguardEnabledV6 { for i := range wlsV6 { wlsV6[i] = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, fmt.Sprintf("dead:beef::0:%d:2", i), fmt.Sprintf("wlv6-%d", i), topologyContainers.Felixes[i]) // Prepare route entry. if i == 0 { routeEntriesV6[i] = fmt.Sprintf("dead:beef::/122 dev %s metric 1024 pref medium", wireguardInterfaceNameV6Default) } else { routeEntriesV6[i] = fmt.Sprintf("dead:beef::%d:0/122 dev %s metric 1024 pref medium", i, wireguardInterfaceNameV6Default) } } // Swap route entry to match between workloads. routeEntriesV6[0], routeEntriesV6[1] = routeEntriesV6[1], routeEntriesV6[0] } for i := range nodeCount { wgBootstrapEvents = topologyContainers.Felixes[i].WatchStdoutFor( regexp.MustCompile(".*(Cleared wireguard public key from datastore|Wireguard public key not set in datastore).+"), ) topologyContainers.Felixes[i].TriggerDelayedStart() } cc = &connectivity.Checker{ // If two nodes send their first packet within a few milliseconds then any on-demand Wireguard // handshake can fail and back off if the handshakes cross on the wire. StaggerStartBy: 100 * time.Millisecond, } // Reset the set of tcp dumps between runs. tcpdumps = nil }) AfterEach(func() { if dmesgKill != nil { log.Info("Stop dmesg log capture") dmesgKill() _ = dmesgCmd.Wait() dmesgKill = nil } for _, tcpdump := range tcpdumps { tcpdump.Stop() } }) Context("with Wireguard enabled", func() { BeforeEach(func() { // Check the Wireguard device exists. Eventually(func() error { for i, felix := range topologyContainers.Felixes { out, err := felix.ExecOutput("ip", "link", "show") if err != nil { return err } if wireguardEnabledV4 { if !strings.Contains(out, wireguardInterfaceNameDefault) { return fmt.Errorf("felix %d has no IPv4 Wireguard device", i) } } if wireguardEnabledV6 { if !strings.Contains(out, wireguardInterfaceNameV6Default) { return fmt.Errorf("felix %d has no IPv6 Wireguard device", i) } } } return nil }, "30s", "330ms").ShouldNot(HaveOccurred()) }) It("should have called bootstrap", func() { Eventually(wgBootstrapEvents, "5s", "100ms").Should(BeClosed()) }) It("the Wireguard routing rule should exist", func() { for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { return getWireguardRoutingRule(felix, 4) }, "5s", "100ms").Should(MatchRegexp("\\d+:\\s+not from all fwmark 0x\\d+/0x\\d+ lookup \\d+")) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRoutingRule(felix, 6) }, "5s", "100ms").Should(MatchRegexp("\\d+:\\s+not from all fwmark 0x\\d+/0x\\d+ lookup \\d+")) } } }) It("the Wireguard route-table entry should exist", func() { for i, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { return getWireguardRouteEntry(felix, 4) }, "5s", "100ms").Should(ContainSubstring(routeEntriesV4[i])) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRouteEntry(felix, 6) }, "5s", "100ms").Should(ContainSubstring(routeEntriesV6[i])) } } }) It("the Wireguard device should be configurable", func() { disableWireguard(client) // Old configuration should disappear. Eventually(func() error { for i, felix := range topologyContainers.Felixes { out, err := felix.ExecOutput("ip", "-d", "link", "show") if err != nil { return err } if wireguardEnabledV4 { if strings.Contains(out, wireguardInterfaceNameDefault) { return fmt.Errorf("felix %d still has IPv4 Wireguard device", i) } } if wireguardEnabledV6 { if strings.Contains(out, wireguardInterfaceNameV6Default) { return fmt.Errorf("felix %d still has IPv6 Wireguard device", i) } } } return nil }, "30s", "330ms").ShouldNot(HaveOccurred()) for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { out, err := felix.ExecOutput("ip", "rule", "show", "pref", wireguardRoutingRulePriorityDefault) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").Should(BeEmpty()) } if wireguardEnabledV6 { Eventually(func() string { out, err := felix.ExecOutput("ip", "-6", "rule", "show", "pref", wireguardRoutingRulePriorityDefault) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").Should(BeEmpty()) } } // Change Wireguard configuration. ifaceName := "wg0" ifaceNameV6 := "wg1" mtu := 1400 mtuV6 := 1380 rule := 100 port := 28150 portV6 := 28151 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() fc, err := client.FelixConfigurations().Get(ctx, "default", options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) fc.Spec.WireguardInterfaceName = ifaceName fc.Spec.WireguardInterfaceNameV6 = ifaceNameV6 fc.Spec.WireguardMTU = &mtu fc.Spec.WireguardMTUV6 = &mtuV6 fc.Spec.WireguardListeningPort = &port fc.Spec.WireguardListeningPortV6 = &portV6 fc.Spec.WireguardRoutingRulePriority = &rule _, err = client.FelixConfigurations().Update(ctx, fc, options.SetOptions{}) Expect(err).NotTo(HaveOccurred()) updateWireguardEnabledConfig(client, wireguardEnabledV4, wireguardEnabledV6, false) // New Wireguard device should appear with default MTU, etc. for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { out, _ := felix.ExecOutput("ip", "-d", "link", "show", ifaceName) return out }, "30s", "330ms").Should(ContainSubstring(fmt.Sprintf("mtu %d", mtu))) } if wireguardEnabledV6 { Eventually(func() string { out, _ := felix.ExecOutput("ip", "-d", "link", "show", ifaceNameV6) return out }, "30s", "330ms").Should(ContainSubstring(fmt.Sprintf("mtu %d", mtuV6))) } } // Expect the settings to be changed on the device. for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { out, err := felix.ExecOutput("wg", "show", ifaceName) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").Should(ContainSubstring(fmt.Sprintf("listening port: %d", port))) Eventually(func() string { out, err := felix.ExecOutput("ip", "rule", "show", "pref", fmt.Sprintf("%d", rule)) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").ShouldNot(BeEmpty()) } if wireguardEnabledV6 { Eventually(func() string { out, err := felix.ExecOutput("wg", "show", ifaceNameV6) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").Should(ContainSubstring(fmt.Sprintf("listening port: %d", portV6))) Eventually(func() string { out, err := felix.ExecOutput("ip", "-6", "rule", "show", "pref", fmt.Sprintf("%d", rule)) Expect(err).NotTo(HaveOccurred()) return out }, "30s", "330ms").ShouldNot(BeEmpty()) } } }) It("the Wireguard device should have napi threading set correctly", func() { // transitions wireguard napi threading on then off wireguardThreadingStates := []string{"0", "1", "0"} for _, state := range wireguardThreadingStates { stateBool, err := strconv.ParseBool(state) Expect(err).NotTo(HaveOccurred()) updateWireguardEnabledConfig(client, wireguardEnabledV4, wireguardEnabledV6, stateBool) for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { s, _ := felix.ExecCombinedOutput("cat", fmt.Sprintf("/sys/class/net/%s/threaded", wireguardInterfaceNameDefault)) return s }, "60s", "5s").Should(ContainSubstring(state)) } if wireguardEnabledV6 { Eventually(func() string { s, _ := felix.ExecCombinedOutput("cat", fmt.Sprintf("/sys/class/net/%s/threaded", wireguardInterfaceNameV6Default)) return s }, "60s", "5s").Should(ContainSubstring(state)) } } } }) It("v3 node resource annotations should contain public-keys", func() { for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { node, err := client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) return node.Status.WireguardPublicKey }, "5s", "100ms").ShouldNot(BeEmpty()) } if wireguardEnabledV6 { Eventually(func() string { node, err := client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) return node.Status.WireguardPublicKeyV6 }, "5s", "100ms").ShouldNot(BeEmpty()) } } }) It("v3 node resource annotations should automatically heal", func() { for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { var wgPubKeyOrig string var node *internalapi.Node var err error // Get the original public-key. Eventually(func() error { node, err = client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) if err != nil { return err } if node.Status.WireguardPublicKey == "" { return errors.New("node.Status.WireguardPublicKey not set yet") } else if wgPubKeyOrig == "" { // Seeing the original public key for the first time. wgPubKeyOrig = node.Status.WireguardPublicKey } // overwrite public-key by fake but valid Wireguard key. node.Status.WireguardPublicKey = fakeWireguardPubKeyV4 _, err = client.Nodes().Update(context.Background(), node, options.SetOptions{}) if err != nil { return err } return nil }, "5s", "300ms").ShouldNot(HaveOccurred()) Eventually(func() string { node, err = client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) if err != nil { return "ERROR: " + err.Error() } return node.Status.WireguardPublicKey }, "5s", "100ms").Should(Equal(wgPubKeyOrig)) } if wireguardEnabledV6 { var wgPubKeyOrig string var node *internalapi.Node var err error // Get the original public-key. Eventually(func() error { node, err = client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) if err != nil { return err } if node.Status.WireguardPublicKeyV6 == "" { return errors.New("node.Status.WireguardPublicKeyV6 not set yet") } else if wgPubKeyOrig == "" { // Seeing the original public key for the first time. wgPubKeyOrig = node.Status.WireguardPublicKeyV6 } // overwrite public-key by fake but valid Wireguard key. node.Status.WireguardPublicKeyV6 = fakeWireguardPubKeyV6 _, err = client.Nodes().Update(context.Background(), node, options.SetOptions{}) if err != nil { return err } return nil }, "5s", "300ms").ShouldNot(HaveOccurred()) Eventually(func() string { node, err = client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) if err != nil { return "ERROR: " + err.Error() } return node.Status.WireguardPublicKeyV6 }, "5s", "100ms").Should(Equal(wgPubKeyOrig)) } } }) }) Context("traffic with Wireguard enabled", func() { // Checks the TCP dump for a count value. Retries until count is correct, or fails after 1.5s. waitForPackets := func(t *tcpdump.TCPDump, timeout time.Time, name string, num int) error { for ; ; time.Now().Before(timeout) { if num == 0 && t.MatchCount(name) > 0 { // We expect no traffic, but got some. Error immediately. break } if t.MatchCount(name) >= num { // We expected some packets and have got at least the required number (we allow more for handshake // etc.) return nil } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("incorrect packet count for %s; expected=%d actual=%d", name, num, t.MatchCount(name)) } // Runs wg and extracts the received and sent packet counts. getWgStatistics := func(felix *infrastructure.Felix, device string) (sent, rcvd string) { xferRegExp := regexp.MustCompile(`transfer:\s+([0-9a-zA-Z. ]+)\s+received,\s+([0-9a-zA-Z. ]+)\s+sent`) out, err := felix.ExecOutput("wg", "show", device) Expect(err).NotTo(HaveOccurred()) matches := xferRegExp.FindStringSubmatch(out) if len(matches) != 3 { return } return matches[2], matches[1] } // Checks connectivity between workloads 0 and 1, checking that eth0 traffic is via wireguard port and that // there are non-empty wireguard stats. checkConn := func() error { // Reset TCP packet counts. By("Resetting the TCP dump counts") for i := range topologyContainers.Felixes { if wireguardEnabledV4 { tcpdumps[i].ResetCount("numInTunnelPackets") tcpdumps[i].ResetCount("numOutTunnelPackets") tcpdumps[i].ResetCount("numWorkload0to1Packets") tcpdumps[i].ResetCount("numWorkload1to0Packets") } if wireguardEnabledV6 { tcpdumps[i].ResetCount("numInTunnelPacketsV6") tcpdumps[i].ResetCount("numOutTunnelPacketsV6") tcpdumps[i].ResetCount("numWorkload0to1PacketsV6") tcpdumps[i].ResetCount("numWorkload1to0PacketsV6") } } // Send packets to and from workloads on each felix. if wireguardEnabledV4 { By("Sending IPv4 packets W1->W2 and W2->W1") if _, err := wlsV4[0].SendPacketsTo(wlsV4[1].IP, 5, 56); err != nil { return err } if _, err := wlsV4[1].SendPacketsTo(wlsV4[0].IP, 5, 56); err != nil { return err } } if wireguardEnabledV6 { By("Sending IPv6 packets W1->W2 and W2->W1") if _, err := wlsV6[0].SendPacketsTo(wlsV6[1].IP, 5, 56); err != nil { return err } if _, err := wlsV6[1].SendPacketsTo(wlsV6[0].IP, 5, 56); err != nil { return err } } // Now check the packet counts are as expected. We should have no WL->WL traffic visible on eth0, but // we should be able to see tunnel traffic. Since we want to verify By("Checking the packet stats from tcpdump") timeout := time.Now().Add(2 * time.Second) for i := range topologyContainers.Felixes { if wireguardEnabledV4 { if err := waitForPackets(tcpdumps[i], timeout, "numInTunnelPackets", 10); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numOutTunnelPackets", 10); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numWorkload0to1Packets", 0); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numWorkload1to0Packets", 0); err != nil { return err } } if wireguardEnabledV6 { if err := waitForPackets(tcpdumps[i], timeout, "numInTunnelPacketsV6", 10); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numOutTunnelPacketsV6", 10); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numWorkload0to1PacketsV6", 0); err != nil { return err } else if err := waitForPackets(tcpdumps[i], timeout, "numWorkload1to0PacketsV6", 0); err != nil { return err } } } By("Checking the packet stats from wg") for i := range topologyContainers.Felixes { if wireguardEnabledV4 { rcvd, sent := getWgStatistics(topologyContainers.Felixes[i], wireguardInterfaceNameDefault) // TODO: counter compare sent/rcvd data from wg tunnel on each node. Expect(rcvd).NotTo(BeEmpty()) Expect(sent).NotTo(BeEmpty()) } if wireguardEnabledV6 { rcvd, sent := getWgStatistics(topologyContainers.Felixes[i], wireguardInterfaceNameV6Default) // TODO: counter compare sent/rcvd data from wg tunnel on each node. Expect(rcvd).NotTo(BeEmpty()) Expect(sent).NotTo(BeEmpty()) } } return nil } BeforeEach(func() { // Tunnel readiness checks. // Check the Wireguard device exists. Eventually(func() error { for i, felix := range topologyContainers.Felixes { out, err := felix.ExecOutput("ip", "link", "show") if err != nil { return err } if wireguardEnabledV4 { if !strings.Contains(out, wireguardInterfaceNameDefault) { return fmt.Errorf("felix %d has no IPv4 Wireguard device", i) } } if wireguardEnabledV6 { if !strings.Contains(out, wireguardInterfaceNameV6Default) { return fmt.Errorf("felix %d has no IPv6 Wireguard device", i) } } } return nil }, "30s", "330ms").ShouldNot(HaveOccurred()) for _, felix := range topologyContainers.Felixes { // Check the rule exists. if wireguardEnabledV4 { Eventually(func() string { return getWireguardRoutingRule(felix, 4) }, "10s", "100ms").Should(MatchRegexp("\\d+:\\s+not from all fwmark 0x\\d+/0x\\d+ lookup \\d+")) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRoutingRule(felix, 6) }, "10s", "100ms").Should(MatchRegexp("\\d+:\\s+not from all fwmark 0x\\d+/0x\\d+ lookup \\d+")) } } for i, felix := range topologyContainers.Felixes { // Check the route entry exists. if wireguardEnabledV4 { Eventually(func() string { return getWireguardRouteEntry(felix, 4) }, "10s", "100ms").Should(ContainSubstring(routeEntriesV4[i])) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRouteEntry(felix, 6) }, "10s", "100ms").Should(ContainSubstring(routeEntriesV6[i])) } } tcpdumps = make([]*tcpdump.TCPDump, nodeCount) for i, felix := range topologyContainers.Felixes { // felix tcpdump tcpdump := felix.AttachTCPDump("eth0") if wireguardEnabledV4 { // Match WireGuard tunnel data packets but exclude keepalive packets. // WireGuard keepalives are 32-byte UDP payloads; real data packets // carrying encapsulated IP traffic are >= 72 bytes. Require length >= 50 // to avoid flakes from keepalive packets tripping the "zero tunnel // packets after deny" assertion. wgLenFilter := `, length ([5-9]\d|[1-9]\d{2,})` inTunnelPacketsPattern := fmt.Sprintf("IP %s\\.51820 > \\d+\\.\\d+\\.\\d+\\.\\d+\\.51820: UDP"+wgLenFilter, felix.IP) tcpdump.AddMatcher("numInTunnelPackets", regexp.MustCompile(inTunnelPacketsPattern)) outTunnelPacketsPattern := fmt.Sprintf("IP \\d+\\.\\d+\\.\\d+\\.\\d+\\.51820 > %s\\.51820: UDP"+wgLenFilter, felix.IP) tcpdump.AddMatcher("numOutTunnelPackets", regexp.MustCompile(outTunnelPacketsPattern)) workload01PacketsPattern := fmt.Sprintf("IP %s\\.\\d+ > %s\\.\\d+: ", wlsV4[0].IP, wlsV4[1].IP) tcpdump.AddMatcher("numWorkload0to1Packets", regexp.MustCompile(workload01PacketsPattern)) workload10PacketsPattern := fmt.Sprintf("IP %s\\.\\d+ > %s\\.\\d+: ", wlsV4[1].IP, wlsV4[0].IP) tcpdump.AddMatcher("numWorkload1to0Packets", regexp.MustCompile(workload10PacketsPattern)) } if wireguardEnabledV6 { wgV6LenFilter := `, length ([5-9]\d|[1-9]\d{2,})` inTunnelPacketsPatternV6 := fmt.Sprintf("IP6 %s\\.51821 > ([a-f0-9:]+:+)+[a-f0-9]+\\.51821: UDP"+wgV6LenFilter, felix.IPv6) tcpdump.AddMatcher("numInTunnelPacketsV6", regexp.MustCompile(inTunnelPacketsPatternV6)) outTunnelPacketsPatternV6 := fmt.Sprintf("IP6 ([a-f0-9:]+:+)+[a-f0-9]+\\.51821 > %s\\.51821: UDP"+wgV6LenFilter, felix.IPv6) tcpdump.AddMatcher("numOutTunnelPacketsV6", regexp.MustCompile(outTunnelPacketsPatternV6)) workload01PacketsPatternV6 := fmt.Sprintf("IP6 %s\\.\\d+ > %s\\.\\d+: ", wlsV6[0].IP, wlsV6[1].IP) tcpdump.AddMatcher("numWorkload0to1PacketsV6", regexp.MustCompile(workload01PacketsPatternV6)) workload10PacketsPatternV6 := fmt.Sprintf("IP6 %s\\.\\d+ > %s\\.\\d+: ", wlsV6[1].IP, wlsV6[0].IP) tcpdump.AddMatcher("numWorkload1to0PacketsV6", regexp.MustCompile(workload10PacketsPatternV6)) } tcpdump.Start(infra) tcpdumps[i] = tcpdump } }) It("between pod to pod should be allowed and encrypted using wg tunnel", func() { Eventually(checkConn, "10s", "100ms").ShouldNot(HaveOccurred()) }) tests := []struct { hep string iptablesPolicy string }{ { hep: "*", iptablesPolicy: "ACCEPT", }, { hep: "*", iptablesPolicy: "DROP", }, { hep: "eth0", iptablesPolicy: "ACCEPT", }, { hep: "eth0", iptablesPolicy: "DROP", }, } for _, xtc := range tests { tc := xtc desc := "wireguard traffic is allowed with a blocking host endpoint policy" + " (using " + tc.hep + " HostEndpoint, " + tc.iptablesPolicy + ")" It(desc, func() { if NFTMode() && tc.iptablesPolicy != "ACCEPT" { Skip("iptables policies other than ACCEPT are not supported in NFT mode") } By("Creating policy to deny wireguard port on main felix host endpoint.") policy := api.NewGlobalNetworkPolicy() policy.Name = "deny-wg-port" port := numorstring.ProtocolFromString(numorstring.ProtocolUDP) policy.Spec.Egress = []api.Rule{} if wireguardEnabledV4 { policy.Spec.Egress = append(policy.Spec.Egress, api.Rule{ // Deny egress UDP to the wireguard port. Action: api.Deny, Protocol: &port, Destination: api.EntityRule{ Selector: "has(host-endpoint)", Ports: []numorstring.Port{numorstring.SinglePort(wireguardListeningPortDefault)}, }, }, ) } if wireguardEnabledV6 { policy.Spec.Egress = append(policy.Spec.Egress, api.Rule{ // Deny egress UDP to the wireguard port. Action: api.Deny, Protocol: &port, Destination: api.EntityRule{ Selector: "has(host-endpoint)", Ports: []numorstring.Port{numorstring.SinglePort(wireguardListeningPortV6Default)}, }, }, ) } policy.Spec.Egress = append(policy.Spec.Egress, api.Rule{Action: api.Allow}) policy.Spec.Ingress = []api.Rule{} if wireguardEnabledV4 { policy.Spec.Ingress = append(policy.Spec.Ingress, api.Rule{ // Deny all UDP traffic to the hosts. Action: api.Deny, Protocol: &port, Destination: api.EntityRule{ Selector: "has(host-endpoint)", Ports: []numorstring.Port{numorstring.SinglePort(wireguardListeningPortDefault)}, }, }, ) } if wireguardEnabledV6 { policy.Spec.Ingress = append(policy.Spec.Ingress, api.Rule{ // Deny all UDP traffic to the hosts. Action: api.Deny, Protocol: &port, Destination: api.EntityRule{ Selector: "has(host-endpoint)", Ports: []numorstring.Port{numorstring.SinglePort(wireguardListeningPortV6Default)}, }, }, ) } policy.Spec.Ingress = append(policy.Spec.Ingress, api.Rule{Action: api.Allow}) policy.Spec.Selector = "all()" policy.Spec.Types = []api.PolicyType{api.PolicyTypeIngress, api.PolicyTypeEgress} _, err := client.GlobalNetworkPolicies().Create(utils.Ctx, policy, utils.NoOptions) Expect(err).NotTo(HaveOccurred()) By("Creating a HostEndpoint for each Felix") for _, f := range topologyContainers.Felixes { hep := api.NewHostEndpoint() hep.Name = "hep-" + f.Name hep.Labels = map[string]string{ "name": hep.Name, "hostname": f.Hostname, "host-endpoint": "true", } hep.Spec.Node = f.Hostname hep.Spec.ExpectedIPs = []string{} if wireguardEnabledV4 { hep.Spec.ExpectedIPs = append(hep.Spec.ExpectedIPs, f.IP) } if wireguardEnabledV6 { hep.Spec.ExpectedIPs = append(hep.Spec.ExpectedIPs, f.IPv6) } hep.Spec.InterfaceName = tc.hep _, err := client.HostEndpoints().Create(utils.Ctx, hep, options.SetOptions{}) Expect(err).NotTo(HaveOccurred()) } By("Setting iptables INPUT chain policy to " + tc.iptablesPolicy) for _, felix := range topologyContainers.Felixes { _, err := felix.ExecOutput("iptables", "-w", "10", "-W", "100000", "-P", "INPUT", tc.iptablesPolicy) Expect(err).NotTo(HaveOccurred()) } By("Waiting for the policy to apply") if BPFMode() { for _, felix := range topologyContainers.Felixes { bpfWaitForGlobalNetworkPolicy(felix, "eth0", "egress", "deny-wg-port") } } By("Checking there is eventually and consistently connectivity between the workloads using wg") Eventually(checkConn, "5s", "100ms").ShouldNot(HaveOccurred()) Consistently(checkConn, "2s", "100ms").ShouldNot(HaveOccurred()) }) } readPolicy := func(name string, action api.Action) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() policy, err := client.GlobalNetworkPolicies().Get(ctx, name, options.GetOptions{}) if err == nil { if len(policy.Spec.Egress) > 0 && policy.Spec.Egress[0].Action == action { return nil } } return fmt.Errorf("policy not applied") } It("between pod to pod should be encrypted using wg tunnel with egress policies applied", func() { policy := api.NewGlobalNetworkPolicy() policy.Name = "f01-egress-deny" order := float64(20) policy.Spec.Order = &order policy.Spec.Egress = []api.Rule{{Action: api.Deny}} if wireguardEnabledV4 && wireguardEnabledV6 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s', '%s', '%s'}", wlsV4[0].Name, wlsV4[1].Name, wlsV6[0].Name, wlsV6[1].Name) } else if wireguardEnabledV4 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s'}", wlsV4[0].Name, wlsV4[1].Name) } else if wireguardEnabledV6 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s'}", wlsV6[0].Name, wlsV6[1].Name) } _, err := client.GlobalNetworkPolicies().Create(utils.Ctx, policy, utils.NoOptions) Expect(err).NotTo(HaveOccurred()) Eventually(func() error { return readPolicy(policy.Name, api.Deny) }, "10s", "100ms").ShouldNot(HaveOccurred()) if wireguardEnabledV4 { cc.ExpectNone(wlsV4[0], wlsV4[1]) cc.ExpectNone(wlsV4[1], wlsV4[0]) } if wireguardEnabledV6 { cc.ExpectNone(wlsV6[0], wlsV6[1]) cc.ExpectNone(wlsV6[1], wlsV6[0]) } cc.CheckConnectivity() // Reset tunnel packet counts after CheckConnectivity confirms the deny // is enforced. During the Eventually retries inside CheckConnectivity, // packets may still traverse the tunnel before Felix programs the deny. By("verifying tunnelled packet count is zero") for i := range topologyContainers.Felixes { if wireguardEnabledV4 { tcpdumps[i].ResetCount("numInTunnelPackets") tcpdumps[i].ResetCount("numOutTunnelPackets") } if wireguardEnabledV6 { tcpdumps[i].ResetCount("numInTunnelPacketsV6") tcpdumps[i].ResetCount("numOutTunnelPacketsV6") } } for i := range topologyContainers.Felixes { if wireguardEnabledV4 { Consistently(func() int { return tcpdumps[i].MatchCount("numInTunnelPackets") }, "5s", "100ms").Should(BeNumerically("==", 0)) Consistently(func() int { return tcpdumps[i].MatchCount("numOutTunnelPackets") }, "5s", "100ms").Should(BeNumerically("==", 0)) } if wireguardEnabledV6 { Consistently(func() int { return tcpdumps[i].MatchCount("numInTunnelPacketsV6") }, "5s", "100ms").Should(BeNumerically("==", 0)) Consistently(func() int { return tcpdumps[i].MatchCount("numOutTunnelPacketsV6") }, "5s", "100ms").Should(BeNumerically("==", 0)) } } cc.ResetExpectations() policy = api.NewGlobalNetworkPolicy() policy.Name = "f01-egress-allow" order = float64(10) policy.Spec.Order = &order // prioritized over deny policy above. policy.Spec.Egress = []api.Rule{{Action: api.Allow}} if wireguardEnabledV4 && wireguardEnabledV6 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s', '%s', '%s'}", wlsV4[0].Name, wlsV4[1].Name, wlsV6[0].Name, wlsV6[1].Name) } else if wireguardEnabledV4 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s'}", wlsV4[0].Name, wlsV4[1].Name) } else if wireguardEnabledV6 { policy.Spec.Selector = fmt.Sprintf("name in { '%s', '%s'}", wlsV6[0].Name, wlsV6[1].Name) } _, err = client.GlobalNetworkPolicies().Create(utils.Ctx, policy, utils.NoOptions) Expect(err).NotTo(HaveOccurred()) Eventually(func() error { return readPolicy(policy.Name, api.Allow) }, "5s", "100ms").ShouldNot(HaveOccurred()) if wireguardEnabledV4 { cc.ExpectSome(wlsV4[0], wlsV4[1]) cc.ExpectSome(wlsV4[1], wlsV4[0]) } if wireguardEnabledV6 { cc.ExpectSome(wlsV6[0], wlsV6[1]) cc.ExpectSome(wlsV6[1], wlsV6[0]) } cc.CheckConnectivity() By("verifying tunnelled packet count is non-zero") for i := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() int { return tcpdumps[i].MatchCount("numInTunnelPackets") }, "10s", "100ms").Should(BeNumerically(">", 0)) Eventually(func() int { return tcpdumps[i].MatchCount("numOutTunnelPackets") }, "10s", "100ms").Should(BeNumerically(">", 0)) } if wireguardEnabledV6 { Eventually(func() int { return tcpdumps[i].MatchCount("numInTunnelPacketsV6") }, "10s", "100ms").Should(BeNumerically(">", 0)) Eventually(func() int { return tcpdumps[i].MatchCount("numOutTunnelPacketsV6") }, "10s", "100ms").Should(BeNumerically(">", 0)) } } }) }) Context("with Wireguard disabled", func() { BeforeEach(func() { disableWireguard(client) for _, felix := range topologyContainers.Felixes { felix.ExpectedWireguardTunnelAddr = "" felix.ExpectedWireguardV6TunnelAddr = "" } // Check Wireguard device doesn't exist. Eventually(func() error { for i, felix := range topologyContainers.Felixes { out, err := felix.ExecOutput("ip", "link", "show") if err != nil { return err } if wireguardEnabledV4 { if strings.Contains(out, wireguardInterfaceNameDefault) { return fmt.Errorf("felix %d has no IPv4 Wireguard device", i) } } if wireguardEnabledV6 { if strings.Contains(out, wireguardInterfaceNameV6Default) { return fmt.Errorf("felix %d has no IPv6 Wireguard device", i) } } } return nil }, "30s", "330ms").ShouldNot(HaveOccurred()) // Check that Wireguard routing rule doesn't exist. for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { return getWireguardRoutingRule(felix, 4) }, "10s", "100ms").Should(BeEmpty()) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRoutingRule(felix, 6) }, "10s", "100ms").Should(BeEmpty()) } } // Check Wireguard route table entry doesn't exist. for i, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { return getWireguardRouteEntry(felix, 4) }, "10s", "100ms").ShouldNot(ContainSubstring(routeEntriesV4[i])) } if wireguardEnabledV6 { Eventually(func() string { return getWireguardRouteEntry(felix, 6) }, "10s", "100ms").ShouldNot(ContainSubstring(routeEntriesV6[i])) } } }) It("v3 node resource shouldn't contain public-key", func() { for _, felix := range topologyContainers.Felixes { if wireguardEnabledV4 { Eventually(func() string { node, err := client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) return node.Status.WireguardPublicKey }, "10s", "100ms").Should(BeEmpty()) } if wireguardEnabledV6 { Eventually(func() string { node, err := client.Nodes().Get(context.Background(), felix.Hostname, options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) return node.Status.WireguardPublicKeyV6 }, "10s", "100ms").Should(BeEmpty()) } } }) It("workload connectivity remains but uses un-encrypted tunnel", func() { if wireguardEnabledV4 { cc.ExpectSome(wlsV4[0], wlsV4[1]) cc.ExpectSome(wlsV4[1], wlsV4[0]) } if wireguardEnabledV6 { cc.ExpectSome(wlsV6[0], wlsV6[1]) cc.ExpectSome(wlsV6[1], wlsV6[0]) } cc.CheckConnectivity() for _, felix := range topologyContainers.Felixes { Eventually(func() string { // No tunnel implies un-encrypted communication. out, err := felix.ExecOutput("wg") Expect(err).NotTo(HaveOccurred()) return out }, "10s", "100ms").Should(BeEmpty()) } }) }) }) } }) var _ = infrastructure.DatastoreDescribe("WireGuard-Unsupported", []apiconfig.DatastoreType{apiconfig.EtcdV3, apiconfig.Kubernetes}, func(getInfra infrastructure.InfraFactory) { var ( infra infrastructure.DatastoreInfra tc infrastructure.TopologyContainers ) type testConf struct { WireguardEnabledV4 bool WireguardEnabledV6 bool } for _, testConfig := range []testConf{ {true, false}, {false, true}, {true, true}, } { wireguardEnabledV4 := testConfig.WireguardEnabledV4 wireguardEnabledV6 := testConfig.WireguardEnabledV6 Describe(fmt.Sprintf("wireguardEnabledV4: %v, wireguardEnabledV6: %v, ", wireguardEnabledV4, wireguardEnabledV6), func() { BeforeEach(func() { // Run these tests only when the Host does not have Wireguard available. if os.Getenv("FELIX_FV_WIREGUARD_AVAILABLE") == "true" { Skip("Skipping Wireguard unsupported tests.") } // Setup a single node cluster. const nodeCount = 1 infra = getInfra() ipipEnabled := !BPFMode() || !wireguardEnabledV6 tc, _ = infrastructure.StartNNodeTopology(nodeCount, wireguardTopologyOptions("CalicoIPAM", ipipEnabled, wireguardEnabledV4, wireguardEnabledV6, false), infra) // Install a default profile that allows all ingress and egress, in the absence of any Policy. infra.AddDefaultAllow() tc.Felixes[0].TriggerDelayedStart() }) It("no Wireguard device exists", func() { Eventually(func() error { out, err := tc.Felixes[0].ExecOutput("ip", "link", "show") if err != nil { return err } if wireguardEnabledV4 { if !strings.Contains(out, wireguardInterfaceNameDefault) { return fmt.Errorf("felix 0 has no IPv4 Wireguard device") } } if wireguardEnabledV6 { if !strings.Contains(out, wireguardInterfaceNameV6Default) { return fmt.Errorf("felix 0 has no IPv6 Wireguard device") } } return nil }, "30s", "330ms").ShouldNot(HaveOccurred()) }) It("no wg tool info exists", func() { Eventually(func() string { out, err := tc.Felixes[0].ExecOutput("wg") Expect(err).NotTo(HaveOccurred()) return out }, "10s", "100ms").Should(BeEmpty()) }) }) } }) var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ WireGuard-Supported 3 node cluster", []apiconfig.DatastoreType{apiconfig.EtcdV3, apiconfig.Kubernetes}, func(getInfra infrastructure.InfraFactory) { const nodeCount = 3 var ( infra infrastructure.DatastoreInfra tc infrastructure.TopologyContainers client clientv3.Interface wls [nodeCount]*workload.Workload // simulated host workloads cc *connectivity.Checker tcpdumps []*tcpdump.TCPDump ) JustBeforeEach(func() { if BPFMode() { ensureAllNodesBPFProgramsAttached(tc.Felixes) } }) BeforeEach(func() { // TODO: add IPv6 coverage when enabling this back Skip("Skipping WireGuard tests for now due to unreliability.") // Run these tests only when the Host has Wireguard kernel module available. if os.Getenv("FELIX_FV_WIREGUARD_AVAILABLE") != "true" { Skip("Skipping Wireguard supported tests.") } infra = getInfra() topologyOptions := wireguardTopologyOptions("CalicoIPAM", true, true, false, false) tc, client = infrastructure.StartNNodeTopology(nodeCount, topologyOptions, infra) // To allow all ingress and egress, in absence of any Policy. infra.AddDefaultAllow() for i := range wls { wls[i] = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, fmt.Sprintf("10.65.%d.2", i), fmt.Sprintf("wl%d", i), tc.Felixes[i]) } // Create 'borrowed' workloads e.g. create workload on felix-0 with IP // borrowed from IPAM block from felix-1. _ = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, "10.65.0.4", "borrowed-0", tc.Felixes[1]) _ = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, "10.65.1.4", "borrowed-1", tc.Felixes[0]) for i := range tc.Felixes { tc.Felixes[i].TriggerDelayedStart() } cc = &connectivity.Checker{ // If two nodes send their first packet within a few milliseconds then any on-demand Wireguard // handshake can fail and back off if the handshakes cross on the wire. StaggerStartBy: 100 * time.Millisecond, } }) AfterEach(func() { if CurrentSpecReport().Failed() { for _, felix := range tc.Felixes { felix.Exec("ip", "addr") felix.Exec("ip", "rule", "list") felix.Exec("ip", "route", "show", "table", "all") felix.Exec("ip", "route", "show", "cached") felix.Exec("wg") } } for _, wl := range wls { wl.Stop() } for _, tcpdump := range tcpdumps { tcpdump.Stop() } tc.Stop() if CurrentSpecReport().Failed() { infra.DumpErrorData() } infra.Stop() }) It("Workload with borrowed IP should be 'handled' on felix 0 and 1", func() { // Check that felix-0, felix-1 is ready // 1. by checking, Wireguard interface exist. Eventually(func() error { for i := range []int{0, 1} { out, err := tc.Felixes[i].ExecOutput("ip", "link") if err != nil { return err } if strings.Contains(out, wireguardInterfaceNameDefault) { continue } return fmt.Errorf("felix-%d has no wireguard device", i) } return nil }, "10s", "100ms").ShouldNot(HaveOccurred()) // 2. by checking, Wireguard rule exist. for i := range []int{0, 1} { Eventually(func() string { return getWireguardRoutingRule(tc.Felixes[i], 4) }, "10s", "100ms").Should(MatchRegexp(`\d+:\s+not from all fwmark 0x\d+/0x\d+ lookup \d+`)) } // 3. by checking, Wireguard route table exist. for i := range []int{0, 1} { Eventually(func() string { return getWireguardRouteEntry(tc.Felixes[i], 4) }, "10s", "100ms").Should(ContainSubstring(fmt.Sprintf("dev %s scope link", wireguardInterfaceNameDefault))) } By("verifying WireGuard route table should show 'throw' entry on felix 0 and 1") Eventually(func() string { return getWireguardRouteEntry(tc.Felixes[0], 4) }, "10s", "100ms").Should(ContainSubstring("throw 10.65.1.4")) Eventually(func() string { return getWireguardRouteEntry(tc.Felixes[1], 4) }, "10s", "100ms").Should(ContainSubstring("throw 10.65.0.4")) }) Context("with Wireguard disabled in node-3", func() { BeforeEach(func() { // Disable WireGuard on felix-2. disableWireguardForFelix(client, "node."+tc.Felixes[2].Hostname) tc.Felixes[2].ExpectedWireguardTunnelAddr = "" tc.Felixes[2].ExpectedWireguardV6TunnelAddr = "" // Check felix-2 is ready with WireGuard disabled. Eventually(func() string { out, _ := tc.Felixes[2].ExecOutput("ip", "link", "show", wireguardInterfaceNameDefault) return out }, "10s", "100ms").Should(BeEmpty()) Eventually(func() string { return getWireguardRoutingRule(tc.Felixes[2], 4) }, "10s", "100ms").Should(BeEmpty()) Eventually(func() string { return getWireguardRouteEntry(tc.Felixes[2], 4) }, "10s", "100ms").ShouldNot(ContainSubstring(fmt.Sprintf("dev %s scope link", wireguardInterfaceNameDefault))) // Check felix-0, felix-1 is ready for tests. Eventually(func() error { for i := range []int{0, 1} { out, err := tc.Felixes[i].ExecOutput("ip", "link") if err != nil { return err } if strings.Contains(out, wireguardInterfaceNameDefault) { continue } return fmt.Errorf("felix-%d has no Wireguard device", i) } return nil }, "10s", "100ms").ShouldNot(HaveOccurred()) for i := range []int{0, 1} { // Check the rule exists. Eventually(func() string { return getWireguardRoutingRule(tc.Felixes[i], 4) }, "10s", "100ms").Should(MatchRegexp(`\d+:\s+not from all fwmark 0x\d+/0x\d+ lookup \d+`)) } for i := range []int{0, 1} { // Check the route entry exists. Eventually(func() string { return getWireguardRouteEntry(tc.Felixes[i], 4) }, "10s", "100ms").Should(ContainSubstring(fmt.Sprintf("dev %s scope link", wireguardInterfaceNameDefault))) } tcpdumps = nil for _, felix := range tc.Felixes { tcpdump := felix.AttachTCPDump("eth0") // tunnel packets. tunnelPackets01Pattern := fmt.Sprintf("IP %s\\.51820 > %s\\.51820: UDP", tc.Felixes[0].IP, tc.Felixes[1].IP) tcpdump.AddMatcher("numTunnelPackets01", regexp.MustCompile(tunnelPackets01Pattern)) tunnelPackets10Pattern := fmt.Sprintf("IP %s\\.51820 > %s\\.51820: UDP", tc.Felixes[1].IP, tc.Felixes[0].IP) tcpdump.AddMatcher("numTunnelPackets10", regexp.MustCompile(tunnelPackets10Pattern)) tunnelPackets02Pattern := fmt.Sprintf("IP %s\\.51820 > %s\\.51820: UDP", tc.Felixes[0].IP, tc.Felixes[2].IP) tcpdump.AddMatcher("numTunnelPackets02", regexp.MustCompile(tunnelPackets02Pattern)) tunnelPackets20Pattern := fmt.Sprintf("IP %s\\.51820 > %s\\.51820: UDP", tc.Felixes[2].IP, tc.Felixes[0].IP) tcpdump.AddMatcher("numTunnelPackets20", regexp.MustCompile(tunnelPackets20Pattern)) // direct workload packets. outWorkloadPacketsPattern := fmt.Sprintf("IP %s\\.\\d+ > %s\\.\\d+:", wls[0].IP, wls[2].IP) tcpdump.AddMatcher("numOutWorkloadPackets", regexp.MustCompile(outWorkloadPacketsPattern)) inWorkloadPacketsPattern := fmt.Sprintf("IP %s\\.\\d+ > %s\\.\\d+:", wls[2].IP, wls[0].IP) tcpdump.AddMatcher("numInWorkloadPackets", regexp.MustCompile(inWorkloadPacketsPattern)) tcpdump.Start(infra) tcpdumps = append(tcpdumps, tcpdump) } }) It("transfer should be encrypted/plain between workloads on WireGuard enabled/disabled nodes", func() { cc.ExpectSome(wls[0], wls[1]) cc.ExpectSome(wls[1], wls[0]) cc.CheckConnectivity() By("verifying packets between felix-0 and felix-1 is encrypted") for i := range []int{0, 1} { Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets01") }, "10s", "100ms").Should(BeNumerically(">", 0)) Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets10") }, "10s", "100ms").Should(BeNumerically(">", 0)) } cc.ResetExpectations() cc.ExpectSome(wls[2], wls[0]) cc.ExpectSome(wls[0], wls[2]) cc.CheckConnectivity() By("verifying packets between felix-0 and felix-2 are not encrypted") for _, f := range []int{0, 2} { Eventually(func() int { return tcpdumps[f].MatchCount("numInWorkloadPackets") }, "10s", "100ms").Should(BeNumerically(">", 0)) Eventually(func() int { return tcpdumps[f].MatchCount("numOutWorkloadPackets") }, "10s", "100ms").Should(BeNumerically(">", 0)) } }) It("WireGuard should be used for host to workload connections on WireGuard enabled nodes", func() { cc.ExpectSome(tc.Felixes[0], wls[1]) cc.CheckConnectivity() By("verifying packets between felix-0 and felix-1 is encrypted") for _, i := range []int{0, 1} { Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets01") }, "10s", "100ms").Should(BeNumerically(">", 0)) Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets10") }, "10s", "100ms").Should(BeNumerically(">", 0)) } }) It("WireGuard should not be used for host to workload connections when WireGuard disabled on either node", func() { cc.ExpectSome(tc.Felixes[0], wls[2]) cc.ExpectSome(tc.Felixes[2], wls[0]) cc.CheckConnectivity() By("verifying packets between felix-0 and felix-2 are not encrypted") for _, i := range []int{0, 2} { Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets02") }, "10s", "100ms").Should(BeNumerically("==", 0)) Eventually(func() int { return tcpdumps[i].MatchCount("numTunnelPackets20") }, "10s", "100ms").Should(BeNumerically("==", 0)) } }) }) }) var _ = infrastructure.DatastoreDescribe("_BPF-SAFE_ WireGuard-Supported 3-node cluster with WorkloadIPs", []apiconfig.DatastoreType{apiconfig.EtcdV3, apiconfig.Kubernetes}, func(getInfra infrastructure.InfraFactory) { const nodeCount, wlPerNode = 3, 2 var ( infra infrastructure.DatastoreInfra tc infrastructure.TopologyContainers client clientv3.Interface // simulated host workloads wlsByHost [nodeCount][wlPerNode]*workload.Workload hostNetworkedWls [nodeCount]*workload.Workload externalClient *containers.Container cc *connectivity.Checker tcpdumps []*tcpdump.TCPDump ) BeforeEach(func() { // TODO: add IPv6 coverage when enabling this back Skip("Skipping WireGuard tests for now due to unreliability.") // Run these tests only when the Host has Wireguard kernel module available. if os.Getenv("FELIX_FV_WIREGUARD_AVAILABLE") != "true" { Skip("Skipping Wireguard supported tests.") } infra = getInfra() topologyOptions := wireguardTopologyOptions("WorkloadIPs", false, true, false, false) tc, client = infrastructure.StartNNodeTopology(nodeCount, topologyOptions, infra) // To allow all ingress and egress, in absence of any Policy. infra.AddDefaultAllow() // initialise pods for felixIdx, felixWls := range wlsByHost { for wlIdx := range felixWls { wlsByHost[felixIdx][wlIdx] = createWorkloadWithAssignedIP( &infra, &topologyOptions, &client, fmt.Sprintf("10.65.%d.%d", felixIdx, 2+wlIdx), fmt.Sprintf("wl-f%d-%d", felixIdx, wlIdx), tc.Felixes[felixIdx]) } } // initialise host-networked pods for i := range hostNetworkedWls { hostNetworkedWls[i] = createHostNetworkedWorkload(fmt.Sprintf("wl-f%d-hn-0", i), tc.Felixes[i], 4) } // initialise external client externalClient = infrastructure.RunExtClient(infra, "ext-client") externalClient.Exec("ip", "route", "add", wlsByHost[0][0].IP, "via", tc.Felixes[0].IP) for i := range tc.Felixes { tc.Felixes[i].TriggerDelayedStart() } // Check felix Wireguard links are ready. for i := range tc.Felixes { Eventually(func() string { out, _ := tc.Felixes[i].ExecOutput("ip", "link", "show", wireguardInterfaceNameDefault) return out }, "10s", "100ms").Should(Not(BeEmpty())) } tcpdumps = nil for _, felix := range tc.Felixes { tcpdump := felix.AttachTCPDump("eth0") tunnelPacketsFelix0toFelix1Pattern := fmt.Sprintf("IP %s\\.%d > %s\\.%d: UDP", tc.Felixes[0].IP, wireguardListeningPortDefault, tc.Felixes[1].IP, wireguardListeningPortDefault) tcpdump.AddMatcher("numTunnelPacketsFelix0toFelix1", regexp.MustCompile(tunnelPacketsFelix0toFelix1Pattern)) tunnelPacketsFelix1toFelix0Pattern := fmt.Sprintf("IP %s\\.%d > %s\\.%d: UDP", tc.Felixes[1].IP, wireguardListeningPortDefault, tc.Felixes[0].IP, wireguardListeningPortDefault) tcpdump.AddMatcher("numTunnelPacketsFelix1toFelix0", regexp.MustCompile(tunnelPacketsFelix1toFelix0Pattern)) nonTunnelPacketsFelix0toFelix1Pattern := fmt.Sprintf("IP %s\\.%s > %s\\.%s: TCP", tc.Felixes[0].IP, defaultWorkloadPort, tc.Felixes[1].IP, defaultWorkloadPort) tcpdump.AddMatcher("numNonTunnelPacketsFelix0toFelix1", regexp.MustCompile(nonTunnelPacketsFelix0toFelix1Pattern)) nonTunnelPacketsFelix1toFelix0Pattern := fmt.Sprintf("IP %s\\.%s > %s\\.%s: TCP", tc.Felixes[1].IP, defaultWorkloadPort, tc.Felixes[0].IP, defaultWorkloadPort) tcpdump.AddMatcher("numNonTunnelPacketsFelix1toFelix0", regexp.MustCompile(nonTunnelPacketsFelix1toFelix0Pattern)) tcpdump.Start(infra) tcpdumps = append(tcpdumps, tcpdump) } cc = &connectivity.Checker{ // If two nodes send their first packet within a few milliseconds then any on-demand Wireguard // handshake can fail and back off if the handshakes cross on the wire. StaggerStartBy: 100 * time.Millisecond, } // Ping other felix nodes from each node to trigger Wireguard handshakes. for i, felix := range tc.Felixes { for j := range tc.Felixes { if i != j { if err := felix.ExecMayFail("ping", "-c", "1", "-W", "1", "-s", "1", tc.Felixes[j].IP); err != nil { log.WithError(err).Warning("felix.ExecMayFail returned err") } } } } // Check felix nodes have performed Wireguard handshakes. for i, felix := range tc.Felixes { var matchers []types.GomegaMatcher for j := range tc.Felixes { if i != j { matchers = append(matchers, BeNumerically(">", 0)) } } Eventually(func() []int { var handshakes []int out, _ := felix.ExecOutput("wg", "show", wireguardInterfaceNameDefault, "latest-handshakes") peers := strings.SplitSeq(out, "\n") for peer := range peers { parts := strings.Split(peer, "\t") if len(parts) == 2 { h, _ := strconv.Atoi(parts[1]) handshakes = append(handshakes, h) } } return handshakes }, "30s", "100ms").Should(ContainElements(matchers)) } }) AfterEach(func() { for _, tcpdump := range tcpdumps { tcpdump.Stop() } }) It("should pass basic connectivity scenarios", func() { By("Checking the interface exists") Eventually(func() error { for _, felix := range tc.Felixes { out, err := felix.ExecOutput("ip", "link") if err != nil { return err } if strings.Contains(out, wireguardInterfaceNameDefault) { continue } return fmt.Errorf("felix %v has no wireguard device", felix.Name) } return nil }, "10s", "100ms").ShouldNot(HaveOccurred()) By("Checking the ip rule exists") for _, felix := range tc.Felixes { Eventually(func() string { return getWireguardRoutingRule(felix, 4) }, "10s", "100ms").Should(MatchRegexp(`\d+:\s+not from all fwmark 0x\d+/0x\d+ lookup \d+`)) } By("Checking the routing table entries exist") for i := range wlsByHost { var matchers []types.GomegaMatcher for j, wls := range wlsByHost { if i != j { // check for routes to other felix nodes matchers = append(matchers, ContainSubstring( fmt.Sprintf("%s dev %s scope link", tc.Felixes[j].IP, wireguardInterfaceNameDefault))) // check for routes to pods on other felix nodes for _, wl := range wls { matchers = append(matchers, ContainSubstring( fmt.Sprintf("%s dev %s scope link", wl.IP, wireguardInterfaceNameDefault))) } } } Eventually(func() []string { return strings.Split(getWireguardRouteEntry(tc.Felixes[i], 4), "\n") }, "10s", "100ms").Should(ContainElements(matchers)) } if NFTMode() { By("Checking the nftables raw chain cali-wireguard-incoming-mark exists") for _, felix := range tc.Felixes { Eventually(func() string { s, _ := felix.ExecCombinedOutput("nft", "list", "table", "ip", "calico") return s }, "10s", "100ms").Should(ContainSubstring("raw-cali-wireguard-incoming-mark")) } } else { By("Checking the iptables raw chain cali-wireguard-incoming-mark exists") for _, felix := range tc.Felixes { Eventually(func() string { s, _ := felix.ExecCombinedOutput("iptables", "-L", "cali-wireguard-incoming-mark", "-t", "raw") return s }, "10s", "100ms").Should(ContainSubstring("Chain cali-wireguard-incoming-mark")) } } By("Checking the proc/sys src valid mark entries") for _, felix := range tc.Felixes { Eventually(func() string { s, _ := felix.ExecCombinedOutput("cat", "/proc/sys/net/ipv4/conf/all/src_valid_mark") return s }, "10s", "100ms").Should(ContainSubstring("1")) } By("Checking wireguard allowed ips") for i := range wlsByHost { var matchers []types.GomegaMatcher for j, wls := range wlsByHost { if i != j { var allowedIPMatchers []types.GomegaMatcher // check for allowed IP entry for other felix nodes allowedIPMatchers = append(allowedIPMatchers, ContainSubstring(tc.Felixes[j].IP)) // check for routes to pods on other felix nodes for _, wl := range wls { allowedIPMatchers = append(allowedIPMatchers, ContainSubstring(wl.IP)) } matchers = append(matchers, And(allowedIPMatchers...)) } } Eventually(func() []string { s, _ := tc.Felixes[i].ExecCombinedOutput("wg", "show", wireguardInterfaceNameDefault, "allowed-ips") return strings.Split(s, "\n") }, "10s", "100ms").Should(ContainElements(matchers)) } By("verifying packets between felix-0 and felix-1 is encrypted") cc.ExpectSome(wlsByHost[0][1], wlsByHost[1][0]) cc.ExpectSome(wlsByHost[1][0], wlsByHost[0][1]) cc.CheckConnectivity() for i := range []int{0, 1} { numNonTunnelPacketsFelix0toFelix1Before := tcpdumps[i].MatchCount("numNonTunnelPacketsFelix0toFelix1") numNonTunnelPacketsFelix1toFelix0Before := tcpdumps[i].MatchCount("numNonTunnelPacketsFelix1toFelix0") Eventually(tcpdumps[i].MatchCountFn("numTunnelPacketsFelix0toFelix1"), "10s", "100ms"). Should(BeNumerically(">", 0)) Eventually(tcpdumps[i].MatchCountFn("numTunnelPacketsFelix1toFelix0"), "10s", "100ms"). Should(BeNumerically(">", 0)) Expect(tcpdumps[i].MatchCount("numNonTunnelPacketsFelix0toFelix1")). Should(BeNumerically("==", numNonTunnelPacketsFelix0toFelix1Before)) Expect(tcpdumps[i].MatchCount("numNonTunnelPacketsFelix1toFelix0")). Should(BeNumerically("==", numNonTunnelPacketsFelix1toFelix0Before)) } cc.ResetExpectations() By("checking same node pod-to-pod connectivity") for felixIdx := range nodeCount { cc.ExpectSome(wlsByHost[felixIdx][0], wlsByHost[felixIdx][1]) } By("checking different node pod-to-pod connectivity") for i := range wlsByHost { for j := range wlsByHost { cc.ExpectSome(wlsByHost[i][0], wlsByHost[j][0]) } } By("checking host-networked pod to regular pod connectivity") for _, wl := range hostNetworkedWls { for j := range wlsByHost { cc.ExpectSome(wl, wlsByHost[j][0]) } } By("checking external node to pod connectivity") cc.ExpectSome(externalClient, wlsByHost[0][0]) By("checking prometheus metrics render") for _, felix := range tc.Felixes { s, err := felix.ExecCombinedOutput("wget", "localhost:9091/metrics", "-O", "-") Expect(err).ToNot(HaveOccurred()) // quick and dirty comparison to see if metrics we want exist and with correct type for _, expectedMetric := range []string{ "# TYPE wireguard_meta gauge", "# TYPE wireguard_latest_handshake_seconds gauge", "# TYPE wireguard_bytes_rcvd counter", "# TYPE wireguard_bytes_sent counter", } { Expect(s).To(ContainSubstring(expectedMetric)) } } cc.CheckConnectivity() }) }) // Setup cluster topology options. // mainly, enable Wireguard with delayed start option. func wireguardTopologyOptions(routeSource string, ipipEnabled, wireguardIPv4Enabled, wireguardIPv6Enabled, wireguardThreadingEnabled bool, extraEnvs ...map[string]string) infrastructure.TopologyOptions { topologyOptions := infrastructure.DefaultTopologyOptions() // Waiting for calico-node to be ready. topologyOptions.DelayFelixStart = true // Enable IPv6 if IPv6 Wireguard will be enabled. topologyOptions.EnableIPv6 = wireguardIPv6Enabled // Indicate wireguard is enabled topologyOptions.WireguardEnabled = wireguardIPv4Enabled topologyOptions.WireguardEnabledV6 = wireguardIPv6Enabled // RouteSource if routeSource == "WorkloadIPs" { topologyOptions.UseIPPools = false } topologyOptions.ExtraEnvVars["FELIX_ROUTESOURCE"] = routeSource topologyOptions.ExtraEnvVars["FELIX_PROMETHEUSMETRICSENABLED"] = "true" if ipipEnabled && !wireguardIPv6Enabled { topologyOptions.IPIPMode = api.IPIPModeAlways } else { topologyOptions.IPIPMode = api.IPIPModeNever topologyOptions.SimulateBIRDRoutes = true } // With Wireguard and BPF mode the default IptablesMarkMask of 0xffff0000 isn't enough. topologyOptions.ExtraEnvVars["FELIX_IPTABLESMARKMASK"] = "4294934528" // 0xffff8000 for _, envs := range extraEnvs { maps.Copy(topologyOptions.ExtraEnvVars, envs) } // Enable Wireguard. felixConfig := api.NewFelixConfiguration() felixConfig.SetName("default") enabled := true if wireguardIPv4Enabled { felixConfig.Spec.WireguardEnabled = &enabled } if wireguardIPv6Enabled { felixConfig.Spec.WireguardEnabledV6 = &enabled } if wireguardThreadingEnabled { felixConfig.Spec.WireguardThreadingEnabled = &enabled } topologyOptions.InitialFelixConfiguration = felixConfig return topologyOptions } func disableWireguard(client clientv3.Interface) { updateWireguardEnabledConfig(client, false, false, false) } func updateWireguardEnabledConfig(client clientv3.Interface, valueV4, valueV6, valueThreadingEnabled bool) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() felixConfig, err := client.FelixConfigurations().Get(ctx, "default", options.GetOptions{}) Expect(err).NotTo(HaveOccurred()) felixConfig.Spec.WireguardEnabled = &valueV4 felixConfig.Spec.WireguardEnabledV6 = &valueV6 felixConfig.Spec.WireguardThreadingEnabled = &valueThreadingEnabled felixConfig, err = client.FelixConfigurations().Update(ctx, felixConfig, options.SetOptions{}) Expect(err).NotTo(HaveOccurred()) } func getWireguardRoutingRule(felix *infrastructure.Felix, ipVersion int) string { Expect(ipVersion == 4 || ipVersion == 6).To(BeTrue()) var rule string var err error switch ipVersion { case 4: rule, err = felix.ExecOutput("ip", "rule", "show", "pref", wireguardRoutingRulePriorityDefault) case 6: rule, err = felix.ExecOutput("ip", "-6", "rule", "show", "pref", wireguardRoutingRulePriorityDefault) } Expect(err).NotTo(HaveOccurred()) return strings.TrimSpace(rule) } func getWireguardRouteEntry(felix *infrastructure.Felix, ipVersion int) string { Expect(ipVersion == 4 || ipVersion == 6).To(BeTrue()) rule := getWireguardRoutingRule(felix, ipVersion) // Get route table index from rule. routingRuleRegExp := regexp.MustCompile(`\d+$`) tableId := routingRuleRegExp.FindString(rule) if tableId == "" { return "" } // Check route table entry. var routes string var err error switch ipVersion { case 4: routes, err = felix.ExecOutput("ip", "route", "show", "table", tableId) case 6: routes, err = felix.ExecOutput("ip", "-6", "route", "show", "table", tableId) } Expect(err).NotTo(HaveOccurred()) return routes } func disableWireguardForFelix(client clientv3.Interface, felixName string) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() felixConfig := api.NewFelixConfiguration() felixConfig.SetName(felixName) disabled := false felixConfig.Spec.WireguardEnabled = &disabled felixConfig.Spec.WireguardEnabledV6 = &disabled felixConfig, err := client.FelixConfigurations().Create(ctx, felixConfig, options.SetOptions{}) Expect(err).NotTo(HaveOccurred()) } func createWorkloadWithAssignedIP( infra *infrastructure.DatastoreInfra, infraOpts *infrastructure.TopologyOptions, client *clientv3.Interface, wlIP, wlName string, felix *infrastructure.Felix, ) *workload.Workload { ip := net.MustParseIP(wlIP) mtu := wireguardMTUDefault if ip.To4() == nil { mtu = wireguardMTUV6Default } if infraOpts.UseIPPools { infrastructure.AssignIP(wlName, wlIP, felix.Hostname, *client) } wl := workload.Run(felix, wlName, "default", wlIP, defaultWorkloadPort, "tcp", workload.WithMTU(mtu)) wl.ConfigureInInfra(*infra) return wl } func createHostNetworkedWorkload(wlName string, felix *infrastructure.Felix, ipVersion int) *workload.Workload { Expect(ipVersion == 4 || ipVersion == 6).To(BeTrue()) ip := felix.IP mtu := wireguardMTUDefault if ipVersion == 6 { ip = felix.IPv6 mtu = wireguardMTUV6Default } return workload.Run(felix, wlName, "default", ip, defaultWorkloadPort, "tcp", workload.WithMTU(mtu)) }