Repository: cloudnativelabs/kube-router
Pull request #1697 (2024-07-05 17:31:57)

feat: convert execs to ip to netlink calls
Not making direct exec calls to user binary interfaces has long been a principle of kube-router. When kube-router was first coded, the netlink library was missing significant features that forced us to exec out. However, now netlink seems to have most of the functionality that we need.

This converts all of the places where we can use netlink to use the netlink functionality.

The current state of this PR is untested and still needs to undergo significant testing:

* [x] Ensure IPv4 routes are getting populated correctly
* [x] Ensure IPv4 source routing is being added to custom table
* [x] Ensure IPv6 routes are getting populated correctly
* [x] Ensure IPv6 source routing is being added to custom table
* [x] Ensure IPv4 Service VIPs get added to the dummy interface
* [x] Ensure IPv6 Service VIPs get added to the dummy interface
* [x] Ensure DSR works
* [x] Ensure ipip encapsulation works
* [x] Ensure fou encapsulation works

github-actions[bot]: This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 10 days.
aauren: There is a minor outstanding case where there is some problems with DSR traffic (see #1870) however, this was true whether netlink was used or iproute2 user-space tooling. So I think that we're fine leaving that issue alone for now.

Patch:
diff --git a/pkg/controllers/proxy/linux_networking.go b/pkg/controllers/proxy/linux_networking.go
index 868f9ae1..eb056b11 100644
--- a/pkg/controllers/proxy/linux_networking.go
+++ b/pkg/controllers/proxy/linux_networking.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"net"
 	"os"
-	"os/exec"
 	"path"
 	"strconv"
 	"strings"
@@ -24,9 +23,6 @@ import (
 )
 
 const (
-	ipv4NetMaskBits = 32
-	ipv6NetMaskBits = 128
-
 	// TODO: it's bad to rely on eth0 here. While this is inside the container's namespace and is determined by the
 	// container runtime and so far we've been able to count on this being reliably set to eth0, it is possible that
 	// this may shift sometime in the future with a different runtime. It would be better to find a reliable way to
@@ -66,8 +62,7 @@ type netlinkCalls interface {
 }
 
 func (ln *linuxNetworking) ipAddrDel(iface netlink.Link, ip string, nodeIP string) error {
-	var netMask net.IPMask
-	var ipRouteCmdArgs []string
+	var nFamily int
 	parsedIP := net.ParseIP(ip)
 	parsedNodeIP := net.ParseIP(nodeIP)
 	if parsedIP.To4() != nil {
@@ -75,9 +70,7 @@ func (ln *linuxNetworking) ipAddrDel(iface netlink.Link, ip string, nodeIP strin
 		if parsedNodeIP.To4() == nil {
 			return fmt.Errorf("nodeIP %s does not match family for VIP IP: %s, unable to proceed", ip, nodeIP)
 		}
-
-		netMask = net.CIDRMask(ipv4NetMaskBits, ipv4NetMaskBits)
-		ipRouteCmdArgs = make([]string, 0)
+		nFamily = netlink.FAMILY_V4
 	} else {
 		// If the IP family of the NodeIP and the VIP IP don't match, we can't proceed
 		if parsedNodeIP.To4() != nil {
@@ -88,49 +81,84 @@ func (ln *linuxNetworking) ipAddrDel(iface netlink.Link, ip string, nodeIP strin
 			klog.V(2).Infof("Ignoring link-local IP address: %s", ip)
 			return nil
 		}
-
-		netMask = net.CIDRMask(ipv6NetMaskBits, ipv6NetMaskBits)
-		ipRouteCmdArgs = []string{"-6"}
+		nFamily = netlink.FAMILY_V6
 	}
 
-	naddr := &netlink.Addr{IPNet: &net.IPNet{IP: parsedIP, Mask: netMask}, Scope: syscall.RT_SCOPE_LINK}
-	err := netlink.AddrDel(iface, naddr)
+	naddr := &netlink.Addr{IPNet: utils.GetSingleIPNet(parsedIP), Scope: syscall.RT_SCOPE_LINK}
+	addrList, err := netlink.AddrList(iface, nFamily)
+	nIfaceHasAddr := false
 	if err != nil {
-		if err.Error() != IfaceHasNoAddr {
-			klog.Errorf("Failed to verify is external ip %s is assocated with dummy interface %s due to %s",
-				ip, iface.Attrs().Name, err.Error())
-			return err
-		} else {
-			klog.Warningf("got an IfaceHasNoAddr error while trying to delete address %s from netlink %s: %v (this "+
-				"is not normally bad enough to stop processing)", ip, iface.Attrs().Name, err)
+		return fmt.Errorf("failed to list addresses for interface %s due to: %v", iface.Attrs().Name, err)
+	}
+	for _, addr := range addrList {
+		if addr.IP.Equal(parsedIP) {
+			nIfaceHasAddr = true
+			break
+		}
+	}
+	if nIfaceHasAddr {
+		err := netlink.AddrDel(iface, naddr)
+		if err != nil {
+			if err.Error() != IfaceHasNoAddr {
+				klog.Errorf("Failed to verify is external ip %s is assocated with dummy interface %s due to %s",
+					ip, iface.Attrs().Name, err.Error())
+				return err
+			} else {
+				klog.Warningf("got an IfaceHasNoAddr error while trying to delete address %s from netlink %s: %v (this "+
+					"is not normally bad enough to stop processing)", ip, iface.Attrs().Name, err)
+			}
 		}
 	}
 
 	// Delete VIP addition to "local" rt table also, fail silently if not found (DSR special case)
 	// #nosec G204
-	ipRouteCmdArgs = append(ipRouteCmdArgs, "route", "delete", "local", ip, "dev", KubeDummyIf,
-		"table", "local", "proto", "kernel", "scope", "host", "src", nodeIP, "table", "local")
-	out, err := exec.Command("ip", ipRouteCmdArgs...).CombinedOutput()
+	nRoute := &netlink.Route{
+		Type:      unix.RTN_LOCAL,
+		Dst:       utils.GetSingleIPNet(parsedIP),
+		LinkIndex: iface.Attrs().Index,
+		Table:     syscall.RT_TABLE_LOCAL,
+		Protocol:  unix.RTPROT_KERNEL,
+		Scope:     syscall.RT_SCOPE_HOST,
+		Src:       parsedNodeIP,
+	}
+	routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, nRoute,
+		netlink.RT_FILTER_TYPE|netlink.RT_FILTER_DST|netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_SRC)
 	if err != nil {
-		if !strings.Contains(string(out), "No such process") {
-			klog.Errorf("Failed to delete route to service VIP %s configured on %s. Error: %v, Output: %s",
-				ip, KubeDummyIf, err, out)
-		} else {
-			klog.Warningf("got a No such process error while trying to remove route: %v (this is not normally bad "+
-				"enough to stop processing)", err)
-			return nil
+		return fmt.Errorf("failed to list filtered routes for route %s: %v", nRoute, err)
+	}
+
+	// Let the user know if we found more than 1 route that matched a single route filter
+	if len(routes) > 1 {
+		klog.Warning("Found more than 1 route that matched a single route filter, this is not expected, please " +
+			"report this upstream as a bug if you see this message along with the information below:")
+		for _, route := range routes {
+			klog.Infof("Found route: %s", route)
 		}
+		klog.Warningf("Continuing with deletion of the route we know about: %s", nRoute)
 	}
 
-	return err
+	// If we found one or no routes, this is expected, so we can proceed with the action
+	if len(routes) > 0 {
+		klog.V(1).Infof("Found %d routes for interface %s, deleting them...", len(routes), iface.Attrs().Name)
+		err = netlink.RouteDel(nRoute)
+		if err != nil {
+			if !strings.Contains(err.Error(), "no such process") {
+				return fmt.Errorf("failed to delete route %s: %v", nRoute, err)
+			}
+			klog.Warningf("got a No such process error while trying to remove route %s: %v (this is not normally bad "+
+				"enough to stop processing)", nRoute, err)
+		}
+	} else {
+		klog.V(1).Infof("No routes found for %s, skipping deletion", nRoute)
+	}
+
+	return nil
 }
 
 // utility method to assign an IP to an interface. Mainly used to assign service VIP's
 // to kube-dummy-if. Also when DSR is used, used to assign VIP to dummy interface
 // inside the container.
 func (ln *linuxNetworking) ipAddrAdd(iface netlink.Link, ip string, nodeIP string, addRoute bool) error {
-	var netMask net.IPMask
-	var ipRouteCmdArgs []string
 	var isIPv6 bool
 	parsedIP := net.ParseIP(ip)
 	parsedNodeIP := net.ParseIP(nodeIP)
@@ -139,22 +167,17 @@ func (ln *linuxNetworking) ipAddrAdd(iface netlink.Link, ip string, nodeIP strin
 		if addRoute && parsedNodeIP.To4() == nil {
 			return fmt.Errorf("nodeIP %s does not match family for VIP IP: %s, unable to proceed", ip, nodeIP)
 		}
-
-		netMask = net.CIDRMask(ipv4NetMaskBits, ipv4NetMaskBits)
-		ipRouteCmdArgs = make([]string, 0)
 		isIPv6 = false
 	} else {
 		// If we're supposed to add a route and the IP family of the NodeIP and the VIP IP don't match, we can't proceed
 		if addRoute && parsedNodeIP.To4() != nil {
 			return fmt.Errorf("nodeIP %s does not match family for VIP IP: %s, unable to proceed", ip, nodeIP)
 		}
-
-		netMask = net.CIDRMask(ipv6NetMaskBits, ipv6NetMaskBits)
-		ipRouteCmdArgs = []string{"-6"}
 		isIPv6 = true
 	}
 
-	naddr := &netlink.Addr{IPNet: &net.IPNet{IP: parsedIP, Mask: netMask}, Scope: syscall.RT_SCOPE_LINK}
+	ipPrefix := utils.GetSingleIPNet(parsedIP)
+	naddr := &netlink.Addr{IPNet: ipPrefix, Scope: syscall.RT_SCOPE_LINK}
 	err := netlink.AddrAdd(iface, naddr)
 	if err != nil && err.Error() != IfaceHasAddr {
 		klog.Errorf("failed to assign cluster ip %s to dummy interface: %s", naddr.IP.String(), err.Error())
@@ -169,16 +192,24 @@ func (ln *linuxNetworking) ipAddrAdd(iface netlink.Link, ip string, nodeIP strin
 		return nil
 	}
 
-	// TODO: netlink.RouteReplace which is replacement for below command is not working as expected. Call succeeds but
-	// route is not replaced. For now do it with command.
-	// #nosec G204
-	ipRouteCmdArgs = append(ipRouteCmdArgs, "route", "replace", "local", ip, "dev", KubeDummyIf,
-		"table", "local", "proto", "kernel", "scope", "host", "src", nodeIP, "table", "local")
-
-	out, err := exec.Command("ip", ipRouteCmdArgs...).CombinedOutput()
+	kubeDummyLink, err := netlink.LinkByName(KubeDummyIf)
+	if err != nil {
+		klog.Errorf("failed to get %s link due to %v", KubeDummyIf, err)
+		return err
+	}
+	nRoute := &netlink.Route{
+		Type:      unix.RTN_LOCAL,
+		Dst:       ipPrefix,
+		LinkIndex: kubeDummyLink.Attrs().Index,
+		Table:     syscall.RT_TABLE_LOCAL,
+		Protocol:  unix.RTPROT_KERNEL,
+		Scope:     syscall.RT_SCOPE_HOST,
+		Src:       parsedNodeIP,
+	}
+	err = netlink.RouteReplace(nRoute)
 	if err != nil {
-		klog.Errorf("Failed to replace route to service VIP %s configured on %s. Error: %v, Output: %s",
-			ip, KubeDummyIf, err, out)
+		klog.Errorf("Failed to replace route to service VIP %s configured on %s. Error: %v",
+			ip, KubeDummyIf, err)
 		return err
 	}
 
@@ -187,7 +218,7 @@ func (ln *linuxNetworking) ipAddrAdd(iface netlink.Link, ip string, nodeIP strin
 	// create the source route below. See: https://github.com/cloudnativelabs/kube-router/issues/1698
 	if isIPv6 {
 		nRoute := &netlink.Route{
-			Dst:   &net.IPNet{IP: parsedIP, Mask: netMask},
+			Dst:   utils.GetSingleIPNet(parsedIP),
 			Table: unix.RT_TABLE_UNSPEC,
 		}
 		routes, err := netlink.RouteListFiltered(netlink.FAMILY_V6, nRoute,
@@ -318,10 +349,10 @@ func (ln *linuxNetworking) ipvsAddService(svcs []*ipvs.Service, vip net.IP, prot
 	var ipMask uint32
 	if vip.To4() != nil {
 		ipvsFamily = syscall.AF_INET
-		ipMask = uint32(ipv4NetMaskBits)
+		ipMask = utils.GetIPv4NetMaxMaskBits()
 	} else {
 		ipvsFamily = syscall.AF_INET6
-		ipMask = uint32(ipv6NetMaskBits)
+		ipMask = utils.GetIPv6NetMaxMaskBits()
 	}
 	svc := ipvs.Service{
 		Address:       vip,
@@ -361,9 +392,9 @@ func (ln *linuxNetworking) ipvsAddFWMarkService(svcs []*ipvs.Service, fwMark uin
 	var netmaskForFamily uint32
 	switch family {
 	case syscall.AF_INET:
-		netmaskForFamily = ipv4NetMaskBits
+		netmaskForFamily = utils.GetIPv4NetMaxMaskBits()
 	case syscall.AF_INET6:
-		netmaskForFamily = ipv6NetMaskBits
+		netmaskForFamily = utils.GetIPv6NetMaxMaskBits()
 	}
 	for _, svc := range svcs {
 		if fwMark == svc.FWMark {
@@ -482,24 +513,59 @@ func (ln *linuxNetworking) setupPolicyRoutingForDSR(setupIPv4, setupIPv6 bool) e
 		return fmt.Errorf("failed to setup policy routing required for DSR due to %v", err)
 	}
 
+	loNetLink, err := netlink.LinkByName("lo")
+	if err != nil {
+		return fmt.Errorf("failed to get loopback interface due to %v", err)
+	}
+
 	if setupIPv4 {
-		out, err := exec.Command("ip", "route", "list", "table", customDSRRouteTableID).Output()
-		if err != nil || !strings.Contains(string(out), " lo ") {
-			if err = exec.Command("ip", "route", "add", "local", "default", "dev", "lo", "table",
-				customDSRRouteTableID).Run(); err != nil {
-				return fmt.Errorf("failed to add route in custom route table due to: %v", err)
+		nFamily := netlink.FAMILY_V4
+		defaultRouteCIDR := utils.GetDefaultIPv4Route()
+		if defaultRouteCIDR == nil {
+			return fmt.Errorf("failed to parse default (%s) route (this is statically defined, so if you see this "+
+				"error please report because something has gone very wrong)", defaultRouteCIDR)
+		}
+		nRoute := &netlink.Route{
+			Type:      unix.RTN_LOCAL,
+			Family:    nFamily,
+			Dst:       defaultRouteCIDR,
+			LinkIndex: loNetLink.Attrs().Index,
+			Table:     customDSRRouteTableID,
+			Scope:     unix.RT_SCOPE_HOST,
+		}
+		routes, err := netlink.RouteListFiltered(nFamily, nRoute, netlink.RT_FILTER_TABLE|netlink.RT_FILTER_OIF)
+		if err != nil || len(routes) < 1 {
+			err = netlink.RouteAdd(nRoute)
+			if err != nil {
+				return fmt.Errorf("failed to add route to custom route table for DSR due to: %v", err)
 			}
 		}
 	}
+
 	if setupIPv6 {
-		out, err := exec.Command("ip", "-6", "route", "list", "table", customDSRRouteTableID).Output()
-		if err != nil || !strings.Contains(string(out), " lo ") {
-			if err = exec.Command("ip", "-6", "route", "add", "local", "default", "dev", "lo", "table",
-				customDSRRouteTableID).Run(); err != nil {
-				return fmt.Errorf("failed to add route in custom route table due to: %v", err)
+		nFamily := netlink.FAMILY_V6
+		defaultRouteCIDR := utils.GetDefaultIPv6Route()
+		if defaultRouteCIDR == nil {
+			return fmt.Errorf("failed to parse default (%s) route (this is statically defined, so if you see this "+
+				"error please report because something has gone very wrong)", defaultRouteCIDR)
+		}
+		nRoute := &netlink.Route{
+			Type:      unix.RTN_LOCAL,
+			Family:    nFamily,
+			Dst:       defaultRouteCIDR,
+			LinkIndex: loNetLink.Attrs().Index,
+			Table:     customDSRRouteTableID,
+			Scope:     unix.RT_SCOPE_HOST,
+		}
+		routes, err := netlink.RouteListFiltered(nFamily, nRoute, netlink.RT_FILTER_TABLE|netlink.RT_FILTER_OIF)
+		if err != nil || len(routes) < 1 {
+			err = netlink.RouteAdd(nRoute)
+			if err != nil {
+				return fmt.Errorf("failed to add route to custom route table for DSR due to: %v", err)
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -507,7 +573,6 @@ func (ln *linuxNetworking) setupPolicyRoutingForDSR(setupIPv4, setupIPv6 bool) e
 // directly responds back with source IP as external IP kernel will treat as martian packet.
 // To prevent martian packets add route to external IP through the `kube-bridge` interface
 // setupRoutesForExternalIPForDSR: setups routing so that kernel does not think return packets as martians
-
 func (ln *linuxNetworking) setupRoutesForExternalIPForDSR(serviceInfoMap serviceInfoMap,
 	setupIPv4, setupIPv6 bool) error {
 	err := utils.RouteTableAdd(externalIPRouteTableID, externalIPRouteTableName)
@@ -515,65 +580,125 @@ func (ln *linuxNetworking) setupRoutesForExternalIPForDSR(serviceInfoMap service
 		return fmt.Errorf("failed to setup policy routing required for DSR due to %v", err)
 	}
 
-	setupIPRulesAndRoutes := func(ipArgs []string) error {
-		out, err := runIPCommandsWithArgs(ipArgs, "rule", "list").Output()
+	setupIPRulesAndRoutes := func(isIPv6 bool) error {
+		nFamily := netlink.FAMILY_V4
+		// kFamily := v1.IPv4Protocol
+		defaultPrefixCIDR := utils.GetDefaultIPv4Route()
+		if isIPv6 {
+			nFamily = netlink.FAMILY_V6
+			// kFamily = v1.IPv6Protocol
+			defaultPrefixCIDR = utils.GetDefaultIPv6Route()
+		}
+		if defaultPrefixCIDR == nil {
+			return fmt.Errorf("failed to parse default route (this is statically defined, so if you see this " +
+				"error please report because something has gone very wrong)")
+		}
+
+		nRule := netlink.NewRule()
+		nRule.Family = nFamily
+		nRule.Priority = defaultDSRPolicyRulePriority
+		nRule.Src = defaultPrefixCIDR
+		nRule.Table = externalIPRouteTableID
+
+		// It would be better if we could filter by src, but it's not actually set by iproute2 when netlink receives it
+		// back. Instead, a rule.Src that is set to 0.0.0.0/0 or ::/0 will come back as nil. So if we filter by src, we
+		// will not find any rules.
+		rules, err := netlink.RuleListFiltered(nFamily, nRule,
+			netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PRIORITY)
 		if err != nil {
-			return fmt.Errorf("failed to verify if `ip rule add prio 32765 from all lookup external_ip` exists due to: %v",
-				err)
+			return fmt.Errorf("failed to list rule for external IP's and verify if `ip rule add prio 32765 from all "+
+				"lookup external_ip` exists due to: %v", err)
+		}
+
+		klog.V(2).Infof("rules found: %d", len(rules))
+		defaultRuleFound := false
+		for _, rule := range rules {
+			klog.V(2).Infof("rule: %+v", rule)
+			// If the rule.Src is nil, it means that the rule is a default route rule (0.0.0.0/0 or ::/0)
+			if rule.Src == nil {
+				defaultRuleFound = true
+			}
 		}
 
-		if !strings.Contains(string(out), externalIPRouteTableName) &&
-			!strings.Contains(string(out), externalIPRouteTableID) {
-			err = runIPCommandsWithArgs(ipArgs, "rule", "add", "prio", "32765", "from", "all", "lookup",
-				externalIPRouteTableID).Run()
+		if !defaultRuleFound {
+			err = netlink.RuleAdd(nRule)
 			if err != nil {
-				klog.Infof("Failed to add policy rule `ip rule add prio 32765 from all lookup external_ip` due to %v",
-					err.Error())
-				return fmt.Errorf("failed to add policy rule `ip rule add prio 32765 from all lookup external_ip` "+
-					"due to %v", err)
+				klog.Infof("Failed to add policy rule (equivalent to `ip rule add prio %d from %s lookup "+
+					"%d`) due to %v", defaultDSRPolicyRulePriority, defaultPrefixCIDR, externalIPRouteTableID, err)
+				return fmt.Errorf("failed to add policy rule (equivalent to `ip rule add prio %d from %s lookup "+
+					"%d`) due to %v", defaultDSRPolicyRulePriority, defaultPrefixCIDR, externalIPRouteTableID, err)
 			}
 		}
 
-		out, _ = runIPCommandsWithArgs(ipArgs, "route", "list", "table", externalIPRouteTableID).Output()
-		outStr := string(out)
+		// Commenting this out for now as it seems to be causing more issues than it solves. This was originally added
+		// in PR #210 - https://github.com/cloudnativelabs/kube-router/pull/210/files to supposedly help with martian
+		// packets, but when enabled it seems to cause issues with service routing from the node to an external IP
+		// service from the local node to an non-local traffic policy service.
+		/* kubeBridgeLink, err := netlink.LinkByName(KubeBridgeIf)
+		if err != nil {
+			return fmt.Errorf("failed to get kube-bridge interface due to %v", err)
+		}
+
 		activeExternalIPs := make(map[string]bool)
 		for _, svc := range serviceInfoMap {
-			for _, externalIP := range svc.externalIPs {
+			externalIPs := getAllExternalIPs(svc, true)
+			for _, externalIP := range externalIPs[kFamily] {
+				klog.V(2).Infof("Looking at adding route for Service %s/%s externalIP: %s", svc.namespace, svc.name,
+					externalIP)
 				// Verify the DSR annotation exists
 				if !svc.directServerReturn {
 					klog.V(1).Infof("Skipping service %s/%s as it does not have DSR annotation",
 						svc.namespace, svc.name)
 					continue
 				}
+				klog.V(2).Infof("DSR annotation exists for Service %s/%s, proceeding with route for: %s",
+					svc.namespace, svc.name, externalIP)
 
-				activeExternalIPs[externalIP] = true
+				activeExternalIPs[externalIP.String()] = true
 
-				if !strings.Contains(outStr, externalIP) {
-					if err = runIPCommandsWithArgs(ipArgs, "route", "add", externalIP, "dev", "kube-bridge", "table",
-						externalIPRouteTableID).Run(); err != nil {
+				nRoute := &netlink.Route{
+					Dst:       utils.GetSingleIPNet(externalIP),
+					LinkIndex: kubeBridgeLink.Attrs().Index,
+					Table:     externalIPRouteTableID,
+				}
+
+				routes, err := netlink.RouteListFiltered(nFamily, nRoute,
+					netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_OIF)
+				if err != nil {
+					return fmt.Errorf("failed to list route for external IP's due to: %s", err)
+				}
+				if len(routes) < 1 {
+					err = netlink.RouteAdd(nRoute)
+					if err != nil {
 						klog.Errorf("Failed to add route for %s in custom route table for external IP's due to: %v",
 							externalIP, err)
 						continue
 					}
+					klog.V(2).Infof("Successfully added route for %s in custom route table for external IP's",
+						externalIP)
+					continue
 				}
+				klog.V(2).Infof("Route for %s in custom route table for external IP's already exists", externalIP)
 			}
-		}
+		} */
 
 		// check if there are any pbr in externalIPRouteTableID for external IP's
-		if len(outStr) > 0 {
-			// clean up stale external IPs
-			for _, line := range strings.Split(strings.Trim(outStr, "\n"), "\n") {
-				route := strings.Split(strings.Trim(line, " "), " ")
-				ip := route[0]
-				if !activeExternalIPs[ip] {
-					args := []string{"route", "del", "table", externalIPRouteTableID}
-					args = append(args, route...)
-					if err = runIPCommandsWithArgs(ipArgs, args...).Run(); err != nil {
-						klog.Errorf("Failed to del route for %v in custom route table for external IP's due to: %s",
-							ip, err)
-						continue
-					}
-				}
+		nRoute := &netlink.Route{
+			Family: nFamily,
+			Table:  externalIPRouteTableID,
+		}
+		routes, err := netlink.RouteListFiltered(nFamily, nRoute, netlink.RT_FILTER_TABLE)
+		if err != nil {
+			return fmt.Errorf("failed to list route for external IP's due to: %s", err)
+		}
+		for idx, route := range routes {
+			ip := route.Dst.IP.String()
+			klog.Infof("Deleting route: %+v in custom route table for external IP's as it is not active", route)
+			err = netlink.RouteDel(&routes[idx])
+			if err != nil {
+				klog.Errorf("Failed to del route for %v in custom route table for external IP's due to: %s",
+					ip, err)
+				continue
 			}
 		}
 
@@ -581,13 +706,13 @@ func (ln *linuxNetworking) setupRoutesForExternalIPForDSR(serviceInfoMap service
 	}
 
 	if setupIPv4 {
-		err = setupIPRulesAndRoutes([]string{})
+		err = setupIPRulesAndRoutes(false)
 		if err != nil {
 			return err
 		}
 	}
 	if setupIPv6 {
-		err = setupIPRulesAndRoutes([]string{"-6"})
+		err = setupIPRulesAndRoutes(true)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/controllers/proxy/network_services_controller.go b/pkg/controllers/proxy/network_services_controller.go
index f0e7fe26..1abbfe09 100644
--- a/pkg/controllers/proxy/network_services_controller.go
+++ b/pkg/controllers/proxy/network_services_controller.go
@@ -33,6 +33,7 @@ const (
 	KubeDummyIf       = "kube-dummy-if"
 	KubeTunnelIfv4    = "kube-tunnel-if"
 	KubeTunnelIfv6    = "kube-tunnel-v6"
+	KubeBridgeIf      = "kube-bridge"
 	IfaceNotFound     = "Link not found"
 	IfaceHasAddr      = "file exists"
 	IfaceHasNoAddr    = "cannot assign requested address"
@@ -42,11 +43,13 @@ const (
 	IpvsSvcFSched2    = "flag-2"
 	IpvsSvcFSched3    = "flag-3"
 
-	customDSRRouteTableID    = "78"
-	customDSRRouteTableName  = "kube-router-dsr"
-	externalIPRouteTableID   = "79"
-	externalIPRouteTableName = "external_ip"
-	kubeRouterProxyName      = "kube-router"
+	customDSRRouteTableID              = 78
+	customDSRRouteTableName            = "kube-router-dsr"
+	externalIPRouteTableID             = 79
+	externalIPRouteTableName           = "external_ip"
+	kubeRouterProxyName                = "kube-router"
+	defaultTrafficDirectorRulePriority = 32764
+	defaultDSRPolicyRulePriority       = 32765
 
 	// Taken from https://github.com/torvalds/linux/blob/master/include/uapi/linux/ip_vs.h#L21
 	ipvsPersistentFlagHex = 0x0001
@@ -1727,24 +1730,39 @@ func (nsc *NetworkServicesController) cleanupMangleTableRule(ip string, protocol
 // For DSR it is required that we dont assign the VIP to any interface to avoid martian packets
 // http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/LVS-HOWTO.routing_to_VIP-less_director.html
 // routeVIPTrafficToDirector: setups policy routing so that FWMARKed packets are delivered locally
-func routeVIPTrafficToDirector(fwmark string, family v1.IPFamily) error {
-	ipArgs := make([]string, 0)
+func routeVIPTrafficToDirector(fwmark uint32, family v1.IPFamily) error {
+	nFamily := netlink.FAMILY_V4
 	if family == v1.IPv6Protocol {
-		ipArgs = append(ipArgs, "-6")
+		nFamily = netlink.FAMILY_V6
 	}
 
-	out, err := runIPCommandsWithArgs(ipArgs, "rule", "list").Output()
+	nRule := netlink.NewRule()
+	nRule.Family = nFamily
+	nRule.Mark = fwmark
+	nRule.Table = customDSRRouteTableID
+	nRule.Priority = defaultTrafficDirectorRulePriority
+
+	routes, err := netlink.RuleListFiltered(nFamily, nRule,
+		netlink.RT_FILTER_MARK|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PRIORITY)
 	if err != nil {
-		return errors.New("Failed to verify if `ip rule` exists due to: " + err.Error())
+		return fmt.Errorf("failed to verify if `ip rule` exists due to: %v", err)
 	}
-	if !strings.Contains(string(out), fwmark+" ") {
-		err = runIPCommandsWithArgs(ipArgs, "rule", "add", "prio", "32764", "fwmark", fwmark, "table",
-			customDSRRouteTableID).Run()
+
+	if len(routes) < 1 {
+		klog.V(1).Infof("adding policy rule (%s) to lookup traffic to VIP through the custom routing table", nRule)
+		err = netlink.RuleAdd(nRule)
 		if err != nil {
-			return errors.New("Failed to add policy rule to lookup traffic to VIP through the custom " +
-				" routing table due to " + err.Error())
+			return fmt.Errorf("failed to add policy rule to lookup traffic to VIP through the custom "+
+				"routing table due to: %v", err)
+		}
+	} else {
+		klog.V(1).Infof("policy rule (%s) for mark %d already exists, skipping", nRule, fwmark)
+		klog.V(1).Info("Routes Found:")
+		for _, route := range routes {
+			klog.V(1).Infof("Route: %+v with mark %d", route, route.Mark)
 		}
 	}
+
 	return nil
 }
 
diff --git a/pkg/controllers/proxy/service_endpoints_sync.go b/pkg/controllers/proxy/service_endpoints_sync.go
index e6325fec..72f9359e 100644
--- a/pkg/controllers/proxy/service_endpoints_sync.go
+++ b/pkg/controllers/proxy/service_endpoints_sync.go
@@ -557,7 +557,7 @@ func (nsc *NetworkServicesController) setupExternalIPForDSRService(svcIn *servic
 	}
 
 	// do policy routing to deliver the packet locally so that IPVS can pick the packet
-	err = routeVIPTrafficToDirector("0x"+fmt.Sprintf("%x", fwMark), family)
+	err = routeVIPTrafficToDirector(fwMark, family)
 	if err != nil {
 		return fmt.Errorf("failed to setup ip rule to lookup traffic to external IP: %s through custom "+
 			"route table due to %v", externalIP, err)
diff --git a/pkg/controllers/proxy/utils.go b/pkg/controllers/proxy/utils.go
index eef5f2ea..20e1b221 100644
--- a/pkg/controllers/proxy/utils.go
+++ b/pkg/controllers/proxy/utils.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"hash/fnv"
 	"net"
-	"os/exec"
 	"runtime"
 	"strconv"
 	"strings"
@@ -603,14 +602,6 @@ func getIPVSFirewallInputChainRule(family v1.IPFamily) []string {
 		"-j", ipvsFirewallChainName}
 }
 
-// runIPCommandsWithArgs extend the exec.Command interface to allow passing an additional array of arguments to ip
-func runIPCommandsWithArgs(ipArgs []string, additionalArgs ...string) *exec.Cmd {
-	var allArgs []string
-	allArgs = append(allArgs, ipArgs...)
-	allArgs = append(allArgs, additionalArgs...)
-	return exec.Command("ip", allArgs...)
-}
-
 // getLabelFromMap checks the list of passed labels for the service.kubernetes.io/service-proxy-name
 // label and if it exists, returns it otherwise returns an error
 func getLabelFromMap(label string, labels map[string]string) (string, error) {
diff --git a/pkg/controllers/routing/bgp_policies.go b/pkg/controllers/routing/bgp_policies.go
index 859a5a3c..ff15b3a0 100644
--- a/pkg/controllers/routing/bgp_policies.go
+++ b/pkg/controllers/routing/bgp_policies.go
@@ -278,8 +278,8 @@ func (nrc *NetworkRoutingController) addServiceVIPsDefinedSet() error {
 // create a defined set to represent just the host default route
 func (nrc *NetworkRoutingController) addDefaultRouteDefinedSet() error {
 	for setName, defaultRoute := range map[string]string{
-		defaultRouteSet:   "0.0.0.0/0",
-		defaultRouteSetV6: "::/0",
+		defaultRouteSet:   utils.IPv4DefaultRoute,
+		defaultRouteSetV6: utils.IPv6DefaultRoute,
 	} {
 		currentDefinedSet, err := nrc.getDefinedSetFromGoBGP(setName, gobgpapi.DefinedType_PREFIX)
 		if err != nil {
diff --git a/pkg/routes/pbr.go b/pkg/routes/pbr.go
index 50b913b6..6947f900 100644
--- a/pkg/routes/pbr.go
+++ b/pkg/routes/pbr.go
@@ -2,15 +2,20 @@ package routes
 
 import (
 	"fmt"
-	"os/exec"
-	"strings"
+	"net"
 
 	"github.com/cloudnativelabs/kube-router/v2/pkg/utils"
+	"github.com/vishvananda/netlink"
+)
+
+const (
+	PBRRuleAdd = iota
+	PBRRuleDel
 )
 
 const (
 	// CustomTableID is the ID of the custom, iproute2 routing table that will be used for policy based routing
-	CustomTableID = "77"
+	CustomTableID = 77
 	// CustomTableName is the name of the custom, iproute2 routing table that will be used for policy based routing
 	CustomTableName = "kube-router"
 )
@@ -35,21 +40,54 @@ func NewPolicyBasedRules(nfa utils.NodeFamilyAware, podIPv4CIDRs, podIPv6CIDRs [
 // ipProtocol is the iproute2 protocol specified as a string ("-4" or "-6"). ipOp is the rule operation specified as a
 // string ("add" or "del). The cidr is the IPv4 / IPv6 source CIDR string that when received will be used to lookup
 // routes in a custom table.
-func ipRuleAbstraction(ipProtocol, ipOp, cidr string) error {
-	out, err := exec.Command("ip", ipProtocol, "rule", "list").Output()
+func ipRuleAbstraction(ipFamily int, ipOp int, cidr string) error {
+	_, nSrc, err := net.ParseCIDR(cidr)
+	if err != nil {
+		return fmt.Errorf("failed to parse CIDR: %s", err.Error())
+	}
+
+	nRule := netlink.NewRule()
+	nRule.Family = ipFamily
+	nRule.Src = nSrc
+	nRule.Table = CustomTableID
+
+	// If the rule that we are abstracting has either the src or dst set to a default route, then we need to handle it
+	// differently. For more information, see: https://github.com/vishvananda/netlink/issues/1080
+	// TODO: If the above issue is resolved, some of the below logic can be removed
+	rules := make([]netlink.Rule, 0)
+	isDefaultRoute, err := utils.IsDefaultRoute(nSrc)
 	if err != nil {
-		return fmt.Errorf("failed to verify if `ip rule` exists: %s", err.Error())
+		return fmt.Errorf("failed to check if CIDR is a default route: %v", err)
 	}
 
-	if strings.Contains(string(out), cidr) && ipOp == "del" {
-		err = exec.Command("ip", ipProtocol, "rule", ipOp, "from", cidr, "lookup", CustomTableID).Run()
+	if isDefaultRoute {
+		var tmpRules []netlink.Rule
+		tmpRules, err = netlink.RuleListFiltered(ipFamily, nRule, netlink.RT_FILTER_TABLE)
 		if err != nil {
-			return fmt.Errorf("failed to add ip rule due to: %s", err.Error())
+			return fmt.Errorf("failed to list rules: %s", err.Error())
 		}
-	} else if !strings.Contains(string(out), cidr) && ipOp == "add" {
-		err = exec.Command("ip", ipProtocol, "rule", ipOp, "from", cidr, "lookup", CustomTableID).Run()
+
+		// Check if one or more of the rules returned are a default route rule
+		for _, rule := range tmpRules {
+			// If the rule has no src, then it is a default route rule which is the match criteria for the rule
+			if rule.Src == nil {
+				rules = append(rules, rule)
+			}
+		}
+	} else {
+		rules, err = netlink.RuleListFiltered(ipFamily, nRule, netlink.RT_FILTER_SRC|netlink.RT_FILTER_TABLE)
 		if err != nil {
-			return fmt.Errorf("failed to add ip rule due to: %s", err.Error())
+			return fmt.Errorf("failed to list rules: %s", err.Error())
+		}
+	}
+
+	if ipOp == PBRRuleDel && len(rules) > 0 {
+		if err := netlink.RuleDel(nRule); err != nil {
+			return fmt.Errorf("failed to delete rule: %s", err.Error())
+		}
+	} else if ipOp == PBRRuleAdd && len(rules) < 1 {
+		if err := netlink.RuleAdd(nRule); err != nil {
+			return fmt.Errorf("failed to add rule: %s", err.Error())
 		}
 	}
 
@@ -66,14 +104,14 @@ func (pbr *PolicyBasedRules) Enable() error {
 
 	if pbr.nfa.IsIPv4Capable() {
 		for _, ipv4CIDR := range pbr.podIPv4CIDRs {
-			if err := ipRuleAbstraction("-4", "add", ipv4CIDR); err != nil {
+			if err := ipRuleAbstraction(netlink.FAMILY_V4, PBRRuleAdd, ipv4CIDR); err != nil {
 				return err
 			}
 		}
 	}
 	if pbr.nfa.IsIPv6Capable() {
 		for _, ipv6CIDR := range pbr.podIPv6CIDRs {
-			if err := ipRuleAbstraction("-6", "add", ipv6CIDR); err != nil {
+			if err := ipRuleAbstraction(netlink.FAMILY_V6, PBRRuleAdd, ipv6CIDR); err != nil {
 				return err
 			}
 		}
@@ -91,14 +129,14 @@ func (pbr *PolicyBasedRules) Disable() error {
 
 	if pbr.nfa.IsIPv4Capable() {
 		for _, ipv4CIDR := range pbr.podIPv4CIDRs {
-			if err := ipRuleAbstraction("-4", "del", ipv4CIDR); err != nil {
+			if err := ipRuleAbstraction(netlink.FAMILY_V4, PBRRuleDel, ipv4CIDR); err != nil {
 				return err
 			}
 		}
 	}
 	if pbr.nfa.IsIPv6Capable() {
 		for _, ipv6CIDR := range pbr.podIPv6CIDRs {
-			if err := ipRuleAbstraction("-6", "del", ipv6CIDR); err != nil {
+			if err := ipRuleAbstraction(netlink.FAMILY_V6, PBRRuleDel, ipv6CIDR); err != nil {
 				return err
 			}
 		}
diff --git a/pkg/tunnels/linux_tunnels.go b/pkg/tunnels/linux_tunnels.go
index 2c8a59e9..eab4fee9 100644
--- a/pkg/tunnels/linux_tunnels.go
+++ b/pkg/tunnels/linux_tunnels.go
@@ -1,7 +1,11 @@
+// Package tunnels provides functionality for setting up and managing overlay tunnels in Linux.
+// It includes support for both IPIP and FOU (Foo over Ethernet) encapsulation types.
+//
+// As much functionality as possible is done via the netlink library, however, FOU tunnels require using the iproute2
+// user space tooling since they are not currently supported by the netlink library.
 package tunnels
 
 import (
-	"bufio"
 	"crypto/sha256"
 	"fmt"
 	"net"
@@ -31,12 +35,32 @@ const (
 	// The maximum and minimum port numbers for encap ports
 	maxPort = uint16(65535)
 	minPort = uint16(1024)
+
+	// Unix tunnel encap types, unfortunately, these are not understood by the netlink library, so we need to use
+	// our own enums which as far as I can tell come from here:
+	//nolint:lll // URL is long
+	// https://github.com/iproute2/iproute2/blob/e6a170a9d4e75d206631da77e469813279c12134/include/uapi/linux/if_tunnel.h#L84-L89
+	UnixTunnelEncapTypeNone uint16 = 0
+	UnixTunnelEncapTypeFOU  uint16 = 1
+	UnixTunnelEncapTypeGUE  uint16 = 2
+	UnixTunnelEncapTypeMPLS uint16 = 3
 )
 
 var (
 	validEncapTypes = []EncapType{EncapTypeFOU, EncapTypeIPIP}
 )
 
+// tunnelConfig holds configuration for IP family-specific settings
+type tunnelConfig struct {
+	nextHopSubnet   *net.IPNet
+	bestIPForFamily net.IP
+	ipipMode        string
+	fouLinkType     string
+	isIPv6          bool
+	ipBase          []string
+	encapPortStr    string
+}
+
 // EncapType represents the type of encapsulation used for an overlay tunnel in kube-router.
 type EncapType string
 
@@ -106,144 +130,292 @@ func (o *OverlayTunnel) EncapPort() EncapPort {
 // setupOverlayTunnel attempts to create a tunnel link and corresponding routes for IPIP based overlay networks
 func (o *OverlayTunnel) SetupOverlayTunnel(tunnelName string, nextHop net.IP,
 	nextHopSubnet *net.IPNet) (netlink.Link, error) {
-	var out []byte
 	link, err := netlink.LinkByName(tunnelName)
+	if err != nil {
+		klog.Warningf("failed to get tunnel link by name %s: %v, will attempt to create it", tunnelName, err)
+		link = nil
+	}
+
+	// Determine IP family and configuration
+	config, err := o.createTunnelConfig(nextHop, nextHopSubnet)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check if tunnel needs recreation due to encap type mismatch
+	link, err = o.applyTunnelConfig(link, tunnelName, nextHop, config)
+	if err != nil {
+		return nil, err
+	}
+
+	// Add route for the tunnel
+	if err := o.addTunnelRoute(link, nextHop, config.isIPv6); err != nil {
+		return nil, err
+	}
+
+	return link, nil
+}
 
-	var bestIPForFamily net.IP
-	var ipipMode, fouLinkType string
-	isIPv6 := false
-	ipBase := make([]string, 0)
-	strFormattedEncapPort := strconv.FormatInt(int64(o.encapPort), 10)
+// createTunnelConfig determines the IP family and returns appropriate configuration
+func (o *OverlayTunnel) createTunnelConfig(nextHop net.IP, nextHopSubnet *net.IPNet) (*tunnelConfig, error) {
+	config := &tunnelConfig{
+		ipBase:        make([]string, 0),
+		encapPortStr:  strconv.FormatInt(int64(o.encapPort), 10),
+		nextHopSubnet: nextHopSubnet,
+	}
 
 	if nextHop.To4() != nil {
-		bestIPForFamily = o.krNode.FindBestIPv4NodeAddress()
-		ipipMode = ipipIPv4Mode
-		fouLinkType = fouIPv4LinkMode
+		config.bestIPForFamily = o.krNode.FindBestIPv4NodeAddress()
+		config.ipipMode = ipipIPv4Mode
+		config.fouLinkType = fouIPv4LinkMode
+		config.isIPv6 = false
 	} else {
-		// Need to activate the ip command in IPv6 mode
-		ipBase = append(ipBase, "-6")
-		bestIPForFamily = o.krNode.FindBestIPv6NodeAddress()
-		ipipMode = ipipIPv6Mode
-		fouLinkType = fouIPv6LinkMode
-		isIPv6 = true
-	}
-	if nil == bestIPForFamily {
+		config.ipBase = append(config.ipBase, "-6")
+		config.bestIPForFamily = o.krNode.FindBestIPv6NodeAddress()
+		config.ipipMode = ipipIPv6Mode
+		config.fouLinkType = fouIPv6LinkMode
+		config.isIPv6 = true
+	}
+
+	if config.bestIPForFamily == nil {
 		return nil, fmt.Errorf("not able to find an appropriate configured IP address on node for destination "+
 			"IP family: %s", nextHop.String())
 	}
 
-	// This indicated that the tunnel already exists, so it's possible that there might be nothing more needed. However,
-	// it is also possible that the user changed the encap type, so we need to make sure that the encap type matches
-	// and if it doesn't, create it
-	recreate := false
-	if err == nil {
-		klog.V(1).Infof("Tunnel interface: %s with encap type %s for the node %s already exists.",
-			tunnelName, link.Attrs().EncapType, nextHop.String())
-
-		switch o.encapType {
-		case EncapTypeIPIP:
-			if linkFOUEnabled(tunnelName) {
-				klog.Infof("Was configured to use ipip tunnels, but found existing fou tunnels in place, cleaning up")
-				recreate = true
-
-				// Even though we are setup for IPIP tunels we have existing tunnels that are FoU tunnels, remove them
-				// so that we can recreate them as IPIP
-				CleanupTunnel(nextHopSubnet, tunnelName)
-
-				// If we are transitioning from FoU to IPIP we also need to clean up the old FoU port if it exists
-				if fouPortAndProtoExist(o.encapPort, isIPv6) {
-					fouArgs := ipBase
-					fouArgs = append(fouArgs, "fou", "del", "port", strFormattedEncapPort)
-					out, err := exec.Command("ip", fouArgs...).CombinedOutput()
-					if err != nil {
-						klog.Warningf("failed to clean up previous FoU tunnel port (this is only a warning because it "+
-							"won't stop kube-router from working for now, but still shouldn't have happened) - error: "+
-							"%v, output %s", err, out)
-					}
-				}
-			}
-		case EncapTypeFOU:
-			if !linkFOUEnabled(tunnelName) {
-				klog.Infof("Was configured to use fou tunnels, but found existing ipip tunnels in place, cleaning up")
-				recreate = true
-				// Even though we are setup for FoU tunels we have existing tunnels that are IPIP tunnels, remove them
-				// so that we can recreate them as IPIP
-				CleanupTunnel(nextHopSubnet, tunnelName)
-			}
-		}
+	return config, nil
+}
+
+// applyTunnelConfig ensures that the existing tunnel matches the desired configuration and cleans up any old tunnels
+// that do not match the config
+func (o *OverlayTunnel) applyTunnelConfig(link netlink.Link, tunnelName string, nextHop net.IP,
+	config *tunnelConfig) (netlink.Link, error) {
+
+	var recreate bool
+	switch o.encapType {
+	case EncapTypeIPIP:
+		recreate = o.checkIPIPTunnelRecreation(tunnelName, config)
+	case EncapTypeFOU:
+		recreate = o.checkFOUTunnelRecreation(tunnelName, config)
+	default:
+		return nil, fmt.Errorf("unknown tunnel encapsulation was passed: %s, unable to continue with overlay "+
+			"setup", o.encapType)
 	}
 
-	// an error here indicates that the tunnel didn't exist, so we need to create it, if it already exists there's
-	// nothing to do here
-	if err != nil || recreate {
-		klog.Infof("Creating tunnel %s of type %s with encap %s for destination %s",
-			tunnelName, fouLinkType, o.encapType, nextHop.String())
-		cmdArgs := ipBase
-		switch o.encapType {
-		case EncapTypeIPIP:
-			// Plain IPIP tunnel without any encapsulation
-			cmdArgs = append(cmdArgs, "tunnel", "add", tunnelName, "mode", ipipMode, "local", bestIPForFamily.String(),
-				"remote", nextHop.String())
-
-		case EncapTypeFOU:
-			// Ensure that the FOU tunnel port is set correctly
-			if !fouPortAndProtoExist(o.encapPort, isIPv6) {
-				fouArgs := ipBase
-				fouArgs = append(fouArgs, "fou", "add", "port", strFormattedEncapPort, "gue")
-				out, err := exec.Command("ip", fouArgs...).CombinedOutput()
-				if err != nil {
-					return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
-						"Failed to set FoU tunnel port - error: %s, output: %s", tunnelName, err, string(out))
-				}
-			}
-
-			// Prep IPIP tunnel for FOU encapsulation
-			cmdArgs = append(cmdArgs, "link", "add", "name", tunnelName, "type", fouLinkType, "remote", nextHop.String(),
-				"local", bestIPForFamily.String(), "ttl", "225", "encap", "gue", "encap-sport", "auto", "encap-dport",
-				strFormattedEncapPort, "mode", ipipMode)
-
-		default:
-			return nil, fmt.Errorf("unknown tunnel encapsulation was passed: %s, unable to continue with overlay "+
-				"setup", o.encapType)
-		}
+	// If the link doesn't exist here, then it means that it likely doesn't exist, or we encountered a random error when
+	// we originally tried to get it. In either case, we need to create a new tunnel.
+	if link == nil || recreate {
+		return o.createTunnel(tunnelName, nextHop, config)
+	}
 
-		klog.V(2).Infof("Executing the following command to create tunnel: ip %s", cmdArgs)
-		out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
+	return link, nil
+}
+
+// checkIPIPTunnelRecreation checks if IPIP tunnel needs recreation and cleans up any old tunnels
+func (o *OverlayTunnel) checkIPIPTunnelRecreation(tunnelName string, config *tunnelConfig) bool {
+	fouEnabled, err := linkFOUEnabled(tunnelName)
+	if err != nil || fouEnabled {
 		if err != nil {
-			return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
-				"Failed to create tunnel interface %s. error: %s, output: %s",
-				nextHop, tunnelName, err, string(out))
+			klog.Infof("failed to check if fou is enabled on the link %s: %v, going to try to clean up and "+
+				"recreate the tunnel anyway", tunnelName, err)
+		} else {
+			klog.Infof("Was configured to use ipip tunnels, but found existing fou tunnels in place, " +
+				"cleaning up")
 		}
 
-		link, err = netlink.LinkByName(tunnelName)
-		if err != nil {
-			return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
-				"Failed to get tunnel interface by name error: %s", tunnelName, err)
+		// Clean up existing FOU tunnel
+		CleanupTunnel(config.nextHopSubnet, tunnelName)
+
+		// Clean up old FOU port if transitioning from FOU to IPIP
+		if fouPortAndProtoExist(o.encapPort, config.isIPv6) {
+			o.cleanupFOUPort(config)
 		}
-		if err = netlink.LinkSetUp(link); err != nil {
-			return nil, fmt.Errorf("failed to bring tunnel interface %s up due to: %v", tunnelName, err)
+
+		return true
+	}
+	return false
+}
+
+// checkFOUTunnelRecreation checks if FOU tunnel needs recreation and cleans up any old tunnels
+func (o *OverlayTunnel) checkFOUTunnelRecreation(tunnelName string, config *tunnelConfig) bool {
+	fouEnabled, err := linkFOUEnabled(tunnelName)
+	if err != nil || !fouEnabled {
+		if err != nil {
+			klog.Errorf("failed to check if fou is enabled on the link %s: %v, going to try to clean up and "+
+				"recreate the tunnel anyway", tunnelName, err)
+		} else {
+			klog.Infof("Was configured to use fou tunnels, but found existing ipip tunnels in place, " +
+				"cleaning up")
 		}
+
+		// Clean up existing IPIP tunnel
+		CleanupTunnel(config.nextHopSubnet, tunnelName)
+		return true
 	}
+	return false
+}
 
-	// Now that the tunnel link exists, we need to add a route to it, so the node knows where to send traffic bound for
-	// this interface
-	//nolint:gocritic // we understand that we are appending to a new slice
-	cmdArgs := append(ipBase, "route", "list", "table", routes.CustomTableID)
-	out, err = exec.Command("ip", cmdArgs...).CombinedOutput()
-	// This used to be "dev "+tunnelName+" scope" but this isn't consistent with IPv6's output, so we changed it to just
-	// "dev "+tunnelName, but at this point I'm unsure if there was a good reason for adding scope on before, so that's
-	// why this comment is here.
-	if err != nil || !strings.Contains(string(out), "dev "+tunnelName) {
-		//nolint:gocritic // we understand that we are appending to a new slice
-		cmdArgs = append(ipBase, "route", "add", nextHop.String(), "dev", tunnelName, "table", routes.CustomTableID)
-		if out, err = exec.Command("ip", cmdArgs...).CombinedOutput(); err != nil {
-			return nil, fmt.Errorf("failed to add route in custom route table, err: %s, output: %s", err, string(out))
+// cleanupFOUPort removes the FOU port configuration
+func (o *OverlayTunnel) cleanupFOUPort(config *tunnelConfig) {
+	fouArgs := config.ipBase
+	fouArgs = append(fouArgs, "fou", "del", "port", config.encapPortStr)
+	out, err := exec.Command("ip", fouArgs...).CombinedOutput()
+	if err != nil {
+		klog.Warningf("failed to clean up previous FoU tunnel port (this is only a warning because it "+
+			"won't stop kube-router from working for now, but still shouldn't have happened) - error: "+
+			"%v, output %s", err, out)
+	}
+}
+
+// createTunnel creates a new tunnel based on the encapsulation type
+func (o *OverlayTunnel) createTunnel(tunnelName string, nextHop net.IP, config *tunnelConfig) (netlink.Link, error) {
+	klog.Infof("Creating tunnel %s with encap %s for destination %s",
+		tunnelName, o.encapType, nextHop.String())
+
+	switch o.encapType {
+	case EncapTypeIPIP:
+		return o.createIPIPTunnel(tunnelName, nextHop, config)
+	case EncapTypeFOU:
+		return o.createFOUTunnel(tunnelName, nextHop, config)
+	default:
+		return nil, fmt.Errorf("unknown tunnel encapsulation was passed: %s, unable to continue with overlay "+
+			"setup", o.encapType)
+	}
+}
+
+// createIPIPTunnel creates an IPIP tunnel using netlink
+func (o *OverlayTunnel) createIPIPTunnel(tunnelName string, nextHop net.IP,
+	config *tunnelConfig) (netlink.Link, error) {
+	var tunnelLink netlink.Link
+	if config.isIPv6 {
+		tunnelLink = &netlink.Ip6tnl{
+			LinkAttrs: netlink.LinkAttrs{Name: tunnelName},
+			Local:     config.bestIPForFamily,
+			Remote:    nextHop,
 		}
+	} else {
+		tunnelLink = &netlink.Iptun{
+			LinkAttrs: netlink.LinkAttrs{Name: tunnelName},
+			Local:     config.bestIPForFamily,
+			Remote:    nextHop,
+		}
+	}
+
+	if err := netlink.LinkAdd(tunnelLink); err != nil {
+		return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
+			"Failed to create tunnel interface %s. error: %v", nextHop, tunnelName, err)
+	}
+
+	return o.bringTunnelUp(tunnelName)
+}
+
+// createFOUTunnel creates a FOU tunnel
+func (o *OverlayTunnel) createFOUTunnel(tunnelName string, nextHop net.IP, config *tunnelConfig) (netlink.Link, error) {
+	// Ensure FOU port exists
+	if err := o.ensureFOUPort(config); err != nil {
+		return nil, err
+	}
+
+	// Create FOU tunnel using iproute2
+	if err := o.createFOUTunnelWithIPRoute2(tunnelName, nextHop, config); err != nil {
+		return nil, err
+	}
+
+	return o.bringTunnelUp(tunnelName)
+}
+
+// ensureFOUPort ensures the FOU port is configured
+func (o *OverlayTunnel) ensureFOUPort(config *tunnelConfig) error {
+	if fouPortAndProtoExist(o.encapPort, config.isIPv6) {
+		return nil
+	}
+
+	var family int
+	if config.isIPv6 {
+		family = netlink.FAMILY_V6
+	} else {
+		family = netlink.FAMILY_V4
+	}
+
+	fouPort := &netlink.Fou{
+		Family:    family,
+		Port:      int(o.encapPort),
+		EncapType: netlink.FOU_ENCAP_GUE,
+	}
+
+	if err := netlink.FouAdd(*fouPort); err != nil {
+		return fmt.Errorf("failed to set FoU tunnel port - error: %v", err)
+	}
+
+	return nil
+}
+
+// createFOUTunnelWithIPRoute2 creates FOU tunnel using iproute2 command. While it would be nice to be able to do this
+// via netlink, the netlink library does not currently support creating secondary encap tunnels (IPIP over GUE). So for
+// now we have to use the iproute2 user-space tooling.
+func (o *OverlayTunnel) createFOUTunnelWithIPRoute2(tunnelName string, nextHop net.IP, config *tunnelConfig) error {
+	cmdArgs := config.ipBase
+	cmdArgs = append(cmdArgs, "link", "add", "name", tunnelName, "type", config.fouLinkType, "remote", nextHop.String(),
+		"local", config.bestIPForFamily.String(), "ttl", "225", "encap", "gue", "encap-sport", "auto", "encap-dport",
+		config.encapPortStr, "mode", config.ipipMode)
+
+	klog.V(2).Infof("Executing the following command to create tunnel: ip %s", cmdArgs)
+	out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("route not injected for the route advertised by the node %s "+
+			"Failed to create tunnel interface %s. error: %s, output: %s",
+			nextHop, tunnelName, err, string(out))
+	}
+
+	return nil
+}
+
+// bringTunnelUp brings the tunnel interface up
+func (o *OverlayTunnel) bringTunnelUp(tunnelName string) (netlink.Link, error) {
+	link, err := netlink.LinkByName(tunnelName)
+	if err != nil {
+		return nil, fmt.Errorf("route not injected for the route advertised by the node %s "+
+			"Failed to get tunnel interface by name error: %s", tunnelName, err)
+	}
+
+	if err = netlink.LinkSetUp(link); err != nil {
+		return nil, fmt.Errorf("failed to bring tunnel interface %s up due to: %v", tunnelName, err)
 	}
 
 	return link, nil
 }
 
+// addTunnelRoute adds a route for the tunnel in the custom table. This is necessary to ensure that the tunnel is used
+// for all traffic destined for the tunnel's destination.
+func (o *OverlayTunnel) addTunnelRoute(link netlink.Link, nextHop net.IP, isIPv6 bool) error {
+	routeFamily := netlink.FAMILY_V4
+	if isIPv6 {
+		routeFamily = netlink.FAMILY_V6
+	}
+
+	route := &netlink.Route{
+		Family:    routeFamily,
+		LinkIndex: link.Attrs().Index,
+		Table:     routes.CustomTableID,
+		Dst:       utils.GetSingleIPNet(nextHop),
+	}
+
+	routeList, err := netlink.RouteListFiltered(routeFamily, route,
+		netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_DST)
+	if err != nil {
+		return fmt.Errorf("failed to list routes in custom table: %v", err)
+	}
+
+	if len(routeList) < 1 {
+		if err = netlink.RouteAdd(route); err != nil {
+			return fmt.Errorf("failed to add route in custom route table, err: %v", err)
+		}
+	} else {
+		klog.V(2).Infof("Route for %s already exists in custom table", nextHop.String())
+	}
+
+	return nil
+}
+
 // cleanupTunnel removes any traces of tunnels / routes that were setup by nrc.setupOverlayTunnel() and are no longer
 // needed. All errors are logged only, as we want to attempt to perform all cleanup actions regardless of their success
 func CleanupTunnel(destinationSubnet *net.IPNet, tunnelName string) {
@@ -283,48 +455,24 @@ func GenerateTunnelName(nodeIP string) string {
 
 // fouPortAndProtoExist checks to see if the given FoU port is already configured on the system via iproute2
 // tooling for the given protocol
-//
-// fou show, shows both IPv4 and IPv6 ports in the same show command, they look like:
-// port 5556 gue
-// port 5556 gue -6
-// where the only thing that distinguishes them is the -6 or not on the end
-// WARNING we're parsing a CLI tool here not an API, this may break at some point in the future
 func fouPortAndProtoExist(port EncapPort, isIPv6 bool) bool {
-	const ipRoute2IPv6Prefix = "-6"
 	strPort := strconv.FormatInt(int64(port), 10)
-	fouArgs := make([]string, 0)
 	klog.V(2).Infof("Checking FOU Port and Proto... %s - %t", strPort, isIPv6)
 
+	nFamily := netlink.FAMILY_V4
 	if isIPv6 {
-		fouArgs = append(fouArgs, ipRoute2IPv6Prefix)
+		nFamily = netlink.FAMILY_V6
 	}
-	fouArgs = append(fouArgs, "fou", "show")
 
-	out, err := exec.Command("ip", fouArgs...).CombinedOutput()
-	// iproute2 returns an error if no fou configuration exists
+	fList, err := netlink.FouList(nFamily)
 	if err != nil {
+		klog.Warningf("failed to list fou ports: %v", err)
 		return false
 	}
 
-	strOut := string(out)
-	klog.V(2).Infof("Combined output of ip fou show: %s", strOut)
-	scanner := bufio.NewScanner(strings.NewReader(strOut))
-
-	// loop over all lines of output
-	for scanner.Scan() {
-		scannedLine := scanner.Text()
-		// if the output doesn't contain our port at all, then continue
-		if !strings.Contains(scannedLine, strPort) {
-			continue
-		}
-
-		// if this is IPv6 port and it has the correct IPv6 suffix (see example above) then return true
-		if isIPv6 && strings.HasSuffix(scannedLine, ipRoute2IPv6Prefix) {
-			return true
-		}
-
-		// if this is not IPv6 and it does not have an IPv6 suffix (see example above) then return true
-		if !isIPv6 && !strings.HasSuffix(scannedLine, ipRoute2IPv6Prefix) {
+	for _, fou := range fList {
+		klog.V(2).Infof("Found fou port: %v", fou)
+		if fou.Port == int(port) && fou.Family == nFamily {
 			return true
 		}
 	}
@@ -335,26 +483,28 @@ func fouPortAndProtoExist(port EncapPort, isIPv6 bool) bool {
 // linkFOUEnabled checks to see whether the given link has FoU (Foo over Ethernet) enabled on it, specifically since
 // kube-router only works with GUE (Generic UDP Encapsulation) we look for that and not just FoU in general. If the
 // linkName is enabled with FoU GUE then we return true, otherwise false
-//
-// Output for a FoU Enabled GUE tunnel looks like:
-// ipip ipip remote <ip> local <ip> dev <dev> ttl 225 pmtudisc encap gue encap-sport auto encap-dport 5555 ...
-// Output for a normal IPIP tunnel looks like:
-// ipip ipip remote <ip> local <ip> dev <dev> ttl inherit ...
-func linkFOUEnabled(linkName string) bool {
-	const fouEncapEnabled = "encap gue"
-	cmdArgs := []string{"-details", "link", "show", linkName}
-
-	out, err := exec.Command("ip", cmdArgs...).CombinedOutput()
-
+func linkFOUEnabled(linkName string) (bool, error) {
+	link, err := netlink.LinkByName(linkName)
 	if err != nil {
-		klog.Warningf("recevied an error while trying to look at the link details of %s, this shouldn't have happened",
-			linkName)
-		return false
+		return false, fmt.Errorf("failed to get link by name: %v", err)
 	}
 
-	if strings.Contains(string(out), fouEncapEnabled) {
-		return true
+	switch link := link.(type) {
+	case *netlink.Iptun:
+		klog.V(2).Infof("Link %s is an IPTun with encap type: %d and encap dport: %d",
+			linkName, link.EncapType, link.EncapDport)
+		if link.EncapType == UnixTunnelEncapTypeGUE {
+			return true, nil
+		}
+	case *netlink.Ip6tnl:
+		klog.V(2).Infof("Link %s is an IP6Tun with encap type: %d and encap dport: %d",
+			linkName, link.EncapType, link.EncapDport)
+		if link.EncapType == UnixTunnelEncapTypeGUE {
+			return true, nil
+		}
+	default:
+		return false, fmt.Errorf("link %s is not an IPTun or IP6Tun, this is not expected", linkName)
 	}
 
-	return false
+	return false, nil
 }
diff --git a/pkg/utils/ip.go b/pkg/utils/ip.go
new file mode 100644
index 00000000..f90eaefc
--- /dev/null
+++ b/pkg/utils/ip.go
@@ -0,0 +1,117 @@
+package utils
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+)
+
+const (
+	IPv4DefaultRoute = "0.0.0.0/0"
+	IPv6DefaultRoute = "::/0"
+
+	ipv4NetMaskBits = 32
+	ipv6NetMaskBits = 128
+)
+
+// GetSingleIPNet returns an IPNet object that represents a subnet containing a single IP address for a given IP address
+// with proper handling for IPv4 and IPv6 addresses.
+func GetSingleIPNet(ip net.IP) *net.IPNet {
+	if ip.To4() != nil {
+		return &net.IPNet{
+			IP:   ip,
+			Mask: net.CIDRMask(ipv4NetMaskBits, ipv4NetMaskBits),
+		}
+	} else {
+		return &net.IPNet{
+			IP:   ip,
+			Mask: net.CIDRMask(ipv6NetMaskBits, ipv6NetMaskBits),
+		}
+	}
+}
+
+// GetIPv4NetMaxMaskBits returns the maximum mask bits for an IPv4 address
+func GetIPv4NetMaxMaskBits() uint32 {
+	return ipv4NetMaskBits
+}
+
+// GetIPv6NetMaxMaskBits returns the maximum mask bits for an IPv6 address
+func GetIPv6NetMaxMaskBits() uint32 {
+	return ipv6NetMaskBits
+}
+
+// ContainsIPv4Address checks a given string array to see if it contains a valid IPv4 address within it
+func ContainsIPv4Address(addrs []string) bool {
+	for _, addr := range addrs {
+		ip := net.ParseIP(addr)
+		if ip == nil {
+			continue
+		}
+		if ip.To4() != nil {
+			return true
+		}
+	}
+	return false
+}
+
+// ContainsIPv6Address checks a given string array to see if it contains a valid IPv6 address within it
+func ContainsIPv6Address(addrs []string) bool {
+	for _, addr := range addrs {
+		ip := net.ParseIP(addr)
+		if ip == nil {
+			continue
+		}
+		if ip.To4() != nil {
+			continue
+		}
+		if ip.To16() != nil {
+			return true
+		}
+	}
+	return false
+}
+
+// GetDefaultIPv4Route returns the default IPv4 route
+func GetDefaultIPv4Route() *net.IPNet {
+	_, defaultPrefixCIDR, err := net.ParseCIDR(IPv4DefaultRoute)
+	if err != nil {
+		return nil
+	}
+	return defaultPrefixCIDR
+}
+
+// GetDefaultIPv6Route returns the default IPv6 route
+func GetDefaultIPv6Route() *net.IPNet {
+	_, defaultPrefixCIDR, err := net.ParseCIDR(IPv6DefaultRoute)
+	if err != nil {
+		return nil
+	}
+	return defaultPrefixCIDR
+}
+
+// IPNetEqual checks if two IPNet objects are equal by comparing the IP and Mask
+func IPNetEqual(a, b *net.IPNet) bool {
+	if a == nil || b == nil {
+		return a == b
+	}
+	return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
+}
+
+// IsDefaultRoute checks if a given CIDR is a default route by comparing it to the default routes for IPv4 and IPv6
+func IsDefaultRoute(cidr *net.IPNet) (bool, error) {
+	var defaultPrefixCIDR *net.IPNet
+	var err error
+
+	if cidr.IP.To4() != nil {
+		_, defaultPrefixCIDR, err = net.ParseCIDR(IPv4DefaultRoute)
+		if err != nil {
+			return false, fmt.Errorf("failed to parse default route: %s", err.Error())
+		}
+	} else {
+		_, defaultPrefixCIDR, err = net.ParseCIDR(IPv6DefaultRoute)
+		if err != nil {
+			return false, fmt.Errorf("failed to parse default route: %s", err.Error())
+		}
+	}
+	return IPNetEqual(defaultPrefixCIDR, cidr), nil
+}
diff --git a/pkg/utils/linux_routing.go b/pkg/utils/linux_routing.go
index fa6cf99b..84711329 100644
--- a/pkg/utils/linux_routing.go
+++ b/pkg/utils/linux_routing.go
@@ -28,7 +28,7 @@ type LocalLinkQuerier interface {
 }
 
 // RouteTableAdd adds a new named table to iproute's rt_tables configuration file
-func RouteTableAdd(tableNumber, tableName string) error {
+func RouteTableAdd(tableNumber int, tableName string) error {
 	var rtTablesLoc string
 	for _, possibleLoc := range rtTablesPosLoc {
 		_, err := os.Stat(possibleLoc)
@@ -37,6 +37,7 @@ func RouteTableAdd(tableNumber, tableName string) error {
 			continue
 		}
 		rtTablesLoc = possibleLoc
+		break
 	}
 	if rtTablesLoc == "" {
 		return fmt.Errorf("did not find rt_tables in any of the expected locations: %s", rtTablesFileName)
@@ -53,7 +54,7 @@ func RouteTableAdd(tableNumber, tableName string) error {
 			return fmt.Errorf("failed to open: %s", err.Error())
 		}
 		defer CloseCloserDisregardError(f)
-		if _, err = f.WriteString(tableNumber + " " + tableName + "\n"); err != nil {
+		if _, err = f.WriteString(fmt.Sprint(tableNumber) + " " + tableName + "\n"); err != nil {
 			return fmt.Errorf("failed to write: %s", err.Error())
 		}
 	}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 490b2fc2..2eaf0543 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -47,37 +47,6 @@ func CloseCloserDisregardError(handler io.Closer) {
 	_ = handler.Close()
 }
 
-// ContainsIPv4Address checks a given string array to see if it contains a valid IPv4 address within it
-func ContainsIPv4Address(addrs []string) bool {
-	for _, addr := range addrs {
-		ip := net.ParseIP(addr)
-		if ip == nil {
-			continue
-		}
-		if ip.To4() != nil {
-			return true
-		}
-	}
-	return false
-}
-
-// ContainsIPv6Address checks a given string array to see if it contains a valid IPv6 address within it
-func ContainsIPv6Address(addrs []string) bool {
-	for _, addr := range addrs {
-		ip := net.ParseIP(addr)
-		if ip == nil {
-			continue
-		}
-		if ip.To4() != nil {
-			continue
-		}
-		if ip.To16() != nil {
-			return true
-		}
-	}
-	return false
-}
-
 // SliceContainsString checks to see if needle is contained within haystack, returns true if found, otherwise
 // returns false
 func SliceContainsString(needle string, haystack []string) bool {


Test patch:
diff --git a/pkg/utils/ip_test.go b/pkg/utils/ip_test.go
new file mode 100644
index 00000000..5027c751
--- /dev/null
+++ b/pkg/utils/ip_test.go
@@ -0,0 +1,344 @@
+package utils
+
+import (
+	"net"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIP_Equal(t *testing.T) {
+	tests := []struct {
+		name     string
+		ip1      net.IP
+		ip2      net.IP
+		expected bool
+	}{
+		{
+			name:     "IPv4 equal",
+			ip1:      net.IPv4(192, 168, 1, 1),
+			ip2:      net.IPv4(192, 168, 1, 1),
+			expected: true,
+		},
+		{
+			name:     "IPv4 not equal",
+			ip1:      net.IPv4(192, 168, 1, 1),
+			ip2:      net.IPv4(192, 168, 1, 2),
+			expected: false,
+		},
+		{
+			name:     "IPv4 mapped IPv6 equal to IPv4",
+			ip1:      net.IPv4(192, 168, 1, 1),
+			ip2:      net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1},
+			expected: true,
+		},
+		{
+			name:     "nil IPs equal",
+			ip1:      nil,
+			ip2:      nil,
+			expected: true,
+		},
+		{
+			name:     "nil and non-nil IP not equal",
+			ip1:      nil,
+			ip2:      net.IPv4(192, 168, 1, 1),
+			expected: false,
+		},
+		{
+			name:     "IPv6 equal",
+			ip1:      net.IPv6loopback,
+			ip2:      net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.ip1.Equal(tt.ip2))
+		})
+	}
+}
+
+func TestIP_To4(t *testing.T) {
+	tests := []struct {
+		name     string
+		ip       net.IP
+		expected net.IP
+	}{
+		{
+			name:     "Valid IPv4",
+			ip:       net.IPv4(192, 168, 1, 1),
+			expected: net.IP{192, 168, 1, 1},
+		},
+		{
+			name:     "IPv4-mapped IPv6",
+			ip:       net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1},
+			expected: net.IP{192, 168, 1, 1},
+		},
+		{
+			name:     "Pure IPv6",
+			ip:       net.IPv6loopback,
+			expected: nil,
+		},
+		{
+			name:     "nil IP",
+			ip:       nil,
+			expected: nil,
+		},
+		{
+			name:     "Invalid length IP",
+			ip:       net.IP{1, 2, 3},
+			expected: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.ip.To4())
+		})
+	}
+}
+
+func TestIPNet_Contains(t *testing.T) {
+	tests := []struct {
+		name     string
+		network  *net.IPNet
+		ip       net.IP
+		expected bool
+	}{
+		{
+			name: "IPv4 in network",
+			network: &net.IPNet{
+				IP:   net.IPv4(192, 168, 1, 0),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			ip:       net.IPv4(192, 168, 1, 1),
+			expected: true,
+		},
+		{
+			name: "IPv4 not in network",
+			network: &net.IPNet{
+				IP:   net.IPv4(192, 168, 1, 0),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			ip:       net.IPv4(192, 168, 2, 1),
+			expected: false,
+		},
+		{
+			name: "IPv6 in network",
+			network: &net.IPNet{
+				IP:   net.IPv6loopback,
+				Mask: net.CIDRMask(128, 128),
+			},
+			ip:       net.IPv6loopback,
+			expected: true,
+		},
+		{
+			name: "Mismatched IP versions",
+			network: &net.IPNet{
+				IP:   net.IPv4(192, 168, 1, 0),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+			ip:       net.IPv6loopback,
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.network.Contains(tt.ip))
+		})
+	}
+}
+
+func TestParseCIDR(t *testing.T) {
+	tests := []struct {
+		name        string
+		cidr        string
+		expectError bool
+		expectedIP  net.IP
+		expectedNet *net.IPNet
+	}{
+		{
+			name:        "Valid IPv4 CIDR",
+			cidr:        "192.168.1.0/24",
+			expectError: false,
+			expectedIP:  net.IPv4(192, 168, 1, 0),
+			expectedNet: &net.IPNet{
+				IP:   net.IPv4(192, 168, 1, 0),
+				Mask: net.IPv4Mask(255, 255, 255, 0),
+			},
+		},
+		{
+			name:        "Invalid CIDR format",
+			cidr:        "192.168.1.0",
+			expectError: true,
+		},
+		{
+			name:        "Invalid prefix length",
+			cidr:        "192.168.1.0/33",
+			expectError: true,
+		},
+		{
+			name:        "Invalid IP address",
+			cidr:        "300.168.1.0/24",
+			expectError: true,
+		},
+		{
+			name:        "Empty string",
+			cidr:        "",
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			ip, ipNet, err := net.ParseCIDR(tt.cidr)
+			if tt.expectError {
+				assert.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+			assert.True(t, tt.expectedIP.Equal(ip))
+			assert.True(t, tt.expectedNet.IP.Equal(ipNet.IP))
+			assert.Equal(t, tt.expectedNet.Mask, ipNet.Mask)
+		})
+	}
+}
+
+func TestIP_DefaultMask(t *testing.T) {
+	tests := []struct {
+		name     string
+		ip       net.IP
+		expected net.IPMask
+	}{
+		{
+			name:     "Class A",
+			ip:       net.IPv4(10, 0, 0, 0),
+			expected: net.IPv4Mask(255, 0, 0, 0),
+		},
+		{
+			name:     "Class B",
+			ip:       net.IPv4(172, 16, 0, 0),
+			expected: net.IPv4Mask(255, 255, 0, 0),
+		},
+		{
+			name:     "Class C",
+			ip:       net.IPv4(192, 168, 0, 0),
+			expected: net.IPv4Mask(255, 255, 255, 0),
+		},
+		{
+			name:     "IPv6 address",
+			ip:       net.IPv6loopback,
+			expected: nil,
+		},
+		{
+			name:     "nil IP",
+			ip:       nil,
+			expected: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.ip.DefaultMask())
+		})
+	}
+}
+
+func TestIP_IsPrivate(t *testing.T) {
+	tests := []struct {
+		name     string
+		ip       net.IP
+		expected bool
+	}{
+		{
+			name:     "Private IPv4 10.x.x.x",
+			ip:       net.IPv4(10, 0, 0, 1),
+			expected: true,
+		},
+		{
+			name:     "Private IPv4 172.16.x.x",
+			ip:       net.IPv4(172, 16, 0, 1),
+			expected: true,
+		},
+		{
+			name:     "Private IPv4 192.168.x.x",
+			ip:       net.IPv4(192, 168, 0, 1),
+			expected: true,
+		},
+		{
+			name:     "Public IPv4",
+			ip:       net.IPv4(8, 8, 8, 8),
+			expected: false,
+		},
+		{
+			name:     "Private IPv6 fc00::/7",
+			ip:       net.IP{0xfc, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+			expected: true,
+		},
+		{
+			name:     "Public IPv6",
+			ip:       net.IPv6loopback,
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.ip.IsPrivate())
+		})
+	}
+}
+
+func TestGetSingleIPNet(t *testing.T) {
+	tests := []struct {
+		name     string
+		ip       net.IP
+		expected *net.IPNet
+	}{
+		{
+			name: "IPv4 address",
+			ip:   net.IPv4(192, 168, 1, 1),
+			expected: &net.IPNet{
+				IP:   net.IPv4(192, 168, 1, 1),
+				Mask: net.CIDRMask(32, 32),
+			},
+		},
+		{
+			name: "IPv4-mapped IPv6 address",
+			ip:   net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1},
+			expected: &net.IPNet{
+				IP:   net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 168, 1, 1},
+				Mask: net.CIDRMask(32, 32),
+			},
+		},
+		{
+			name: "IPv6 address",
+			ip:   net.IPv6loopback,
+			expected: &net.IPNet{
+				IP:   net.IPv6loopback,
+				Mask: net.CIDRMask(128, 128),
+			},
+		},
+		{
+			name: "Another IPv6 address",
+			ip:   net.IP{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+			expected: &net.IPNet{
+				IP:   net.IP{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
+				Mask: net.CIDRMask(128, 128),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := GetSingleIPNet(tt.ip)
+			require.NotNil(t, result)
+			assert.True(t, tt.expected.IP.Equal(result.IP))
+			assert.Equal(t, tt.expected.Mask, result.Mask)
+		})
+	}
+}