Skip to content

Commit 944ab91

Browse files
committed
fix(FoU): make more robust
FoU implementation now properly handles a whole host of things: * It now actually handles IPv6 by changing the encapsulation protocol to GUE instead of generic FoU. I worked with generic FoU tunnels for several days and could get it to support IPv4 and IPv6 at all even when placing using it with the IPv6 proto and with iproute2 in IPv6 mode (-6) * It now handles converting between the two tunnel types seemlessly and without leaving legacy tunnel artifacts behind. Previously, you could change the encap type but it wouldn't change the tunnels * Abstracted constants
1 parent bac4ae6 commit 944ab91

File tree

3 files changed

+149
-29
lines changed

3 files changed

+149
-29
lines changed

pkg/controllers/routing/network_routes_controller.go

+68-24
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ const (
7777
encapTypeFOU = "fou"
7878
encapTypeIPIP = "ipip"
7979

80+
ipipModev4 = "ipip"
81+
ipipModev6 = "ip6ip6"
82+
8083
maxPort = uint16(65535)
8184
minPort = uint16(1024)
8285
)
@@ -656,7 +659,7 @@ func (nrc *NetworkRoutingController) injectRoute(path *gobgpapi.Path) error {
656659
// if the user has disabled overlays, don't create tunnels. If we're not creating a tunnel, check to see if there is
657660
// any cleanup that needs to happen.
658661
if shouldCreateTunnel() {
659-
link, err = nrc.setupOverlayTunnel(tunnelName, nextHop)
662+
link, err = nrc.setupOverlayTunnel(tunnelName, nextHop, dst)
660663
if err != nil {
661664
return err
662665
}
@@ -741,60 +744,104 @@ func (nrc *NetworkRoutingController) cleanupTunnel(destinationSubnet *net.IPNet,
741744
}
742745

743746
// setupOverlayTunnel attempts to create a tunnel link and corresponding routes for IPIP based overlay networks
744-
func (nrc *NetworkRoutingController) setupOverlayTunnel(tunnelName string, nextHop net.IP) (netlink.Link, error) {
747+
func (nrc *NetworkRoutingController) setupOverlayTunnel(tunnelName string, nextHop net.IP,
748+
nextHopSubnet *net.IPNet) (netlink.Link, error) {
745749
var out []byte
746750
link, err := netlink.LinkByName(tunnelName)
747751

748752
var bestIPForFamily net.IP
749-
var ipipMode string
750-
var ipProto string
753+
var ipipMode, fouLinkType string
754+
isIPv6 := false
751755
ipBase := make([]string, 0)
756+
strFormattedEncapPort := strconv.FormatInt(int64(nrc.overlayEncapPort), 10)
757+
752758
if nextHop.To4() != nil {
753759
bestIPForFamily = utils.FindBestIPv4NodeAddress(nrc.primaryIP, nrc.nodeIPv4Addrs)
754-
ipipMode = "ipip"
755-
ipProto = "4"
760+
ipipMode = encapTypeIPIP
761+
fouLinkType = ipipModev4
756762
} else {
757763
// Need to activate the ip command in IPv6 mode
758764
ipBase = append(ipBase, "-6")
759765
bestIPForFamily = utils.FindBestIPv6NodeAddress(nrc.primaryIP, nrc.nodeIPv6Addrs)
760-
ipipMode = "ip6ip6"
761-
ipProto = "6"
766+
ipipMode = ipipModev6
767+
fouLinkType = "ip6tnl"
768+
isIPv6 = true
762769
}
763770
if nil == bestIPForFamily {
764771
return nil, fmt.Errorf("not able to find an appropriate configured IP address on node for destination "+
765772
"IP family: %s", nextHop.String())
766773
}
767774

775+
// This indicated that the tunnel already exists, so it's possible that there might be nothing more needed. However,
776+
// it is also possible that the user changed the encap type, so we need to make sure that the encap type matches
777+
// and if it doesn't, create it
778+
recreate := false
779+
if err == nil {
780+
klog.V(1).Infof("Tunnel interface: %s with encap type %s for the node %s already exists.",
781+
tunnelName, link.Attrs().EncapType, nextHop.String())
782+
783+
switch nrc.overlayEncap {
784+
case encapTypeIPIP:
785+
if linkFOUEnabled(tunnelName) {
786+
klog.Infof("Was configured to use ipip tunnels, but found existing fou tunnels in place, cleaning up")
787+
recreate = true
788+
789+
// Even though we are setup for IPIP tunels we have existing tunnels that are FoU tunnels, remove them
790+
// so that we can recreate them as IPIP
791+
nrc.cleanupTunnel(nextHopSubnet, tunnelName)
792+
793+
// If we are transitioning from FoU to IPIP we also need to clean up the old FoU port if it exists
794+
if fouPortAndProtoExist(nrc.overlayEncapPort, isIPv6) {
795+
fouArgs := ipBase
796+
fouArgs = append(fouArgs, "fou", "del", "port", strFormattedEncapPort)
797+
out, err := exec.Command("ip", fouArgs...).CombinedOutput()
798+
if err != nil {
799+
klog.Warningf("failed to clean up previous FoU tunnel port (this is only a warning because it "+
800+
"won't stop kube-router from working for now, but still shouldn't have happened) - error: "+
801+
"%v, output %s", err, out)
802+
}
803+
}
804+
}
805+
case encapTypeFOU:
806+
if !linkFOUEnabled(tunnelName) {
807+
klog.Infof("Was configured to use fou tunnels, but found existing ipip tunnels in place, cleaning up")
808+
recreate = true
809+
// Even though we are setup for FoU tunels we have existing tunnels that are IPIP tunnels, remove them
810+
// so that we can recreate them as IPIP
811+
nrc.cleanupTunnel(nextHopSubnet, tunnelName)
812+
}
813+
}
814+
}
815+
768816
// an error here indicates that the tunnel didn't exist, so we need to create it, if it already exists there's
769817
// nothing to do here
770-
if err != nil {
818+
if err != nil || recreate {
819+
klog.Infof("Creating tunnel %s of type %s with encap %s for destination %s",
820+
tunnelName, fouLinkType, nrc.overlayEncap, nextHop.String())
771821
cmdArgs := ipBase
772822
switch nrc.overlayEncap {
773-
case "ipip":
823+
case encapTypeIPIP:
774824
// Plain IPIP tunnel without any encapsulation
775825
cmdArgs = append(cmdArgs, "tunnel", "add", tunnelName, "mode", ipipMode, "local", bestIPForFamily.String(),
776826
"remote", nextHop.String())
777-
case "fou":
778-
strFormattedEncapPort := strconv.FormatInt(int64(nrc.overlayEncapPort), 10)
779827

828+
case encapTypeFOU:
780829
// Ensure that the FOU tunnel port is set correctly
781-
cmdArgs = append(cmdArgs, "fou", "show")
782-
out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
783-
if err != nil || !strings.Contains(string(out), strFormattedEncapPort) {
784-
//nolint:gocritic // we understand that we are appending to a new slice
785-
cmdArgs = append(ipBase, "fou", "add", "port", strFormattedEncapPort, "ipproto", ipProto)
786-
out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
830+
if !fouPortAndProtoExist(nrc.overlayEncapPort, isIPv6) {
831+
fouArgs := ipBase
832+
fouArgs = append(fouArgs, "fou", "add", "port", strFormattedEncapPort, "gue")
833+
out, err := exec.Command("ip", fouArgs...).CombinedOutput()
787834
if err != nil {
788835
return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
789836
"Failed to set FoU tunnel port - error: %s, output: %s", tunnelName, err, string(out))
790837
}
791838
}
792839

793840
// Prep IPIP tunnel for FOU encapsulation
794-
//nolint:gocritic // we understand that we are appending to a new slice
795-
cmdArgs = append(ipBase, "link", "add", "name", tunnelName, "type", "ipip", "remote", nextHop.String(),
796-
"local", bestIPForFamily.String(), "ttl", "225", "encap", "fou", "encap-sport", "auto", "encap-dport",
841+
cmdArgs = append(cmdArgs, "link", "add", "name", tunnelName, "type", fouLinkType, "remote", nextHop.String(),
842+
"local", bestIPForFamily.String(), "ttl", "225", "encap", "gue", "encap-sport", "auto", "encap-dport",
797843
strFormattedEncapPort, "mode", ipipMode)
844+
798845
default:
799846
return nil, fmt.Errorf("unknown tunnel encapsulation was passed: %s, unable to continue with overlay "+
800847
"setup", nrc.overlayEncap)
@@ -821,9 +868,6 @@ func (nrc *NetworkRoutingController) setupOverlayTunnel(tunnelName string, nextH
821868
if err = netlink.LinkSetUp(link); err != nil {
822869
return nil, errors.New("Failed to bring tunnel interface " + tunnelName + " up due to: " + err.Error())
823870
}
824-
} else {
825-
klog.V(1).Infof(
826-
"Tunnel interface: " + tunnelName + " for the node " + nextHop.String() + " already exists.")
827871
}
828872

829873
// Now that the tunnel link exists, we need to add a route to it, so the node knows where to send traffic bound for

pkg/controllers/routing/network_routes_controller_test.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -1996,11 +1996,7 @@ func Test_generateTunnelName(t *testing.T) {
19961996
tunnelName := generateTunnelName(testcase.nodeIP)
19971997
assert.Lessf(t, len(tunnelName), 16, "the maximum length of the tunnel name should never exceed"+
19981998
"15 characters as 16 characters is the maximum length of a Unix interface name")
1999-
if tunnelName != testcase.tunnelName {
2000-
t.Logf("actual tunnel interface name: %s", tunnelName)
2001-
t.Logf("expected tunnel interface name: %s", testcase.tunnelName)
2002-
t.Error("did not get expected tunnel interface name")
2003-
}
1999+
assert.Equal(t, testcase.tunnelName, tunnelName, "did not get expected tunnel interface name")
20042000
})
20052001
}
20062002
}

pkg/controllers/routing/utils.go

+80
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package routing
22

33
import (
4+
"bufio"
45
"crypto/sha256"
56
"encoding/base64"
67
"errors"
78
"fmt"
89
"net"
10+
"os/exec"
911
"regexp"
1012
"strconv"
1113
"strings"
@@ -324,3 +326,81 @@ func (nrc *NetworkRoutingController) getBGPRouteInfoForVIP(vip string) (subnet u
324326
err = fmt.Errorf("could not convert IP to IPv4 or IPv6, unable to find subnet for: %s", vip)
325327
return
326328
}
329+
330+
// fouPortAndProtoExist checks to see if the given FoU port is already configured on the system via iproute2
331+
// tooling for the given protocol
332+
//
333+
// fou show, shows both IPv4 and IPv6 ports in the same show command, they look like:
334+
// port 5556 gue
335+
// port 5556 gue -6
336+
// where the only thing that distinguishes them is the -6 or not on the end
337+
// WARNING we're parsing a CLI tool here not an API, this may break at some point in the future
338+
func fouPortAndProtoExist(port uint16, isIPv6 bool) bool {
339+
const ipRoute2IPv6Prefix = "-6"
340+
strPort := strconv.FormatInt(int64(port), 10)
341+
fouArgs := make([]string, 0)
342+
klog.V(2).Infof("Checking FOU Port and Proto... %s - %t", strPort, isIPv6)
343+
344+
if isIPv6 {
345+
fouArgs = append(fouArgs, ipRoute2IPv6Prefix)
346+
}
347+
fouArgs = append(fouArgs, "fou", "show")
348+
349+
out, err := exec.Command("ip", fouArgs...).CombinedOutput()
350+
// iproute2 returns an error if no fou configuration exists
351+
if err != nil {
352+
return false
353+
}
354+
355+
strOut := string(out)
356+
klog.V(2).Infof("Combined output of ip fou show: %s", strOut)
357+
scanner := bufio.NewScanner(strings.NewReader(strOut))
358+
359+
// loop over all lines of output
360+
for scanner.Scan() {
361+
scannedLine := scanner.Text()
362+
// if the output doesn't contain our port at all, then continue
363+
if !strings.Contains(scannedLine, strPort) {
364+
continue
365+
}
366+
367+
// if this is IPv6 port and it has the correct IPv6 suffix (see example above) then return true
368+
if isIPv6 && strings.HasSuffix(scannedLine, ipRoute2IPv6Prefix) {
369+
return true
370+
}
371+
372+
// if this is not IPv6 and it does not have an IPv6 suffix (see example above) then return true
373+
if !isIPv6 && !strings.HasSuffix(scannedLine, ipRoute2IPv6Prefix) {
374+
return true
375+
}
376+
}
377+
378+
return false
379+
}
380+
381+
// linkFOUEnabled checks to see whether the given link has FoU (Foo over Ethernet) enabled on it, specifically since
382+
// kube-router only works with GUE (Generic UDP Encapsulation) we look for that and not just FoU in general. If the
383+
// linkName is enabled with FoU GUE then we return true, otherwise false
384+
//
385+
// Output for a FoU Enabled GUE tunnel looks like:
386+
// ipip ipip remote <ip> local <ip> dev <dev> ttl 225 pmtudisc encap gue encap-sport auto encap-dport 5555 ...
387+
// Output for a normal IPIP tunnel looks like:
388+
// ipip ipip remote <ip> local <ip> dev <dev> ttl inherit ...
389+
func linkFOUEnabled(linkName string) bool {
390+
const fouEncapEnabled = "encap gue"
391+
cmdArgs := []string{"-details", "link", "show", linkName}
392+
393+
out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
394+
395+
if err != nil {
396+
klog.Warning("recevied an error while trying to look at the link details of %s, this shouldn't have happened",
397+
linkName)
398+
return false
399+
}
400+
401+
if strings.Contains(string(out), fouEncapEnabled) {
402+
return true
403+
}
404+
405+
return false
406+
}

0 commit comments

Comments
 (0)