From a6e8842c428da924c1f34baa4e1d9ed1678ee7e5 Mon Sep 17 00:00:00 2001 From: ssup2 Date: Sun, 15 Nov 2020 13:02:45 +0000 Subject: [PATCH] Reborn to network-node-manager --- README.md | 74 +-- controllers/rule_common.go | 116 ++++ controllers/rule_drop_invalid_input.go | 108 ++++ controllers/rule_external_cluster.go | 465 +++++++++++++++ controllers/service_controller.go | 557 ++++-------------- ....yml => network-node-manager_iptables.yml} | 0 deploy/network-node-manager_ipvs.yml | 97 +++ img/network-node-manager.pptx | Bin 0 -> 50608 bytes img/network-node-manager_Architecture.PNG | Bin 0 -> 22437 bytes .../connection_reset_issue_pod_out_cluster.md | 27 + ...xternal_IP_access_issue_IPVS_proxy_mode.md | 52 ++ main.go | 3 + pkg/configs/configs.go | 43 +- pkg/configs/configs_test.go | 26 + pkg/iptables/iptables.go | 12 + 15 files changed, 1099 insertions(+), 481 deletions(-) create mode 100644 controllers/rule_common.go create mode 100644 controllers/rule_drop_invalid_input.go create mode 100644 controllers/rule_external_cluster.go rename deploy/{network-node-manager.yml => network-node-manager_iptables.yml} (100%) create mode 100644 deploy/network-node-manager_ipvs.yml create mode 100644 img/network-node-manager.pptx create mode 100644 img/network-node-manager_Architecture.PNG create mode 100644 issues/connection_reset_issue_pod_out_cluster.md create mode 100644 issues/external_IP_access_issue_IPVS_proxy_mode.md diff --git a/README.md b/README.md index 0a0a212..921b937 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # Network Node Manager -network-node-manager is the kubernetes controller that solves External-IP (Load Balancer IP) issue with IPVS proxy mode. IPVS proxy mode has various problems, and one of them is that the External-IP assigned through the LoadBalancer type service with externalTrafficPolicy=Local option cannot access inside the cluster. More details on this issue can be found at [here](https://github.com/kubernetes/kubernetes/issues/75262). network-node-manager solves this issue. network-node-manager is based on [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder). +network-node-manager is a kubernetes controller that controls the network configuration of a node to resolve network issues of kubernetes. By simply deploying and configuring network-node-manager, you can solve kubernetes network issues that cannot be resolved by kubernetes or resolved by the higher kubernetes Version. Below is a list of kubernetes's issues to be resolved by network-node-manager. network-node-manager is based on [kubebuilder](https://github.com/kubernetes-sigs/kubebuilder). + +* [External-IP access issue with IPVS proxy mode](issues/external_IP_access_issue_IPVS_proxy_mode.md) +* [Connection reset issue between pod and out of cluster](issues/connection_reset_issue_pod_out_cluster.md) ## Deploy @@ -9,55 +12,56 @@ network-node-manager now supports below CPU architectures. * amd64 * arm64 -Deploy network-node-managers through below command. +Deploy network-node-managers through below command according to kube-proxy mode. ``` -kubectl apply -f https://raw.githubusercontent.com/kakao/network-node-manager/master/deploy/network-node-manager.yml +iptables proxy mode : kubectl apply -f https://raw.githubusercontent.com/kakao/network-node-manager/master/deploy/network-node-manager_iptables.yml +IPVS proxy mode : kubectl apply -f https://raw.githubusercontent.com/kakao/network-node-manager/master/deploy/network-node-manager_ipvs.yml ``` ## Configuration -### IPv6 +### Network Stack (IPv4, IPv6) -network-node-manager supports IPv6. However, IPv6 is not enabled by default. To use IPv6, set "NET_STACK" environment in the DaemonSet manifests of network-node-manager as follows. +* Default : "ipv4" +* iptables proxy mode manifest : "ipv4" +* IPVS proxy mode manifest : "ipv4" ``` -... -env: -- name: NET_STACK - value: ipv4,ipv6 -- name: NODE_NAME - valueFrom: -... +IPv4 : kubectl -n kube-system set env daemonset/network-node-manager NET_STACK=ipv4 +IPv6 : kubectl -n kube-system set env daemonset/network-node-manager NET_STACK=ipv6 +IPv4,IPv6 : kubectl -n kube-system set env daemonset/network-node-manager NET_STACK=ipv4,ipv6 ``` -## How it works? +### External-IP to Cluster-IP DNAT Rule -network-node-manager works on all worker nodes and adds the DNAT rules that converts destination IP of a packet from External-IP to Cluster-IP to iptables. network-node-manager adds two DNAT rules for each LoadBalancer type service. One is added to the prerouting chain and the other is added to the output chain. The DNAT rule in the prerouting chain is for the pod that uses pod-only network namespace. On the other hand, The DNAT rule in the output chain is for the pod that uses host network namespace. All DNAT rules only target packets from pods on the host. Below is an example. +* Related issue : [External-IP access issue with IPVS proxy mode](issues/external_IP_access_issue_IPVS_proxy_mode.md) +* Default : false +* iptables proxy mode manifest : false +* IPVS proxy mode manifest : true ``` -$ kubectl -n default get service -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -lb-service-1 LoadBalancer 10.231.42.164 10.19.20.201 80:31751/TCP,443:30126/TCP 16d -lb-service-2 LoadBalancer 10.231.2.62 10.19.22.57 80:32352/TCP,443:31549/TCP 16d - -$ iptables -nvL -t nat -... -Chain NMANAGER_IPVS_LB_OUTPUT (1 references) - pkts bytes target prot opt in out source destination - 0 0 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 10.19.20.201 /* default/lb-service-1 */ ADDRTYPE match src-type LOCAL - 0 0 DNAT all -- * * 0.0.0.0/0 10.19.20.201 /* default/lb-service-1 */ ADDRTYPE match src-type LOCAL to:10.231.42.164 - 2 120 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 10.19.22.57 /* default/lb-service-2 */ ADDRTYPE match src-type LOCAL - 2 120 DNAT all -- * * 0.0.0.0/0 10.19.22.57 /* default/lb-service-2 */ ADDRTYPE match src-type LOCAL to:10.231.2.62 - -Chain NMANAGER_IPVS_LB_PREROUTING (1 references) - pkts bytes target prot opt in out source destination - 0 0 KUBE-MARK-MASQ all -- * * 10.240.2.128/25 10.19.20.201 /* default/lb-service-1 */ - 0 0 DNAT all -- * * 10.240.2.128/25 10.19.20.201 /* default/lb-service-1 */ to:10.231.42.164 - 1 60 KUBE-MARK-MASQ all -- * * 10.240.2.128/25 10.19.22.57 /* default/lb-service-2 */ - 1 60 DNAT all -- * * 10.240.2.128/25 10.19.22.57 /* default/lb-service-2 */ to:10.231.2.62 -... +On : kubectl -n kube-system set env daemonset/network-node-manager RULE_EXTERNAL_CLUSTER=true +Off : kubectl -n kube-system set env daemonset/network-node-manager RULE_EXTERNAL_CLUSTER=false +``` + +### Drop Invalid Packet Rule in INPUT chain + +* Related issue : [Connection reset issue between pod and out of cluster](issues/connection_reset_issue_pod_out_cluster.md) +* Default : true +* iptables proxy mode manifest : true +* IPVS proxy mode manifest : true + ``` +On : kubectl -n kube-system set env daemonset/network-node-manager RULE_DROP_INVALID_INPUT=true +Off : kubectl -n kube-system set env daemonset/network-node-manager RULE_DROP_INVALID_INPUT=false +``` + +## How it works? + +![kpexec Architecture](img/network-node-manager_Architecture.PNG) + +network-node-manager runs on all kubernetes cluster nodes in host network namespace with network privileges and manage the node network configuration. network-node-manager watches the kubernetes object through kubenetes API server like a general kubernetes controller and manage the node network configuration. Now network-node-manager only watches service object. ## License diff --git a/controllers/rule_common.go b/controllers/rule_common.go new file mode 100644 index 0000000..a868815 --- /dev/null +++ b/controllers/rule_common.go @@ -0,0 +1,116 @@ +package controllers + +import ( + "github.com/go-logr/logr" + + "github.com/kakao/network-node-manager/pkg/configs" + "github.com/kakao/network-node-manager/pkg/iptables" +) + +// Constants +const ( + ChainFilterInput = "INPUT" + ChainNATPrerouting = "PREROUTING" + ChainNATOutput = "OUTPUT" + + ChainNATKubeMarkMasq = "KUBE-MARK-MASQ" + + ChainFilterBaseInput = "NMANAGER_INPUT" + ChainNATBasePrerouting = "NMANAGER_PREROUTING" + ChainNATBaseOutput = "NMANAGER_OUTPUT" + + ChainFilterDropInvalidInput = "NMANAGER_DROP_INVALID_INPUT" + ChainNATExternalClusterPrerouting = "NMANAGER_EX_CLUS_PREROUTING" + ChainNATExternalClusterOutput = "NMANAGER_EX_CLUS_OUTPUT" +) + +func initBaseChains(logger logr.Logger) error { + // Get network stack config + configIPv4Enabled, configIPv6Enabled, err := configs.GetConfigNetStack() + if err != nil { + return err + } + + // IPv4 + if configIPv4Enabled { + // Create base chain in tables + out, err := iptables.CreateChainIPv4(iptables.TableFilter, ChainFilterBaseInput) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv4(iptables.TableNAT, ChainNATBasePrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv4(iptables.TableNAT, ChainNATBaseOutput) + if err != nil { + logger.Error(err, out) + return err + } + + // Create jump rule to each chain in tables + ruleJumpFilterInput := []string{"-j", ChainFilterBaseInput} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableFilter, ChainFilterInput, "", ruleJumpFilterInput...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpNATPre := []string{"-j", ChainNATBasePrerouting} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATPrerouting, "", ruleJumpNATPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpNATOut := []string{"-j", ChainNATBaseOutput} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATOutput, "", ruleJumpNATOut...) + if err != nil { + logger.Error(err, out) + return err + } + + } + + // IPv6 + if configIPv6Enabled { + // Create base chain in nat table + out, err := iptables.CreateChainIPv6(iptables.TableFilter, ChainFilterBaseInput) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv6(iptables.TableNAT, ChainNATBasePrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv6(iptables.TableNAT, ChainNATBaseOutput) + if err != nil { + logger.Error(err, out) + return err + } + + // Create jump rule to each chain in nat table + ruleJumpFilterInput := []string{"-j", ChainFilterBaseInput} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableFilter, ChainFilterInput, "", ruleJumpFilterInput...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpNATPre := []string{"-j", ChainNATBasePrerouting} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATPrerouting, "", ruleJumpNATPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpNATOut := []string{"-j", ChainNATBaseOutput} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATOutput, "", ruleJumpNATOut...) + if err != nil { + logger.Error(err, out) + return err + } + } + + return nil +} diff --git a/controllers/rule_drop_invalid_input.go b/controllers/rule_drop_invalid_input.go new file mode 100644 index 0000000..d2d4b21 --- /dev/null +++ b/controllers/rule_drop_invalid_input.go @@ -0,0 +1,108 @@ +package controllers + +import ( + "github.com/go-logr/logr" + + "github.com/kakao/network-node-manager/pkg/iptables" +) + +func createRulesDropInvalidInput(logger logr.Logger) error { + if err := initBaseChains(logger); err != nil { + logger.Error(err, "failed to init base chain for externalIP to clusterIP Rules") + return err + } + + // IPv4 + if configIPv4Enabled { + // Create chain + out, err := iptables.CreateChainIPv4(iptables.TableFilter, ChainFilterDropInvalidInput) + if err != nil { + logger.Error(err, out) + return err + } + + // Set drop rule + ruleDrop := []string{"-m", "conntrack", "--ctstate", "INVALID", "-j", "DROP"} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableFilter, ChainFilterDropInvalidInput, "", ruleDrop...) + if err != nil { + logger.Error(err, out) + return err + } + + // Set jump rule + ruleJump := []string{"-j", ChainFilterDropInvalidInput} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableFilter, ChainFilterBaseInput, "", ruleJump...) + if err != nil { + logger.Error(err, out) + return err + } + } + + // IPv6 + if configIPv6Enabled { + // Create chain + out, err := iptables.CreateChainIPv6(iptables.TableFilter, ChainFilterDropInvalidInput) + if err != nil { + logger.Error(err, out) + return err + } + + // Set drop rule + ruleDrop := []string{"-m", "conntrack", "--ctstate", "INVALID", "-j", "DROP"} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableFilter, ChainFilterDropInvalidInput, "", ruleDrop...) + if err != nil { + logger.Error(err, out) + return err + } + + // Set jump rule + ruleJump := []string{"-j", ChainFilterDropInvalidInput} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableFilter, ChainFilterBaseInput, "", ruleJump...) + if err != nil { + logger.Error(err, out) + return err + } + } + + return nil +} + +func deleteRulesDropInvalidInput(logger logr.Logger) error { + // IPv4 + if configIPv4Enabled { + // Delete jump rule + ruleJump := []string{"-j", ChainFilterDropInvalidInput} + out, err := iptables.DeleteRuleIPv4(iptables.TableFilter, ChainFilterBaseInput, "", ruleJump...) + if err != nil { + logger.Error(err, out) + return err + } + + // Delete chain + out, err = iptables.DeleteChainIPv4(iptables.TableFilter, ChainFilterDropInvalidInput) + if err != nil { + logger.Error(err, out) + return err + } + } + + // IPv6 + if configIPv6Enabled { + // Delete jump rule + ruleJump := []string{"-j", ChainFilterDropInvalidInput} + out, err := iptables.DeleteRuleIPv6(iptables.TableFilter, ChainFilterBaseInput, "", ruleJump...) + if err != nil { + logger.Error(err, out) + return err + } + + // Delete chain + out, err = iptables.DeleteChainIPv6(iptables.TableFilter, ChainFilterDropInvalidInput) + if err != nil { + logger.Error(err, out) + return err + } + } + + return nil +} diff --git a/controllers/rule_external_cluster.go b/controllers/rule_external_cluster.go new file mode 100644 index 0000000..2c367f8 --- /dev/null +++ b/controllers/rule_external_cluster.go @@ -0,0 +1,465 @@ +package controllers + +import ( + "errors" + "strings" + + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/go-logr/logr" + "github.com/kakao/network-node-manager/pkg/ip" + "github.com/kakao/network-node-manager/pkg/iptables" +) + +func initRulesExternalCluster(logger logr.Logger) error { + if err := initBaseChains(logger); err != nil { + logger.Error(err, "failed to init base chain for externalIP to clusterIP Rules") + return err + } + + // IPv4 + if configIPv4Enabled { + // Create chain in nat table + out, err := iptables.CreateChainIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv4(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + logger.Error(err, out) + return err + } + + // Set jump rule to each chain in nat table + ruleJumpPre := []string{"-j", ChainNATExternalClusterPrerouting} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATBasePrerouting, "", ruleJumpPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpOut := []string{"-j", ChainNATExternalClusterOutput} + out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATBaseOutput, "", ruleJumpOut...) + if err != nil { + logger.Error(err, out) + return err + } + } + // IPv6 + if configIPv6Enabled { + // Create chain in nat table + out, err := iptables.CreateChainIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.CreateChainIPv6(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + logger.Error(err, out) + return err + } + + // Set jump rule to each chain in nat table + ruleJumpPre := []string{"-j", ChainNATExternalClusterPrerouting} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATBasePrerouting, "", ruleJumpPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpOut := []string{"-j", ChainNATExternalClusterOutput} + out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATBaseOutput, "", ruleJumpOut...) + if err != nil { + logger.Error(err, out) + return err + } + } + + return nil +} + +func destoryRulesExternalCluster(logger logr.Logger) error { + // IPv4 + if configIPv4Enabled { + // Delete jump rule to each chain in nat table + ruleJumpPre := []string{"-j", ChainNATExternalClusterPrerouting} + out, err := iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATBasePrerouting, "", ruleJumpPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpOut := []string{"-j", ChainNATExternalClusterOutput} + out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATBaseOutput, "", ruleJumpOut...) + if err != nil { + logger.Error(err, out) + return err + } + + // Delete chain in nat table + out, err = iptables.DeleteChainIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.DeleteChainIPv4(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + logger.Error(err, out) + return err + } + } + // IPv6 + if configIPv6Enabled { + // Delete jump rule to each chain in nat table + ruleJumpPre := []string{"-j", ChainNATExternalClusterPrerouting} + out, err := iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATBasePrerouting, "", ruleJumpPre...) + if err != nil { + logger.Error(err, out) + return err + } + ruleJumpOut := []string{"-j", ChainNATExternalClusterOutput} + out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATBaseOutput, "", ruleJumpOut...) + if err != nil { + logger.Error(err, out) + return err + } + + // Delete chain in nat table + out, err = iptables.DeleteChainIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + logger.Error(err, out) + return err + } + out, err = iptables.DeleteChainIPv6(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + logger.Error(err, out) + return err + } + } + + return nil +} + +func cleanupRulesExternalCluster(logger logr.Logger, svcs *corev1.ServiceList, podCIDRIPv4, podCIDRIPv6 string) error { + // IPv4 + if configIPv4Enabled { + // Make up service map + svcMap := make(map[string]*corev1.Service) + for _, svc := range svcs.Items { + if ip.IsIPv4Addr(svc.Spec.ClusterIP) && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + svcMap[svc.Namespace+"/"+svc.Name] = svc.DeepCopy() + } + } + + // Cleanup prerouting chain + preRules, err := iptables.GetRulesIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + return err + } + for _, rule := range preRules { + // Get service info from rule and k8s, and delete iptables rules + nsName, src, dest, jump, dnatDest := getSvcInfoFromRule(rule) + svc, ok := svcMap[nsName] + if !ok { + logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup prerouting chain IPv4 rule") + out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + continue + } + + // Compare service info and delete iptables rules + for _, ingress := range svc.Status.LoadBalancer.Ingress { + externalIP := ingress.IP + "/32" + if (jump == ChainNATKubeMarkMasq && (src == podCIDRIPv4 && dest == externalIP)) || + (jump == "DNAT" && (src == podCIDRIPv4 && dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { + continue + } + logger.WithValues("rule", rule).Info("service info is diff. cleanup prerouting chain IPv4 rule") + out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + } + } + + // Cleanup output chain + outRules, err := iptables.GetRulesIPv4(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + return err + } + for _, rule := range outRules { + // Get service info from rule and k8s, and delete iptables rules + nsName, _, dest, jump, dnatDest := getSvcInfoFromRule(rule) + svc, ok := svcMap[nsName] + if !ok { + logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup output chain IPv4 rule") + out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + continue + } + + // Compare service info and delete diff iptables rules + for _, ingress := range svc.Status.LoadBalancer.Ingress { + externalIP := ingress.IP + "/32" + if (jump == ChainNATKubeMarkMasq && dest == externalIP) || + (jump == "DNAT" && (dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { + continue + } + logger.WithValues("rule", rule).Info("service info is diff. cleanup output chain IPv4 rule") + out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + } + } + } + // IPv6 + if configIPv6Enabled { + // Make up service map + svcMap := make(map[string]*corev1.Service) + for _, svc := range svcs.Items { + if ip.IsIPv6Addr(svc.Spec.ClusterIP) && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { + svcMap[svc.Namespace+"/"+svc.Name] = svc.DeepCopy() + } + } + + // Cleanup prerouting chain + preRules, err := iptables.GetRulesIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting) + if err != nil { + return err + } + for _, rule := range preRules { + // Get service info from rule and k8s, and delete iptables rules + nsName, src, dest, jump, dnatDest := getSvcInfoFromRule(rule) + svc, ok := svcMap[nsName] + if !ok { + logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup prerouting chain IPv6 rule") + out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + continue + } + + // Compare service info and delete iptables rules + for _, ingress := range svc.Status.LoadBalancer.Ingress { + externalIP := ingress.IP + "/128" + if (jump == ChainNATKubeMarkMasq && (src == podCIDRIPv6 && dest == externalIP)) || + (jump == "DNAT" && (src == podCIDRIPv6 && dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { + continue + } + logger.WithValues("rule", rule).Info("service info is diff. cleanup prerouting chain IPv6 rule") + out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + } + } + + // Cleanup output + outRules, err := iptables.GetRulesIPv6(iptables.TableNAT, ChainNATExternalClusterOutput) + if err != nil { + return err + } + for _, rule := range outRules { + // Get service info from rule and k8s, and delete iptables rules + nsName, _, dest, jump, dnatDest := getSvcInfoFromRule(rule) + svc, ok := svcMap[nsName] + if !ok { + logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup output chain IPv6 rule") + out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + continue + } + + // Compare service info and delete diff iptables rules + for _, ingress := range svc.Status.LoadBalancer.Ingress { + externalIP := ingress.IP + "/128" + if (jump == ChainNATKubeMarkMasq && dest == externalIP) || + (jump == "DNAT" && (dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { + continue + } + logger.WithValues("rule", rule).Info("service info is diff. cleanup output chain IPv6 rule") + out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) + if err != nil { + logger.Error(err, out) + return err + } + } + } + } + + return nil +} + +func createRulesExternalCluster(logger logr.Logger, req *ctrl.Request, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6 string) error { + // Don't use spec.ipFamily to distingush between IPv4 and IPv6 Address + // for kubernetes version that dosen't support IPv6 dualstack + if configIPv4Enabled && ip.IsIPv4Addr(clusterIP) { + // IPv4 + // Set prerouting + rulePreMasq := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err := iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreMasq...) + if err != nil { + logger.Error(err, out) + return err + } + rulePreDNAT := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + + // Set output + ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutMasq...) + if err != nil { + logger.Error(err, out) + return err + } + ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + } else if configIPv6Enabled && ip.IsIPv6Addr(clusterIP) { + // IPv6 + // Set prerouting + rulePreMasq := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err := iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreMasq...) + if err != nil { + logger.Error(err, out) + return err + } + rulePreDNAT := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + + // Set output + ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutMasq...) + if err != nil { + logger.Error(err, out) + return err + } + ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + } else { + if ip.IsVaildIP(clusterIP) { + logger.WithValues("clusterIP", clusterIP).Error(errors.New("invalid IP"), "invaild IP") + } + } + + return nil +} + +func deleteRulesExternalCluster(logger logr.Logger, req *ctrl.Request, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6 string) error { + // Don't use spec.ipFamily to distingush between IPv4 and IPv6 Address + // for kubernetes version that dosen't support IPv6 dualstack + if configIPv4Enabled && ip.IsIPv4Addr(clusterIP) { + // IPv4 + // Unset prerouting + rulePreMasq := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err := iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreMasq...) + if err != nil { + logger.Error(err, out) + return err + } + rulePreDNAT := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + + // Unset output + ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutMasq...) + if err != nil { + logger.Error(err, out) + return err + } + ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + } else if configIPv6Enabled && ip.IsIPv6Addr(clusterIP) { + // IPv6 + // Unset prerouting + rulePreMasq := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err := iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreMasq...) + if err != nil { + logger.Error(err, out) + return err + } + rulePreDNAT := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATExternalClusterPrerouting, req.String(), rulePreDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + + // Unset output + ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMarkMasq} + out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutMasq...) + if err != nil { + logger.Error(err, out) + return err + } + ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} + out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATExternalClusterOutput, req.String(), ruleOutDNAT...) + if err != nil { + logger.Error(err, out) + return err + } + } else { + if ip.IsVaildIP(clusterIP) { + logger.WithValues("clusterIP", clusterIP).Error(errors.New("invalid IP"), "invaild IP") + } + } + return nil +} + +func getPodCIDR(cidrs []string) (ipv4CIDR string, ipv6CIDR string) { + for _, cidr := range cidrs { + addr := strings.Split(cidr, "/")[0] + if ip.IsIPv4Addr(addr) { + ipv4CIDR = cidr + } else if ip.IsIPv6Addr(addr) { + ipv6CIDR = cidr + } + } + return +} + +func getSvcInfoFromRule(rule string) (nsName, src, dest, jump, dnatDest string) { + nsName = iptables.GetRuleComment(rule) + src = iptables.GetRuleSrc(rule) + dest = iptables.GetRuleDest(rule) + jump = iptables.GetRuleJump(rule) + dnatDest = iptables.GetRuleDNATDest(rule) + return +} diff --git a/controllers/service_controller.go b/controllers/service_controller.go index 6c4983b..21b0e87 100644 --- a/controllers/service_controller.go +++ b/controllers/service_controller.go @@ -18,9 +18,8 @@ package controllers import ( "context" - "errors" "os" - "strings" + "time" corev1 "k8s.io/api/core/v1" apierror "k8s.io/apimachinery/pkg/api/errors" @@ -31,8 +30,6 @@ import ( "github.com/go-logr/logr" "github.com/kakao/network-node-manager/pkg/configs" - "github.com/kakao/network-node-manager/pkg/ip" - "github.com/kakao/network-node-manager/pkg/iptables" ) // ServiceReconciler reconciles a Service object @@ -42,22 +39,15 @@ type ServiceReconciler struct { Scheme *runtime.Scheme } -// Constants -const ( - ChainNATPrerouting = "PREROUTING" - ChainNATOutput = "OUTPUT" - ChainNATKubeMasquerade = "KUBE-MARK-MASQ" - - ChainNATIPVSLBPrerouting = "NMANAGER_IPVS_LB_PREROUTING" - ChainNATIPVSLBOutput = "NMANAGER_IPVS_LB_OUTPUT" -) - // Variables var ( configNodeName string configIPv4Enabled bool configIPv6Enabled bool + configRuleExternalCluster bool + configRuleDropInvalidInput bool + initFlag = false podCIDRIPv4 string podCIDRIPv6 string @@ -91,478 +81,157 @@ func (r *ServiceReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { logger.Error(err, "config error") os.Exit(1) } - logger.WithValues("node name", configNodeName).Info("config node name") configIPv4Enabled, configIPv6Enabled, err = configs.GetConfigNetStack() if err != nil { logger.Error(err, "config error") os.Exit(1) } - logger.WithValues("IPv4", configIPv4Enabled).WithValues("IPv6", configIPv6Enabled).Info("config network stack") - - // Get nodes's pod CIDR - node := &corev1.Node{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: configNodeName}, node); err != nil { - logger.Error(err, "failed to get the pod's node info from API server") - os.Exit(1) - } - podCIDRIPv4, podCIDRIPv6 = getPodCIDR(node.Spec.PodCIDRs) - logger.WithValues("pod CIDR IPV4", podCIDRIPv4).WithValues("pod CIDR IPv6", podCIDRIPv6).Info("pod CIDR") - - // Init iptables - if err := initIptables(logger); err != nil { - logger.Error(err, "failed to init iptables") - os.Exit(1) - } - - // Get all services - svcs := &corev1.ServiceList{} - if err := r.Client.List(ctx, svcs, client.InNamespace("")); err != nil { - logger.Error(err, "failed to get all services from API server") + configRuleExternalCluster, err = configs.GetConfigRuleExternalCluster() + if err != nil { + logger.Error(err, "config error") os.Exit(1) } - - // Cleanup iptables for deleted services - if err := cleanupIptables(logger, svcs, podCIDRIPv4, podCIDRIPv6); err != nil { - logger.Error(err, "failed to cleanup iptables") + configRuleDropInvalidInput, err = configs.GetConfigRuleDropInvalidInput() + if err != nil { + logger.Error(err, "config error") os.Exit(1) } - } - // Get service info - svc := &corev1.Service{} - if err := r.Client.Get(ctx, req.NamespacedName, svc); err != nil { - if apierror.IsNotFound(err) { - // Not found service means that the service is removed. - // Delete iptables rules by using cache - - // Get service from cache - oldSvc, exist := serviceCache[req] - if !exist { - // If there is no service info in cache, skip it - return ctrl.Result{}, nil - } - - // Delete iptables rules - for _, oldIngress := range oldSvc.Status.LoadBalancer.Ingress { - oldClusterIP := oldSvc.Spec.ClusterIP - oldExternalIP := oldIngress.IP - - // Delete iptables rules - logger.WithValues("externalIP", oldExternalIP).Info("delete iptables rules") - if err := deleteIptablesRules(logger, &req, oldClusterIP, oldExternalIP, podCIDRIPv4, podCIDRIPv6); err != nil { - return ctrl.Result{}, err - } + logger.WithValues("node name", configNodeName).Info("config node name") + logger.WithValues("IPv4", configIPv4Enabled).WithValues("IPv6", configIPv6Enabled).Info("config network stack") + logger.WithValues("flag", configRuleExternalCluster).Info("config rule externalIP to clusterIP") + logger.WithValues("flag", configRuleDropInvalidInput).Info("config rule drop invalid in input chain") + + // Run rules first + if configRuleDropInvalidInput { + if err := createRulesDropInvalidInput(logger); err != nil { + logger.Error(err, "failed to create rule drop invalid input") + os.Exit(1) } - return ctrl.Result{}, nil } else { - logger.Error(err, "failed to get service info") - return ctrl.Result{}, err - } - } - - // Check service is LoadBalancer type - if svc.Spec.Type != corev1.ServiceTypeLoadBalancer { - return ctrl.Result{}, nil - } - - // Create iptables rules - for _, ingress := range svc.Status.LoadBalancer.Ingress { - clusterIP := svc.Spec.ClusterIP - externalIP := ingress.IP - - // Cache service to use when deleting service - serviceCache[req] = *svc.DeepCopy() - - // Create iptables rules - logger.WithValues("externalIP", externalIP).Info("create iptables rules") - if err := createIptablesRules(logger, &req, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6); err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil -} - -func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Set controller manager - return ctrl.NewControllerManagedBy(mgr). - For(&corev1.Service{}). - Complete(r) -} - -func initIptables(logger logr.Logger) error { - logger.Info("create iptables chains") - - // IPv4 - if configIPv4Enabled { - // Create chain in nat table - logger.Info("create the IPVS IPv4 chains") - out, err := iptables.CreateChainIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting) - if err != nil { - logger.Error(err, out) - return err - } - out, err = iptables.CreateChainIPv4(iptables.TableNAT, ChainNATIPVSLBOutput) - if err != nil { - logger.Error(err, out) - return err - } - - // Set jump rule to each chain in nat table - logger.Info("create jump rules for the IPVS IPv4 chains") - ruleJumpPre := []string{"-j", ChainNATIPVSLBPrerouting} - out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATPrerouting, "", ruleJumpPre...) - if err != nil { - logger.Error(err, out) - return err - } - ruleJumpOut := []string{"-j", ChainNATIPVSLBOutput} - out, err = iptables.CreateRuleFirstIPv4(iptables.TableNAT, ChainNATOutput, "", ruleJumpOut...) - if err != nil { - logger.Error(err, out) - return err - } - } - // IPv6 - if configIPv6Enabled { - // Create chain in nat table - logger.Info("create the IPVS IPv6 chains") - out, err := iptables.CreateChainIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting) - if err != nil { - logger.Error(err, out) - return err - } - out, err = iptables.CreateChainIPv6(iptables.TableNAT, ChainNATIPVSLBOutput) - if err != nil { - logger.Error(err, out) - return err - } - - // Set jump rule to each chain in nat table - logger.Info("create jump rules for the IPVS IPv6 chains") - ruleJumpPre := []string{"-j", ChainNATIPVSLBPrerouting} - out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATPrerouting, "", ruleJumpPre...) - if err != nil { - logger.Error(err, out) - return err - } - ruleJumpOut := []string{"-j", ChainNATIPVSLBOutput} - out, err = iptables.CreateRuleFirstIPv6(iptables.TableNAT, ChainNATOutput, "", ruleJumpOut...) - if err != nil { - logger.Error(err, out) - return err - } - } - - return nil -} - -func cleanupIptables(logger logr.Logger, svcs *corev1.ServiceList, podCIDRIPv4, podCIDRIPv6 string) error { - logger.Info("cleanup iptables for deleted services") - - // IPv4 - if configIPv4Enabled { - // Make up service map - svcMap := make(map[string]*corev1.Service) - for _, svc := range svcs.Items { - if ip.IsIPv4Addr(svc.Spec.ClusterIP) && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { - svcMap[svc.Namespace+"/"+svc.Name] = svc.DeepCopy() + if err := deleteRulesDropInvalidInput(logger); err != nil { + logger.Error(err, "failed to delete rule drop invalid input") + os.Exit(1) } } - // Cleanup prerouting chain - preRules, err := iptables.GetRulesIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting) - if err != nil { - return err - } - for _, rule := range preRules { - // Get service info from rule and k8s, and delete iptables rules - nsName, src, dest, jump, dnatDest := getSvcInfoFromRule(rule) - svc, ok := svcMap[nsName] - if !ok { - logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup prerouting chain IPv4 rule") - out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - if err != nil { - logger.Error(err, out) + // Run rules periodically + ticker := time.NewTicker(60 * time.Second) + go func() { + for { + <-ticker.C + if configRuleDropInvalidInput { + if err := createRulesDropInvalidInput(logger); err != nil { + logger.Error(err, "failed to cleanup rule drop invalid input") + } } - continue } + }() - // Compare service info and delete iptables rules - for _, ingress := range svc.Status.LoadBalancer.Ingress { - externalIP := ingress.IP + "/32" - if (jump == ChainNATKubeMasquerade && (src == podCIDRIPv4 && dest == externalIP)) || - (jump == "DNAT" && (src == podCIDRIPv4 && dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { - continue - } - logger.WithValues("rule", rule).Info("service info is diff. cleanup prerouting chain IPv4 rule") - out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - if err != nil { - logger.Error(err, out) - } + if configRuleExternalCluster { + // Get nodes's pod CIDR + node := &corev1.Node{} + if err := r.Client.Get(ctx, types.NamespacedName{Name: configNodeName}, node); err != nil { + logger.Error(err, "failed to get the pod's node info from API server") + os.Exit(1) } - } + podCIDRIPv4, podCIDRIPv6 = getPodCIDR(node.Spec.PodCIDRs) + logger.WithValues("pod CIDR IPV4", podCIDRIPv4).WithValues("pod CIDR IPv6", podCIDRIPv6).Info("pod CIDR") - // Cleanup output chain - outRules, err := iptables.GetRulesIPv4(iptables.TableNAT, ChainNATIPVSLBOutput) - if err != nil { - return err - } - for _, rule := range outRules { - // Get service info from rule and k8s, and delete iptables rules - nsName, _, dest, jump, dnatDest := getSvcInfoFromRule(rule) - svc, ok := svcMap[nsName] - if !ok { - logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup output chain IPv4 rule") - iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - continue + // Initialize externalIP to clusterIP rules + if err := initRulesExternalCluster(logger); err != nil { + logger.Error(err, "failed to initalize rule externalIP to clusterIP") + os.Exit(1) } - // Compare service info and delete diff iptables rules - for _, ingress := range svc.Status.LoadBalancer.Ingress { - externalIP := ingress.IP + "/32" - if (jump == ChainNATKubeMasquerade && dest == externalIP) || - (jump == "DNAT" && (dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { - continue - } - logger.WithValues("rule", rule).Info("service info is diff. cleanup output chain IPv4 rule") - out, err := iptables.DeleteRuleRawIPv4(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - if err != nil { - logger.Error(err, out) - } + // Get all services + svcs := &corev1.ServiceList{} + if err := r.Client.List(ctx, svcs, client.InNamespace("")); err != nil { + logger.Error(err, "failed to get all services from API server") + os.Exit(1) } - } - } - // IPv6 - if configIPv6Enabled { - // Make up service map - svcMap := make(map[string]*corev1.Service) - for _, svc := range svcs.Items { - if ip.IsIPv6Addr(svc.Spec.ClusterIP) && svc.Spec.Type == corev1.ServiceTypeLoadBalancer { - svcMap[svc.Namespace+"/"+svc.Name] = svc.DeepCopy() - } - } - // Cleanup prerouting chain - preRules, err := iptables.GetRulesIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting) - if err != nil { - return err - } - for _, rule := range preRules { - // Get service info from rule and k8s, and delete iptables rules - nsName, src, dest, jump, dnatDest := getSvcInfoFromRule(rule) - svc, ok := svcMap[nsName] - if !ok { - logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup prerouting chain IPv6 rule") - out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - if err != nil { - logger.Error(err, out) - } - continue + // Cleanup externalIP to clusterIP rules for deleted services + if err := cleanupRulesExternalCluster(logger, svcs, podCIDRIPv4, podCIDRIPv6); err != nil { + logger.Error(err, "failed to cleanup rule externalIP to clusterIP") + os.Exit(1) } - - // Compare service info and delete iptables rules - for _, ingress := range svc.Status.LoadBalancer.Ingress { - externalIP := ingress.IP + "/128" - if (jump == ChainNATKubeMasquerade && (src == podCIDRIPv6 && dest == externalIP)) || - (jump == "DNAT" && (src == podCIDRIPv6 && dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { - continue - } - logger.WithValues("rule", rule).Info("service info is diff. cleanup prerouting chain IPv6 rule") - out, err := iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - if err != nil { - logger.Error(err, out) - } + } else { + // Destroy externalIP to clusterIP rules + if err := destoryRulesExternalCluster(logger); err != nil { + logger.Error(err, "failed to destroy rule externalIP to clusterIP") + os.Exit(1) } } + } - // Cleanup output - outRules, err := iptables.GetRulesIPv6(iptables.TableNAT, ChainNATIPVSLBOutput) - if err != nil { - return err + // Loop + if configRuleExternalCluster { + // In case the iptables chain is deleted, initalize again + if err := initRulesExternalCluster(logger); err != nil { + logger.Error(err, "failed to initalize rule externalIP to clusterIP") + os.Exit(1) } - for _, rule := range outRules { - // Get service info from rule and k8s, and delete iptables rules - nsName, _, dest, jump, dnatDest := getSvcInfoFromRule(rule) - svc, ok := svcMap[nsName] - if !ok { - logger.WithValues("rule", rule).Info("there is no service info in k8s. cleanup output chain IPv6 rule") - iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - continue - } - // Compare service info and delete diff iptables rules - for _, ingress := range svc.Status.LoadBalancer.Ingress { - externalIP := ingress.IP + "/128" - if (jump == ChainNATKubeMasquerade && dest == externalIP) || - (jump == "DNAT" && (dest == externalIP && dnatDest == svc.Spec.ClusterIP)) { - continue + // Get service info + svc := &corev1.Service{} + if err := r.Client.Get(ctx, req.NamespacedName, svc); err != nil { + if apierror.IsNotFound(err) { + // Not found service means that the service is removed. + // Delete iptables rules by using cache + + // Get service from cache + oldSvc, exist := serviceCache[req] + if !exist { + // If there is no service info in cache, skip it + return ctrl.Result{}, nil } - logger.WithValues("rule", rule).Info("service info is diff. cleanup output chain IPv6 rule") - iptables.DeleteRuleRawIPv6(iptables.TableNAT, iptables.ChangeRuleToDelete(rule)...) - } - } - } - return nil -} + // Delete rules + for _, oldIngress := range oldSvc.Status.LoadBalancer.Ingress { + oldClusterIP := oldSvc.Spec.ClusterIP + oldExternalIP := oldIngress.IP -func createIptablesRules(logger logr.Logger, req *ctrl.Request, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6 string) error { - // Don't use spec.ipFamily to distingush between IPv4 and IPv6 Address - // for kubernetes version that dosen't support IPv6 dualstack - if configIPv4Enabled && ip.IsIPv4Addr(clusterIP) { - // IPv4 - // Set prerouting - rulePreMasq := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err := iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreMasq...) - if err != nil { - logger.Error(err, out) - return err - } - rulePreDNAT := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - - // Set output - ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutMasq...) - if err != nil { - logger.Error(err, out) - return err - } - ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.CreateRuleLastIPv4(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - } else if configIPv6Enabled && ip.IsIPv6Addr(clusterIP) { - // IPv6 - // Set prerouting - rulePreMasq := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err := iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreMasq...) - if err != nil { - logger.Error(err, out) - return err - } - rulePreDNAT := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreDNAT...) - if err != nil { - logger.Error(err, out) - return err + // Delete rules + logger.WithValues("externalIP", oldExternalIP).WithValues("clusterIP", oldClusterIP).Info("delete rule externalIp to clusterIP") + if err := deleteRulesExternalCluster(logger, &req, oldClusterIP, oldExternalIP, podCIDRIPv4, podCIDRIPv6); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } else { + logger.Error(err, "failed to get service info") + return ctrl.Result{}, err + } } - // Set output - ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutMasq...) - if err != nil { - logger.Error(err, out) - return err - } - ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.CreateRuleLastIPv6(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - } else { - if ip.IsVaildIP(clusterIP) { - logger.WithValues("clusterIP", clusterIP).Error(errors.New("invalid IP"), "invaild IP") + // Check service is LoadBalancer type + if svc.Spec.Type != corev1.ServiceTypeLoadBalancer { + return ctrl.Result{}, nil } - } - return nil -} + // Create rules + for _, ingress := range svc.Status.LoadBalancer.Ingress { + clusterIP := svc.Spec.ClusterIP + externalIP := ingress.IP -func deleteIptablesRules(logger logr.Logger, req *ctrl.Request, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6 string) error { - // Don't use spec.ipFamily to distingush between IPv4 and IPv6 Address - // for kubernetes version that dosen't support IPv6 dualstack - if configIPv4Enabled && ip.IsIPv4Addr(clusterIP) { - // IPv4 - // Unset prerouting - rulePreMasq := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err := iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreMasq...) - if err != nil { - logger.Error(err, out) - return err - } - rulePreDNAT := []string{"-s", podCIDRIPv4, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreDNAT...) - if err != nil { - logger.Error(err, out) - return err - } + // Cache service to use deleting service + serviceCache[req] = *svc.DeepCopy() - // Unset output - ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutMasq...) - if err != nil { - logger.Error(err, out) - return err - } - ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.DeleteRuleIPv4(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - } else if configIPv6Enabled && ip.IsIPv6Addr(clusterIP) { - // IPv6 - // Unset prerouting - rulePreMasq := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err := iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreMasq...) - if err != nil { - logger.Error(err, out) - return err - } - rulePreDNAT := []string{"-s", podCIDRIPv6, "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATIPVSLBPrerouting, req.String(), rulePreDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - - // Unset output - ruleOutMasq := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", ChainNATKubeMasquerade} - out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutMasq...) - if err != nil { - logger.Error(err, out) - return err - } - ruleOutDNAT := []string{"-m", "addrtype", "--src-type", "LOCAL", "-d", externalIP, "-j", "DNAT", "--to-destination", clusterIP} - out, err = iptables.DeleteRuleIPv6(iptables.TableNAT, ChainNATIPVSLBOutput, req.String(), ruleOutDNAT...) - if err != nil { - logger.Error(err, out) - return err - } - } else { - if ip.IsVaildIP(clusterIP) { - logger.WithValues("clusterIP", clusterIP).Error(errors.New("invalid IP"), "invaild IP") + // Create rules + logger.WithValues("externalIP", externalIP).WithValues("clusterIP", clusterIP).Info("create iptables rules") + if err := createRulesExternalCluster(logger, &req, clusterIP, externalIP, podCIDRIPv4, podCIDRIPv6); err != nil { + return ctrl.Result{}, err + } } } - return nil -} -func getPodCIDR(cidrs []string) (ipv4CIDR string, ipv6CIDR string) { - for _, cidr := range cidrs { - addr := strings.Split(cidr, "/")[0] - if ip.IsIPv4Addr(addr) { - ipv4CIDR = cidr - } else if ip.IsIPv6Addr(addr) { - ipv6CIDR = cidr - } - } - return + return ctrl.Result{}, nil } -func getSvcInfoFromRule(rule string) (nsName, src, dest, jump, dnatDest string) { - nsName = iptables.GetRuleComment(rule) - src = iptables.GetRuleSrc(rule) - dest = iptables.GetRuleDest(rule) - jump = iptables.GetRuleJump(rule) - dnatDest = iptables.GetRuleDNATDest(rule) - return +func (r *ServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Set controller manager + return ctrl.NewControllerManagedBy(mgr). + For(&corev1.Service{}). + Complete(r) } diff --git a/deploy/network-node-manager.yml b/deploy/network-node-manager_iptables.yml similarity index 100% rename from deploy/network-node-manager.yml rename to deploy/network-node-manager_iptables.yml diff --git a/deploy/network-node-manager_ipvs.yml b/deploy/network-node-manager_ipvs.yml new file mode 100644 index 0000000..f1cf0eb --- /dev/null +++ b/deploy/network-node-manager_ipvs.yml @@ -0,0 +1,97 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: network-node-manager-role +rules: +- apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - services/status + verbs: + - get + - patch +- apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - watch + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: network-node-manager + namespace: kube-system + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: network-node-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: network-node-manager-role +subjects: +- kind: ServiceAccount + name: network-node-manager + namespace: kube-system + +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: network-node-manager + namespace: kube-system + labels: + control-plane: network-node-manager +spec: + selector: + matchLabels: + control-plane: network-node-manager + template: + metadata: + labels: + control-plane: network-node-manager + spec: + serviceAccountName: network-node-manager + hostNetwork: true + containers: + - command: + - /network-node-manager + args: + - --metrics-addr=0 + image: kakaocorp/network-node-manager:latest + name: network-node-manager + resources: + limits: + cpu: 100m + memory: 100Mi + requests: + cpu: 100m + memory: 100Mi + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: RULE_EXTERNAL_CLUSTER + value: "true" + securityContext: + capabilities: + add: ["NET_ADMIN"] + tolerations: + - effect: NoSchedule + key: node-role.kubernetes.io/master diff --git a/img/network-node-manager.pptx b/img/network-node-manager.pptx new file mode 100644 index 0000000000000000000000000000000000000000..7b4408a21b056e47589d46ce005cfcaf4b03345c GIT binary patch literal 50608 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 literal 0 HcmV?d00001 diff --git a/img/network-node-manager_Architecture.PNG b/img/network-node-manager_Architecture.PNG new file mode 100644 index 0000000000000000000000000000000000000000..503b184f15d15074e59a2c76020678c153dac17a GIT binary patch literal 22437 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