From 3d27ada47a611c4f80f43984d876c06037265239 Mon Sep 17 00:00:00 2001 From: Michael Reiger <47994873+mreiger@users.noreply.github.com> Date: Thu, 12 May 2022 15:08:41 +0200 Subject: [PATCH] Log accepted new connections (#118) --- README.md | 14 +++-- api/v1/firewall_types.go | 2 + .../crd/bases/metal-stack.io_firewalls.yaml | 4 ++ pkg/nftables/firewall.go | 4 +- pkg/nftables/networkpolicy.go | 18 +++--- pkg/nftables/networkpolicy_test.go | 57 +++++++++++++++---- pkg/nftables/nftables.tpl | 2 +- pkg/nftables/rendering.go | 4 +- pkg/nftables/service.go | 8 +-- pkg/nftables/service_test.go | 31 +++++++--- pkg/nftables/test_data/more-rules.nftable.v4 | 2 +- pkg/nftables/test_data/simple.nftable.v4 | 2 +- pkg/nftables/test_data/validated.nftable.v4 | 2 +- pkg/nftables/util.go | 29 +++++++--- pkg/nftables/util_test.go | 12 ++-- 15 files changed, 133 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index b5b05ced..e8bd2bab 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ spec: interval: 10s # Ratelimits specify on which physical interface, which maximum rate of traffic is allowed ratelimits: + # LogAcceptedConnections specifies whether accepted connections should be logged by the firewall in addition to dropped/rejected connections + logAcceptedConnections: false # The name of the interface visible with ip link show - interface: vrf104009 # The maximum rate in MBits/s @@ -207,12 +209,12 @@ The output will look like: ```json -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:27 +0000 UTC {"DPT":"4000","DST":"1.2.3.4","ID":"54321","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"38464","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"236","URGP":"0","WINDOW":"65535","timestamp":"2020-06-17 13:23:27 +0000 UTC"} -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:34 +0000 UTC {"DPT":"2362","DST":"1.2.3.4","ID":"44545","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"242","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:34 +0000 UTC"} -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:30 +0000 UTC {"DPT":"650","DST":"1.2.3.4","ID":"12399","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"241","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:30 +0000 UTC"} -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:34 +0000 UTC {"DPT":"2362","DST":"1.2.3.4","ID":"44545","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"242","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:34 +0000 UTC"} -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:10 +0000 UTC {"DPT":"63351","DST":"1.2.3.4","ID":"11855","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"54589","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"245","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:10 +0000 UTC"} -droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:51 +0000 UTC {"DPT":"8002","DST":"1.2.3.4","ID":"17539","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"47615","SRC":"2.3.4.5","SYN":"","TOS":"0x08","TTL":"239","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:51 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:27 +0000 UTC {"ACTION":"Drop","DPT":"4000","DST":"1.2.3.4","ID":"54321","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"38464","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"236","URGP":"0","WINDOW":"65535","timestamp":"2020-06-17 13:23:27 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:34 +0000 UTC {"ACTION":"Drop","DPT":"2362","DST":"1.2.3.4","ID":"44545","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"242","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:34 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:30 +0000 UTC {"ACTION":"Accept","DPT":"650","DST":"1.2.3.4","ID":"12399","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"241","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:30 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:34 +0000 UTC {"ACTION":"Accept","DPT":"2362","DST":"1.2.3.4","ID":"44545","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"40194","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"242","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:34 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:10 +0000 UTC {"ACTION":"Accept","DPT":"63351","DST":"1.2.3.4","ID":"11855","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"vlan179","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"54589","SRC":"2.3.4.5","SYN":"","TOS":"0x00","TTL":"245","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:10 +0000 UTC"} +droptailer-6d556bd988-4g8gp droptailer 2020-06-17 13:23:51 +0000 UTC {"ACTION":"Accept","DPT":"8002","DST":"1.2.3.4","ID":"17539","IN":"vrf104009","LEN":"40","MAC":"ca:41:f9:80:fa:89:aa:bb:0e:62:8c:a6:08:00","OUT":"","PREC":"0x00","PROTO":"TCP","RES":"0x00","SPT":"47615","SRC":"2.3.4.5","SYN":"","TOS":"0x08","TTL":"239","URGP":"0","WINDOW":"1024","timestamp":"2020-06-17 13:23:51 +0000 UTC"} ``` You can forward the droptailer logs to any log aggregation infrastructure you have in place. diff --git a/api/v1/firewall_types.go b/api/v1/firewall_types.go index 44c23d88..b02611b1 100644 --- a/api/v1/firewall_types.go +++ b/api/v1/firewall_types.go @@ -61,6 +61,8 @@ type FirewallSpec struct { ControllerVersion string `json:"controllerVersion,omitempty"` // ControllerURL points to the downloadable binary artifact of the firewall controller ControllerURL string `json:"controllerURL,omitempty"` + // LogAcceptedConnections if set to true, also log accepted connections in the droptailer log + LogAcceptedConnections bool `json:"logAcceptedConnections,omitempty"` } // Data contains the fields over which the signature is calculated. diff --git a/config/crd/bases/metal-stack.io_firewalls.yaml b/config/crd/bases/metal-stack.io_firewalls.yaml index adf7a569..386e1afe 100644 --- a/config/crd/bases/metal-stack.io_firewalls.yaml +++ b/config/crd/bases/metal-stack.io_firewalls.yaml @@ -45,6 +45,10 @@ spec: spec: description: FirewallSpec defines the desired state of Firewall properties: + logAcceptedConnections: + description: LogAcceptedConnections if set to true, also log accepted connections + in the droptailer log + type: boolean controllerURL: description: ControllerURL points to the downloadable binary artifact of the firewall controller diff --git a/pkg/nftables/firewall.go b/pkg/nftables/firewall.go index af1804f0..29f41d1c 100644 --- a/pkg/nftables/firewall.go +++ b/pkg/nftables/firewall.go @@ -38,7 +38,8 @@ type Firewall struct { primaryPrivateNet *firewallv1.FirewallNetwork networkMap networkMap - dryRun bool + dryRun bool + logAcceptedConnections bool } type networkMap map[string]firewallv1.FirewallNetwork @@ -77,6 +78,7 @@ func NewFirewall(nps *firewallv1.ClusterwideNetworkPolicyList, svcs *corev1.Serv primaryPrivateNet: primaryPrivateNet, networkMap: networkMap, dryRun: spec.DryRun, + logAcceptedConnections: spec.LogAcceptedConnections, log: log, } } diff --git a/pkg/nftables/networkpolicy.go b/pkg/nftables/networkpolicy.go index 69e0461e..dabfaecd 100644 --- a/pkg/nftables/networkpolicy.go +++ b/pkg/nftables/networkpolicy.go @@ -9,18 +9,18 @@ import ( ) // clusterwideNetworkPolicyRules generates nftables rules for a clusterwidenetworkpolicy -func clusterwideNetworkPolicyRules(np firewallv1.ClusterwideNetworkPolicy) (nftablesRules, nftablesRules) { +func clusterwideNetworkPolicyRules(np firewallv1.ClusterwideNetworkPolicy, logAcceptedConnections bool) (nftablesRules, nftablesRules) { ingress, egress := nftablesRules{}, nftablesRules{} if len(np.Spec.Egress) > 0 { - egress = append(egress, clusterwideNetworkPolicyEgressRules(np)...) + egress = append(egress, clusterwideNetworkPolicyEgressRules(np, logAcceptedConnections)...) } if len(np.Spec.Ingress) > 0 { - ingress = append(ingress, clusterwideNetworkPolicyIngressRules(np)...) + ingress = append(ingress, clusterwideNetworkPolicyIngressRules(np, logAcceptedConnections)...) } return ingress, egress } -func clusterwideNetworkPolicyIngressRules(np firewallv1.ClusterwideNetworkPolicy) nftablesRules { +func clusterwideNetworkPolicyIngressRules(np firewallv1.ClusterwideNetworkPolicy, logAcceptedConnections bool) nftablesRules { ingress := np.Spec.Ingress if ingress == nil { return nil @@ -43,16 +43,16 @@ func clusterwideNetworkPolicyIngressRules(np firewallv1.ClusterwideNetworkPolicy tcpPorts, udpPorts := calculatePorts(i.Ports) comment := fmt.Sprintf("accept traffic for k8s network policy %s", np.ObjectMeta.Name) if len(tcpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(common, "tcp", tcpPorts, comment+" tcp")) + rules = append(rules, assembleDestinationPortRule(common, "tcp", tcpPorts, logAcceptedConnections, comment+" tcp")) } if len(udpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(common, "udp", udpPorts, comment+" udp")) + rules = append(rules, assembleDestinationPortRule(common, "udp", udpPorts, logAcceptedConnections, comment+" udp")) } } return uniqueSorted(rules) } -func clusterwideNetworkPolicyEgressRules(np firewallv1.ClusterwideNetworkPolicy) nftablesRules { +func clusterwideNetworkPolicyEgressRules(np firewallv1.ClusterwideNetworkPolicy, logAcceptedConnections bool) nftablesRules { egress := np.Spec.Egress if egress == nil { return nil @@ -77,10 +77,10 @@ func clusterwideNetworkPolicyEgressRules(np firewallv1.ClusterwideNetworkPolicy) } comment := fmt.Sprintf("accept traffic for np %s", np.ObjectMeta.Name) if len(tcpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(ruleBase, "tcp", tcpPorts, comment+" tcp")) + rules = append(rules, assembleDestinationPortRule(ruleBase, "tcp", tcpPorts, logAcceptedConnections, comment+" tcp")) } if len(udpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(ruleBase, "udp", udpPorts, comment+" udp")) + rules = append(rules, assembleDestinationPortRule(ruleBase, "udp", udpPorts, logAcceptedConnections, comment+" udp")) } } return uniqueSorted(rules) diff --git a/pkg/nftables/networkpolicy_test.go b/pkg/nftables/networkpolicy_test.go index 2813da5d..8777c95d 100644 --- a/pkg/nftables/networkpolicy_test.go +++ b/pkg/nftables/networkpolicy_test.go @@ -21,8 +21,10 @@ func TestClusterwideNetworkPolicyRules(t *testing.T) { udp := corev1.ProtocolUDP type want struct { - ingress nftablesRules - egress nftablesRules + ingress nftablesRules + egress nftablesRules + ingressAL nftablesRules + egressAL nftablesRules } tests := []struct { @@ -93,19 +95,36 @@ func TestClusterwideNetworkPolicyRules(t *testing.T) { `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53, 443-448 } counter accept comment "accept traffic for np tcp"`, `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } counter accept comment "accept traffic for np udp"`, }, + ingressAL: nftablesRules{ + `ip saddr != { 1.1.0.1 } ip saddr { 1.1.0.0/24 } tcp dport { 80, 443-448 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr != { 1.1.0.1 } ip saddr { 1.1.0.0/24 } tcp dport { 80, 443-448 } counter accept comment "accept traffic for k8s network policy tcp"`, + }, + egressAL: nftablesRules{ + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53, 443-448 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53, 443-448 } counter accept comment "accept traffic for np tcp"`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } counter accept comment "accept traffic for np udp"`, + }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - ingress, egress := clusterwideNetworkPolicyRules(tt.input) + ingress, egress := clusterwideNetworkPolicyRules(tt.input, false) if !cmp.Equal(ingress, tt.want.ingress) { t.Errorf("clusterwideNetworkPolicyRules() ingress diff: %v", cmp.Diff(ingress, tt.want.ingress)) } if !cmp.Equal(egress, tt.want.egress) { t.Errorf("clusterwideNetworkPolicyRules() egress diff: %v", cmp.Diff(egress, tt.want.egress)) } + ingressAL, egressAL := clusterwideNetworkPolicyRules(tt.input, true) + if !cmp.Equal(ingressAL, tt.want.ingressAL) { + t.Errorf("clusterwideNetworkPolicyRules() ingress with accessLog diff: %v", cmp.Diff(ingressAL, tt.want.ingressAL)) + } + if !cmp.Equal(egressAL, tt.want.egressAL) { + t.Errorf("clusterwideNetworkPolicyRules() egress with accessLog diff: %v", cmp.Diff(egressAL, tt.want.egressAL)) + } }) } } @@ -113,10 +132,16 @@ func TestClusterwideNetworkPolicyRules(t *testing.T) { func TestClusterwideNetworkPolicyEgressRules(t *testing.T) { tcp := corev1.ProtocolTCP udp := corev1.ProtocolUDP + + type want struct { + egress nftablesRules + egressAL nftablesRules + } + tests := []struct { name string input firewallv1.ClusterwideNetworkPolicy - want nftablesRules + want want }{ { name: "multiple protocols, multiple ip block + exception egress policy", @@ -147,18 +172,30 @@ func TestClusterwideNetworkPolicyEgressRules(t *testing.T) { }, }, }, - want: nftablesRules{ - `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53 } counter accept comment "accept traffic for np tcp"`, - `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } counter accept comment "accept traffic for np udp"`, + want: want{ + egress: nftablesRules{ + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53 } counter accept comment "accept traffic for np tcp"`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } counter accept comment "accept traffic for np udp"`, + }, + egressAL: nftablesRules{ + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } tcp dport { 53 } counter accept comment "accept traffic for np tcp"`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr == @cluster_prefixes ip daddr != { 1.1.0.1 } ip daddr { 1.1.0.0/24, 1.1.1.0/24 } udp dport { 53 } counter accept comment "accept traffic for np udp"`, + }, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - got := clusterwideNetworkPolicyEgressRules(tt.input) - if !cmp.Equal(got, tt.want) { - t.Errorf("clusterwideNetworkPolicyEgressRules() diff: %v", cmp.Diff(got, tt.want)) + egress := clusterwideNetworkPolicyEgressRules(tt.input, false) + if !cmp.Equal(egress, tt.want.egress) { + t.Errorf("clusterwideNetworkPolicyEgressRules() diff: %v", cmp.Diff(egress, tt.want.egress)) + } + egressAL := clusterwideNetworkPolicyEgressRules(tt.input, true) + if !cmp.Equal(egressAL, tt.want.egressAL) { + t.Errorf("clusterwideNetworkPolicyEgressRules() with accessLog diff: %v", cmp.Diff(egressAL, tt.want.egressAL)) } }) } diff --git a/pkg/nftables/nftables.tpl b/pkg/nftables/nftables.tpl index d393e1cf..46a71735 100644 --- a/pkg/nftables/nftables.tpl +++ b/pkg/nftables/nftables.tpl @@ -48,7 +48,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules {{- range .ForwardingRules.Ingress }} diff --git a/pkg/nftables/rendering.go b/pkg/nftables/rendering.go index 3ec009ff..d5b86151 100644 --- a/pkg/nftables/rendering.go +++ b/pkg/nftables/rendering.go @@ -25,13 +25,13 @@ func newFirewallRenderingData(f *Firewall) (*firewallRenderingData, error) { if err != nil { continue } - i, e := clusterwideNetworkPolicyRules(np) + i, e := clusterwideNetworkPolicyRules(np, f.logAcceptedConnections) ingress = append(ingress, i...) egress = append(egress, e...) } for _, svc := range f.services.Items { - ingress = append(ingress, serviceRules(svc)...) + ingress = append(ingress, serviceRules(svc, f.logAcceptedConnections)...) } snatRules, err := snatRules(f) diff --git a/pkg/nftables/service.go b/pkg/nftables/service.go index 3bc39054..9ce978a9 100644 --- a/pkg/nftables/service.go +++ b/pkg/nftables/service.go @@ -19,7 +19,7 @@ func isIP(ip string) bool { } // serviceRules generates nftables rules base on a k8s service definition -func serviceRules(svc corev1.Service) nftablesRules { +func serviceRules(svc corev1.Service, logAcceptedConnections bool) nftablesRules { if svc.Spec.Type != corev1.ServiceTypeLoadBalancer && svc.Spec.Type != corev1.ServiceTypeNodePort { return nil } @@ -73,10 +73,10 @@ func serviceRules(svc corev1.Service) nftablesRules { comment := fmt.Sprintf("accept traffic for k8s service %s/%s", svc.ObjectMeta.Namespace, svc.ObjectMeta.Name) rules := nftablesRules{} if len(tcpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(ruleBase, "tcp", tcpPorts, comment)) + rules = append(rules, assembleDestinationPortRule(ruleBase, "tcp", tcpPorts, logAcceptedConnections, comment)) } if len(udpPorts) > 0 { - rules = append(rules, assembleDestinationPortRule(ruleBase, "udp", udpPorts, comment)) + rules = append(rules, assembleDestinationPortRule(ruleBase, "udp", udpPorts, logAcceptedConnections, comment)) } - return rules + return uniqueSorted(rules) } diff --git a/pkg/nftables/service_test.go b/pkg/nftables/service_test.go index b0bfe85a..589df273 100644 --- a/pkg/nftables/service_test.go +++ b/pkg/nftables/service_test.go @@ -9,10 +9,15 @@ import ( ) func TestServiceRules(t *testing.T) { + type want struct { + ingress nftablesRules + ingressAL nftablesRules + } + tests := []struct { name string input corev1.Service - want nftablesRules + want want }{ { name: "standard service type loadbalancer with restricted source IP range", @@ -42,8 +47,14 @@ func TestServiceRules(t *testing.T) { }, }, }, - want: nftablesRules{ - `ip saddr { 185.0.0.0/16, 185.1.0.0/16 } ip daddr { 185.0.0.1 } tcp dport { 443 } counter accept comment "accept traffic for k8s service test/svc"`, + want: want{ + ingress: nftablesRules{ + `ip saddr { 185.0.0.0/16, 185.1.0.0/16 } ip daddr { 185.0.0.1 } tcp dport { 443 } counter accept comment "accept traffic for k8s service test/svc"`, + }, + ingressAL: nftablesRules{ + `ip saddr { 185.0.0.0/16, 185.1.0.0/16 } ip daddr { 185.0.0.1 } tcp dport { 443 } log prefix "nftables-firewall-accepted: " limit rate 10/second`, + `ip saddr { 185.0.0.0/16, 185.1.0.0/16 } ip daddr { 185.0.0.1 } tcp dport { 443 } counter accept comment "accept traffic for k8s service test/svc"`, + }, }, }, { @@ -60,7 +71,7 @@ func TestServiceRules(t *testing.T) { }, }, }, - want: nil, + want: want{nil, nil}, }, { name: "service type clusterip is a noop", @@ -76,15 +87,19 @@ func TestServiceRules(t *testing.T) { }, }, }, - want: nil, + want: want{nil, nil}, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - got := serviceRules(tt.input) - if !cmp.Equal(got, tt.want) { - t.Errorf("serviceRules() diff: %v", cmp.Diff(got, tt.want)) + ingress := serviceRules(tt.input, false) + if !cmp.Equal(ingress, tt.want.ingress) { + t.Errorf("serviceRules() diff: %v", cmp.Diff(ingress, tt.want.ingress)) + } + ingressAL := serviceRules(tt.input, true) + if !cmp.Equal(ingressAL, tt.want.ingressAL) { + t.Errorf("serviceRules() diff: %v", cmp.Diff(ingressAL, tt.want.ingressAL)) } }) } diff --git a/pkg/nftables/test_data/more-rules.nftable.v4 b/pkg/nftables/test_data/more-rules.nftable.v4 index d4726849..d721ca46 100644 --- a/pkg/nftables/test_data/more-rules.nftable.v4 +++ b/pkg/nftables/test_data/more-rules.nftable.v4 @@ -46,7 +46,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules ingress rule 1 diff --git a/pkg/nftables/test_data/simple.nftable.v4 b/pkg/nftables/test_data/simple.nftable.v4 index 7a732a5f..387d7041 100644 --- a/pkg/nftables/test_data/simple.nftable.v4 +++ b/pkg/nftables/test_data/simple.nftable.v4 @@ -46,7 +46,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules ingress rule diff --git a/pkg/nftables/test_data/validated.nftable.v4 b/pkg/nftables/test_data/validated.nftable.v4 index d7558519..fc81708c 100644 --- a/pkg/nftables/test_data/validated.nftable.v4 +++ b/pkg/nftables/test_data/validated.nftable.v4 @@ -45,7 +45,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules ip saddr == 1.2.3.4 diff --git a/pkg/nftables/util.go b/pkg/nftables/util.go index 76fd2ca4..1e8e6f15 100644 --- a/pkg/nftables/util.go +++ b/pkg/nftables/util.go @@ -16,12 +16,16 @@ func uniqueSorted(elements []string) []string { for _, e := range elements { t[e] = true } - r := []string{} + rawRules := []string{} for k := range t { - r = append(r, k) + rawRules = append(rawRules, k) } - sort.Strings(r) - return r + sort.Strings(rawRules) + rules := []string{} + for _, r := range rawRules { // split multiline log\naccept rules for pretty nftables file formatting + rules = append(rules, strings.Split(r, "\n")...) + } + return rules } func equal(source, target string) bool { @@ -55,15 +59,24 @@ func checksum(file string) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } -func assembleDestinationPortRule(common []string, protocol string, ports []string, comment string) string { +func assembleDestinationPortRule(common []string, protocol string, ports []string, logAcceptedConnections bool, comment string) string { + logRule := "" + rule := "" parts := common parts = append(parts, fmt.Sprintf("%s dport { %s }", protocol, strings.Join(ports, ", "))) - parts = append(parts, "counter") - parts = append(parts, "accept") + if logAcceptedConnections { + logParts := append(parts, "log prefix \"nftables-firewall-accepted: \" limit rate 10/second") + logRule = strings.Join(logParts, " ") + } + parts = append(parts, "counter", "accept") if comment != "" { parts = append(parts, "comment", fmt.Sprintf(`"%s"`, comment)) } - return strings.Join(parts, " ") + rule = strings.Join(parts, " ") + if logRule != "" { + rule = logRule + "\n" + rule + } + return rule } func proto(p *corev1.Protocol) string { diff --git a/pkg/nftables/util_test.go b/pkg/nftables/util_test.go index a6a98498..eaee552c 100644 --- a/pkg/nftables/util_test.go +++ b/pkg/nftables/util_test.go @@ -28,9 +28,9 @@ table ip firewall { type ipv4_addr flags interval auto-merge - + elements = { 1.2.3.4 } - + } # Prefixes in the cluster, typically 10.x.x.x @@ -70,7 +70,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules ingress rule @@ -90,9 +90,9 @@ table ip firewall { type ipv4_addr flags interval auto-merge - + elements = { 1.2.3.4 } - + } # Prefixes in the cluster, typically 10.x.x.x @@ -132,7 +132,7 @@ table ip firewall { # icmp ip protocol icmp icmp type echo-request limit rate over 10/second burst 4 packets counter drop comment "drop ping floods" - ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter accept comment "accept icmp" + ip protocol icmp icmp type { destination-unreachable, router-solicitation, router-advertisement, time-exceeded, parameter-problem } counter log prefix "nftables-firewall-accepted: " accept comment "accept icmp" # dynamic ingress rules ingress rule