Skip to content

Commit a0442e5

Browse files
committed
fix: allow basic ICMPv6 neighbor discovery
This fixes the problem where if network policy is applied before any communication between two pods, all subsequent communication fails because the two pods aren't able to discovery each other.
1 parent eac472c commit a0442e5

File tree

4 files changed

+114
-38
lines changed

4 files changed

+114
-38
lines changed

pkg/controllers/netpol/network_policy_controller.go

+19-6
Original file line numberDiff line numberDiff line change
@@ -581,12 +581,7 @@ func (npc *NetworkPolicyController) ensureExplicitAccept() {
581581

582582
// Creates custom chains KUBE-NWPLCY-DEFAULT
583583
func (npc *NetworkPolicyController) ensureDefaultNetworkPolicyChain() {
584-
for _, iptablesCmdHandler := range npc.iptablesCmdHandlers {
585-
markArgs := make([]string, 0)
586-
markComment := "rule to mark traffic matching a network policy"
587-
markArgs = append(markArgs, "-j", "MARK", "-m", "comment", "--comment", markComment,
588-
"--set-xmark", "0x10000/0x10000")
589-
584+
for family, iptablesCmdHandler := range npc.iptablesCmdHandlers {
590585
exists, err := iptablesCmdHandler.ChainExists("filter", kubeDefaultNetpolChain)
591586
if err != nil {
592587
klog.Fatalf("failed to check for the existence of chain %s, error: %v", kubeDefaultNetpolChain, err)
@@ -598,6 +593,24 @@ func (npc *NetworkPolicyController) ensureDefaultNetworkPolicyChain() {
598593
kubeDefaultNetpolChain, err.Error())
599594
}
600595
}
596+
597+
// Add common IPv4/IPv6 ICMP rules to the default network policy chain to ensure that pods communicate properly
598+
icmpRules := utils.CommonICMPRules(family)
599+
for _, icmpRule := range icmpRules {
600+
icmpArgs := []string{"-m", "comment", "--comment", icmpRule.Comment, "-p", icmpRule.IPTablesProto,
601+
icmpRule.IPTablesType, icmpRule.ICMPType, "-j", "ACCEPT"}
602+
err = iptablesCmdHandler.AppendUnique("filter", kubeDefaultNetpolChain, icmpArgs...)
603+
if err != nil {
604+
klog.Fatalf("failed to run iptables command: %v", err)
605+
}
606+
}
607+
608+
// Start off by marking traffic with an invalid mark so that we can allow list only traffic accepted by a
609+
// matching policy. Anything that still has 0x10000
610+
markArgs := make([]string, 0)
611+
markComment := "rule to mark traffic matching a network policy"
612+
markArgs = append(markArgs, "-j", "MARK", "-m", "comment", "--comment", markComment,
613+
"--set-xmark", "0x10000/0x10000")
601614
err = iptablesCmdHandler.AppendUnique("filter", kubeDefaultNetpolChain, markArgs...)
602615
if err != nil {
603616
klog.Fatalf("Failed to run iptables command: %s", err.Error())

pkg/controllers/proxy/network_services_controller.go

+9-32
Original file line numberDiff line numberDiff line change
@@ -473,47 +473,24 @@ func (nsc *NetworkServicesController) setupIpvsFirewall() error {
473473
var comment string
474474
var args []string
475475
var exists bool
476-
var icmpProto string
477-
var icmpType string
478476
var icmpRejectType string
479-
480477
//nolint:exhaustive // we don't need exhaustive searching for IP Families
481478
switch family {
482479
case v1.IPv4Protocol:
483-
icmpProto = "icmp"
484-
icmpType = "--icmp-type"
485480
icmpRejectType = "icmp-port-unreachable"
486481
case v1.IPv6Protocol:
487-
icmpProto = "ipv6-icmp"
488-
icmpType = "--icmpv6-type"
489482
icmpRejectType = "icmp6-port-unreachable"
490483
}
491484

492-
// Allow various types of ICMP that are important for routing
493-
comment = "allow icmp echo requests to service IPs"
494-
args = []string{"-m", "comment", "--comment", comment, "-p", icmpProto, icmpType, "echo-request",
495-
"-j", "ACCEPT"}
496-
err = iptablesCmdHandler.AppendUnique("filter", ipvsFirewallChainName, args...)
497-
if err != nil {
498-
return fmt.Errorf("failed to run iptables command: %s", err.Error())
499-
}
500-
501-
comment = "allow icmp ttl exceeded messages to service IPs"
502-
args = []string{"-m", "comment", "--comment", comment, "-p", icmpProto, icmpType, "time-exceeded",
503-
"-j", "ACCEPT"}
504-
err = iptablesCmdHandler.AppendUnique("filter", ipvsFirewallChainName, args...)
505-
if err != nil {
506-
return fmt.Errorf("failed to run iptables command: %s", err.Error())
507-
}
508-
509-
// destination-unreachable here is also responsible for handling / allowing
510-
// PMTU (https://en.wikipedia.org/wiki/Path_MTU_Discovery) responses
511-
comment = "allow icmp destination unreachable messages to service IPs"
512-
args = []string{"-m", "comment", "--comment", comment, "-p", icmpProto, icmpType, "destination-unreachable",
513-
"-j", "ACCEPT"}
514-
err = iptablesCmdHandler.AppendUnique("filter", ipvsFirewallChainName, args...)
515-
if err != nil {
516-
return fmt.Errorf("failed to run iptables command: %s", err.Error())
485+
// Add common IPv4/IPv6 ICMP rules to the default network policy chain to ensure that pods communicate properly
486+
icmpRules := utils.CommonICMPRules(family)
487+
for _, icmpRule := range icmpRules {
488+
icmpArgs := []string{"-m", "comment", "--comment", icmpRule.Comment, "-p", icmpRule.IPTablesProto,
489+
icmpRule.IPTablesType, icmpRule.ICMPType, "-j", "ACCEPT"}
490+
err = iptablesCmdHandler.AppendUnique("filter", ipvsFirewallChainName, icmpArgs...)
491+
if err != nil {
492+
return fmt.Errorf("failed to run iptables command: %v", err)
493+
}
517494
}
518495

519496
// Get into specific service specific allowances

pkg/utils/iptables.go

+40
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ import (
1414

1515
var hasWait bool
1616

17+
const (
18+
ICMPv4Proto = "icmp"
19+
ICMPv4Type = "--icmp-type"
20+
ICMPv6Proto = "ipv6-icmp"
21+
ICMPv6Type = "--icmpv6-type"
22+
)
23+
1724
// IPTablesHandler interface based on the IPTables struct from github.com./coreos/go-iptables
1825
// which allows to mock it.
1926
type IPTablesHandler interface {
@@ -43,6 +50,13 @@ type IPTablesHandler interface {
4350
GetIptablesVersion() (int, int, int)
4451
}
4552

53+
type ICMPRule struct {
54+
IPTablesProto string
55+
IPTablesType string
56+
ICMPType string
57+
Comment string
58+
}
59+
4660
//nolint:gochecknoinits // This is actually a good usage of the init() function
4761
func init() {
4862
path, err := exec.LookPath("iptables-restore")
@@ -181,3 +195,29 @@ func (i *IPTablesSaveRestore) Restore(table string, data []byte) error {
181195
}
182196
return i.exec(i.restoreCmd, args, data, nil)
183197
}
198+
199+
// CommonICMPRules returns a list of common ICMP rules that should always be allowed for given IP family
200+
func CommonICMPRules(family v1core.IPFamily) []ICMPRule {
201+
// Allow various types of ICMP that are important for routing
202+
// This first block applies to both IPv4 and IPv6 type rules
203+
icmpRules := []ICMPRule{
204+
{ICMPv4Proto, ICMPv4Type, "echo-request", "allow icmp echo requests"},
205+
// destination-unreachable here is also responsible for handling / allowing PMTU
206+
// (https://en.wikipedia.org/wiki/Path_MTU_Discovery) responses
207+
{ICMPv4Proto, ICMPv4Type, "destination-unreachable", "allow icmp destination unreachable messages"},
208+
{ICMPv4Proto, ICMPv4Type, "time-exceeded", "allow icmp time exceeded messages"},
209+
}
210+
211+
if family == v1core.IPv6Protocol {
212+
// Neighbor discovery packets are especially crucial here as without them pods will not communicate properly
213+
// over IPv6. Neighbor discovery packets are essentially like ARP for IPv4 which was always allowed under.
214+
// previous kube-router versions.
215+
icmpRules = append(icmpRules, []ICMPRule{
216+
{ICMPv6Proto, ICMPv6Type, "neighbor-solicitation", "allow icmp neighbor solicitation messages"},
217+
{ICMPv6Proto, ICMPv6Type, "neighbor-advertisement", "allow icmp neighbor advertisement messages"},
218+
{ICMPv6Proto, ICMPv6Type, "echo-reply", "allow icmp echo reply messages"},
219+
}...)
220+
}
221+
222+
return icmpRules
223+
}

pkg/utils/iptables_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package utils
2+
3+
import (
4+
"testing"
5+
6+
"github.com./stretchr/testify/assert"
7+
8+
v1core "k8s.io/api/core/v1"
9+
)
10+
11+
func TestCommonICMPRules(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
family v1core.IPFamily
15+
expected []ICMPRule
16+
}{
17+
{
18+
name: "IPv4",
19+
family: v1core.IPv4Protocol,
20+
expected: []ICMPRule{
21+
{"icmp", "--icmp-type", "echo-request", "allow icmp echo requests"},
22+
{"icmp", "--icmp-type", "destination-unreachable", "allow icmp destination unreachable messages"},
23+
{"icmp", "--icmp-type", "time-exceeded", "allow icmp time exceeded messages"},
24+
},
25+
},
26+
{
27+
name: "IPv6",
28+
family: v1core.IPv6Protocol,
29+
expected: []ICMPRule{
30+
{"icmp", "--icmp-type", "echo-request", "allow icmp echo requests"},
31+
{"icmp", "--icmp-type", "destination-unreachable", "allow icmp destination unreachable messages"},
32+
{"icmp", "--icmp-type", "time-exceeded", "allow icmp time exceeded messages"},
33+
{"ipv6-icmp", "--icmpv6-type", "neighbor-solicitation", "allow icmp neighbor solicitation messages"},
34+
{"ipv6-icmp", "--icmpv6-type", "neighbor-advertisement", "allow icmp neighbor advertisement messages"},
35+
{"ipv6-icmp", "--icmpv6-type", "echo-reply", "allow icmp echo reply messages"},
36+
},
37+
},
38+
}
39+
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
result := CommonICMPRules(tt.family)
43+
assert.Equal(t, tt.expected, result, "CommonICMPRules(%v) = %v, want %v", tt.family, result, tt.expected)
44+
})
45+
}
46+
}

0 commit comments

Comments
 (0)