Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dual stack support #692

Merged
merged 4 commits into from
Mar 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion images/kindnetd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# first stage build kindnetd binary
# NOTE: tentatively follow upstream kubernetes go version based on k8s in go.mod
FROM golang:1.13
FROM golang:1.15
aojea marked this conversation as resolved.
Show resolved Hide resolved
WORKDIR /go/src
# make deps fetching cacheable
COPY go.mod go.sum ./
Expand Down
48 changes: 30 additions & 18 deletions images/kindnetd/cmd/kindnetd/cni.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,39 @@ import (
"github.com/pkg/errors"

corev1 "k8s.io/api/core/v1"
"k8s.io/utils/net"
)

/* cni config management */

// CNIConfigInputs is supplied to the CNI config template
type CNIConfigInputs struct {
PodCIDR string
DefaultRoute string
Mtu int
PodCIDRs []string
DefaultRoutes []string
Mtu int
}

// ComputeCNIConfigInputs computes the template inputs for CNIConfigWriter
func ComputeCNIConfigInputs(node corev1.Node) CNIConfigInputs {
podCIDR := node.Spec.PodCIDR
defaultRoute := "0.0.0.0/0"

if net.IsIPv6CIDRString(podCIDR) {
defaultRoute = "::/0"
defaultRoutes := []string{"0.0.0.0/0", "::/0"}
// check if is a dualstack cluster
if len(node.Spec.PodCIDRs) > 1 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be zero on older clusters? or some client side magic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since PodCIDRs is a []string , my understanding is that client-go will set the nil value if it does not exist in the server.
You know that @liggitt will illustrate us 😄

return CNIConfigInputs{
PodCIDRs: node.Spec.PodCIDRs,
DefaultRoutes: defaultRoutes,
}
}
// the cluster is single stack
// we use the legacy node.Spec.PodCIDR for backwards compatibility
podCIDRs := []string{node.Spec.PodCIDR}
// This is a single stack cluster
defaultRoute := defaultRoutes[:1]
if isIPv6CIDRString(podCIDRs[0]) {
defaultRoute = defaultRoutes[1:]
}
return CNIConfigInputs{
PodCIDR: podCIDR,
DefaultRoute: defaultRoute,
PodCIDRs: podCIDRs,
DefaultRoutes: defaultRoute,
}
}

Expand Down Expand Up @@ -82,17 +92,19 @@ const cniConfigTemplate = `
"type": "host-local",
"dataDir": "/run/cni-ipam-state",
"routes": [
{
"dst": "{{ .DefaultRoute }}"
}
{{$first := true}}
{{- range $route := .DefaultRoutes}}
{{if $first}}{{$first = false}}{{else}},{{end}}
{ "dst": "{{ $route }}" }
{{- end}}
],
"ranges": [
[
{
"subnet": "{{ .PodCIDR }}"
}
{{$first := true}}
{{- range $cidr := .PodCIDRs}}
{{if $first}}{{$first = false}}{{else}},{{end}}
[ { "subnet": "{{ $cidr }}" } ]
{{- end}}
]
]
}
{{if .Mtu}},
"mtu": {{ .Mtu }}
Expand Down
178 changes: 139 additions & 39 deletions images/kindnetd/cmd/kindnetd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@ import (
"fmt"
"net"
"os"
"strings"
"syscall"
"time"

"k8s.io/apimachinery/pkg/util/sets"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
utilsnet "k8s.io/utils/net"
)

const (
Expand All @@ -50,6 +52,18 @@ const (

// TODO: improve logging & error handling

// IPFamily defines kindnet networking operating model
type IPFamily string

const (
// IPv4Family sets IPFamily to ipv4
IPv4Family IPFamily = "ipv4"
// IPv6Family sets IPFamily to ipv6
IPv6Family IPFamily = "ipv6"
// DualStackFamily sets ClusterIPFamily to DualStack
DualStackFamily IPFamily = "dualstack"
)

func main() {
// enable logging
klog.InitFlags(nil)
Expand Down Expand Up @@ -92,10 +106,10 @@ func main() {
hostIP, podIP := os.Getenv("HOST_IP"), os.Getenv("POD_IP")
klog.Infof("hostIP = %s\npodIP = %s\n", hostIP, podIP)
if hostIP != podIP {
panic(fmt.Sprintf(
klog.Warningf(
"hostIP(= %q) != podIP(= %q) but must be running with host network: ",
hostIP, podIP,
))
)
}

mtu, err := computeBridgeMTU()
Expand All @@ -110,20 +124,58 @@ func main() {
}

// enforce ip masquerade rules
// TODO: dual stack...?
masqAgent, err := NewIPMasqAgent(utilsnet.IsIPv6String(hostIP), []string{os.Getenv("POD_SUBNET")})
if err != nil {
panic(err.Error())
podSubnetEnv := os.Getenv("POD_SUBNET")
if podSubnetEnv == "" {
panic("missing environment variable POD_SUBNET")
}
podSubnetEnv = strings.TrimSpace(podSubnetEnv)
podSubnets := strings.Split(podSubnetEnv, ",")
clusterIPv4Subnets, clusterIPv6Subnets := splitCIDRs(podSubnets)

// detect the cluster IP family based on the Cluster CIDR aka PodSubnet
var ipFamily IPFamily
if len(clusterIPv4Subnets) > 0 && len(clusterIPv6Subnets) > 0 {
ipFamily = DualStackFamily
} else if len(clusterIPv6Subnets) > 0 {
ipFamily = IPv6Family
} else if len(clusterIPv4Subnets) > 0 {
ipFamily = IPv4Family
} else {
panic(fmt.Sprintf("podSubnets ClusterCIDR/Pod_Subnet: %v", podSubnetEnv))
}
klog.Infof("kindnetd IP family: %q", ipFamily)

// create an ipMasqAgent for IPv4
if len(clusterIPv4Subnets) > 0 {
klog.Infof("noMask IPv4 subnets: %v", clusterIPv4Subnets)
masqAgentIPv4, err := NewIPMasqAgent(false, clusterIPv4Subnets)
if err != nil {
panic(err.Error())
}
go func() {
if err := masqAgentIPv4.SyncRulesForever(time.Second * 60); err != nil {
panic(err)
}
}()
}
// run the masqAgent and panic if is not able to install the rules to no masquerade the pod to pod traffic
go func() {
if err := masqAgent.SyncRulesForever(time.Second * 60); err != nil {

// create an ipMasqAgent for IPv6
if len(clusterIPv6Subnets) > 0 {
klog.Infof("noMask IPv6 subnets: %v", clusterIPv6Subnets)
masqAgentIPv6, err := NewIPMasqAgent(true, clusterIPv6Subnets)
if err != nil {
panic(err.Error())
}
}()

go func() {
if err := masqAgentIPv6.SyncRulesForever(time.Second * 60); err != nil {
panic(err)
}
}()
}

// setup nodes reconcile function, closes over arguments
reconcileNodes := makeNodesReconciler(cniConfigWriter, hostIP)
reconcileNodes := makeNodesReconciler(cniConfigWriter, hostIP, ipFamily)

// main control loop
for {
Expand Down Expand Up @@ -162,27 +214,17 @@ func main() {
}

// nodeNodesReconciler returns a reconciliation func for nodes
func makeNodesReconciler(cniConfig *CNIConfigWriter, hostIP string) func(*corev1.NodeList) error {
func makeNodesReconciler(cniConfig *CNIConfigWriter, hostIP string, ipFamily IPFamily) func(*corev1.NodeList) error {
// reconciles a node
reconcileNode := func(node corev1.Node) error {
// first get this node's IP
nodeIP := internalIP(node)
if nodeIP == "" {
klog.Infof("Node %v has no Internal IP, ignoring\n", node.Name)
return nil
}

// don't do anything unless there is a PodCIDR
podCIDR := node.Spec.PodCIDR
if podCIDR == "" {
klog.Infof("Node %v has no CIDR, ignoring\n", node.Name)
return nil
}

// This is our node. We don't need to add routes, but we might need to
// update the cni config.
if nodeIP == hostIP {
klog.Infof("handling current node\n")
// first get this node's IPs
// we don't support more than one IP address per IP family for simplification
nodeIPs := internalIPs(node)
klog.Infof("Handling node with IPs: %v\n", nodeIPs)
// This is our node. We don't need to add routes,
// but we might need to update the cni config
if nodeIPs.Has(hostIP) {
klog.Info("handling current node\n")
// compute the current cni config inputs
if err := cniConfig.Write(
ComputeCNIConfigInputs(node),
Expand All @@ -193,12 +235,41 @@ func makeNodesReconciler(cniConfig *CNIConfigWriter, hostIP string) func(*corev1
return nil
}

klog.Infof("Handling node with IP: %s\n", nodeIP)
klog.Infof("Node %v has CIDR %s \n", node.Name, podCIDR)
if err := syncRoute(nodeIP, podCIDR); err != nil {
return err
// This is another node. Add routes to the POD subnets in the other nodes
// don't do anything unless there is a PodCIDR
var podCIDRs []string
if ipFamily == DualStackFamily {
podCIDRs = node.Spec.PodCIDRs
} else {
podCIDRs = []string{node.Spec.PodCIDR}
}
if len(podCIDRs) == 0 {
fmt.Printf("Node %v has no CIDR, ignoring\n", node.Name)
return nil
}
klog.Infof("Node %v has CIDR %s \n", node.Name, podCIDRs)
podCIDRsv4, podCIDRsv6 := splitCIDRs(podCIDRs)

// obtain the PodCIDR gateway
var nodeIPv4, nodeIPv6 string
for _, ip := range nodeIPs.List() {
if isIPv6String(ip) {
nodeIPv6 = ip
} else {
nodeIPv4 = ip
}
}

if nodeIPv4 != "" && len(podCIDRsv4) > 0 {
if err := syncRoute(nodeIPv4, podCIDRsv4); err != nil {
return err
}
}
if nodeIPv6 != "" && len(podCIDRsv6) > 0 {
if err := syncRoute(nodeIPv6, podCIDRsv6); err != nil {
return err
}
}
aojea marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

Expand All @@ -213,14 +284,30 @@ func makeNodesReconciler(cniConfig *CNIConfigWriter, hostIP string) func(*corev1
}
}

// internalIP returns the internalIP address for node
func internalIP(node corev1.Node) string {
// internalIPs returns the internal IP addresses for node
func internalIPs(node corev1.Node) sets.String {
ips := sets.NewString()
// check the node.Status.Addresses
for _, address := range node.Status.Addresses {
if address.Type == "InternalIP" {
return address.Address
ips.Insert(address.Address)
}
}
return ""
return ips
}

// splitCIDRs given a slice of strings with CIDRs it returns 2 slice of strings per IP family
// The order returned is always v4 v6
func splitCIDRs(cidrs []string) ([]string, []string) {
var v4subnets, v6subnets []string
for _, subnet := range cidrs {
if isIPv6CIDRString(subnet) {
v6subnets = append(v6subnets, subnet)
} else {
v4subnets = append(v4subnets, subnet)
}
}
return v4subnets, v6subnets
}

// Modified from agnhost connect command in k/k
Expand Down Expand Up @@ -251,3 +338,16 @@ func probeTCP(address string, timeout time.Duration) bool {
klog.Warningf("OTHER %s: %v", address, err)
return false
}

// isIPv6String returns if ip is IPv6.
func isIPv6String(ip string) bool {
netIP := net.ParseIP(ip)
return netIP != nil && netIP.To4() == nil
}

// isIPv6CIDRString returns if cidr is IPv6.
// This assumes cidr is a valid CIDR.
func isIPv6CIDRString(cidr string) bool {
ip, _, _ := net.ParseCIDR(cidr)
return ip != nil && ip.To4() == nil
}
38 changes: 20 additions & 18 deletions images/kindnetd/cmd/kindnetd/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,30 @@ import (
"k8s.io/klog/v2"
)

func syncRoute(nodeIP, podCIDR string) error {
// parse subnet
dst, err := netlink.ParseIPNet(podCIDR)
if err != nil {
return err
}

// Check if the route exists to the other node's PodCIDR
func syncRoute(nodeIP string, podCIDRs []string) error {
ip := net.ParseIP(nodeIP)
routeToDst := netlink.Route{Dst: dst, Gw: ip}
route, err := netlink.RouteListFiltered(nl.GetIPFamily(ip), &routeToDst, netlink.RT_FILTER_DST)
if err != nil {
return err
}

// Add route if not present
if len(route) == 0 {
if err := netlink.RouteAdd(&routeToDst); err != nil {
for _, podCIDR := range podCIDRs {
// parse subnet
dst, err := netlink.ParseIPNet(podCIDR)
if err != nil {
return err
}
klog.Infof("Adding route %v \n", routeToDst)
}

// Check if the route exists to the other node's PodCIDR
routeToDst := netlink.Route{Dst: dst, Gw: ip}
route, err := netlink.RouteListFiltered(nl.GetIPFamily(ip), &routeToDst, netlink.RT_FILTER_DST)
if err != nil {
return err
}

// Add route if not present
if len(route) == 0 {
klog.Infof("Adding route %v \n", routeToDst)
if err := netlink.RouteAdd(&routeToDst); err != nil {
return err
}
}
}
return nil
}
Loading