Skip to content

Commit d97bacf

Browse files
committed
apply network policies to existing connections.
The dataplane uses conntrack labels to identify the connections that are already processed and skip the ones that are processed and established. The dataplane now inspect the existing connections in the conntrack table and evaluates against the current network policies, if one of the connections is no longer valid the label is removed, so the packets gets requeued and reevaluated. The strict mode is enabled by default and runs at most every 30 seconds once there is a change triggered in the dataplane, this is to avoid performance issues for listing conntrack entries too often.
1 parent 8a5c150 commit d97bacf

13 files changed

Lines changed: 652 additions & 21 deletions

File tree

cmd/kube-network-policies/iptracker/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func run() int {
123123
FailOpen: opts.FailOpen,
124124
QueueID: opts.QueueID,
125125
NetfilterBug1766Fix: opts.NetfilterBug1766Fix,
126+
StrictMode: opts.StrictMode,
126127
}
127128

128129
var config *rest.Config

cmd/kube-network-policies/npa-v1alpha2/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func run() int {
8383
FailOpen: opts.FailOpen,
8484
QueueID: opts.QueueID,
8585
NetfilterBug1766Fix: opts.NetfilterBug1766Fix,
86+
StrictMode: opts.StrictMode,
8687
}
8788

8889
var config *rest.Config

cmd/kube-network-policies/standard/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func run() int {
7878
FailOpen: opts.FailOpen,
7979
QueueID: opts.QueueID,
8080
NetfilterBug1766Fix: opts.NetfilterBug1766Fix,
81+
StrictMode: opts.StrictMode,
8182
}
8283

8384
var config *rest.Config

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.24.3
55
require (
66
github.com/armon/go-radix v1.0.0
77
github.com/containerd/nri v0.10.0
8-
github.com/florianl/go-nfqueue/v2 v2.0.1
8+
github.com/florianl/go-nfqueue/v2 v2.0.2
99
github.com/google/go-cmp v0.7.0
1010
github.com/google/nftables v0.3.0
1111
github.com/mdlayher/netlink v1.8.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
2929
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3030
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
3131
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
32-
github.com/florianl/go-nfqueue/v2 v2.0.1 h1:UNVaW5YSAH2vpQcJ+lK17OHiArPTdd1z57OBE/rymuI=
33-
github.com/florianl/go-nfqueue/v2 v2.0.1/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
32+
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
33+
github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
3434
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
3535
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
3636
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Options struct {
2525
HostnameOverride string
2626
NetfilterBug1766Fix bool
2727
DisableNRI bool
28+
StrictMode bool
2829
}
2930

3031
// NewOptions creates a new Options object with default values.
@@ -41,6 +42,7 @@ func (o *Options) AddFlags(fs *flag.FlagSet) {
4142
fs.StringVar(&o.HostnameOverride, "hostname-override", "", "If non-empty, will be used as the name of the Node that kube-network-policies is running on. If unset, the node name is assumed to be the same as the node's hostname.")
4243
fs.BoolVar(&o.NetfilterBug1766Fix, "netfilter-bug-1766-fix", true, "If set, process DNS packets on the PREROUTING hooks to avoid the race condition on the conntrack subsystem, not needed for kernels 6.12+ (see https://bugzilla.netfilter.org/show_bug.cgi?id=1766)")
4344
fs.BoolVar(&o.DisableNRI, "disable-nri", false, "If set, disable NRI, that is used to get the Pod IP information directly from the runtime to avoid the race explained in https://issues.k8s.io/85966")
45+
fs.BoolVar(&o.StrictMode, "strict-mode", true, "If set, changes to network policies also affect established connections")
4446

4547
fs.Usage = func() {
4648
fmt.Fprint(os.Stderr, "Usage: kube-network-policies [options]\n\n")

pkg/dataplane/conntrack.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package dataplane
2+
3+
import (
4+
"github.com/vishvananda/netlink"
5+
"golang.org/x/sys/unix"
6+
v1 "k8s.io/api/core/v1"
7+
"k8s.io/klog/v2"
8+
"sigs.k8s.io/kube-network-policies/pkg/network"
9+
)
10+
11+
var (
12+
mapIPFamilyToString = map[uint8]v1.IPFamily{
13+
unix.AF_INET: v1.IPv4Protocol,
14+
unix.AF_INET6: v1.IPv6Protocol,
15+
}
16+
mapProtocolToString = map[uint8]v1.Protocol{
17+
unix.IPPROTO_TCP: v1.ProtocolTCP,
18+
unix.IPPROTO_UDP: v1.ProtocolUDP,
19+
unix.IPPROTO_SCTP: v1.ProtocolSCTP,
20+
}
21+
)
22+
23+
func PacketFromFlow(flow *netlink.ConntrackFlow) *network.Packet {
24+
if flow == nil {
25+
return nil
26+
}
27+
packet := network.Packet{
28+
SrcIP: flow.Forward.SrcIP,
29+
DstIP: flow.Reverse.SrcIP,
30+
SrcPort: int(flow.Forward.SrcPort),
31+
DstPort: int(flow.Reverse.SrcPort),
32+
}
33+
34+
if family, ok := mapIPFamilyToString[flow.FamilyType]; ok {
35+
packet.Family = family
36+
} else {
37+
klog.InfoS("Unknown IP family", "family", flow.FamilyType, "flow", flow)
38+
return nil
39+
}
40+
41+
if protocol, ok := mapProtocolToString[flow.Forward.Protocol]; ok {
42+
packet.Proto = protocol
43+
} else {
44+
klog.InfoS("Unknown protocol", "protocol", flow.Forward.Protocol, "flow", flow)
45+
return nil
46+
}
47+
48+
return &packet
49+
}
50+
51+
// generateLabelMask creates a 16-byte (128-bit) mask with a single bit set at the
52+
// specified bitIndex.
53+
// If the bit index is out of the valid range [0, 127], it returns a 16-byte
54+
// slice of all zeros.
55+
// This function implements a Big Endia 128-bit layout. This means the
56+
// most significant byte (containing bits 127-120) is at index 0 of the
57+
// slice, and the least significant *byte* (containing bits 7-0) is at
58+
// index 15.
59+
func generateLabelMask(bitIndex int) []byte {
60+
labelMask := make([]byte, 16)
61+
if bitIndex < 0 || bitIndex > 127 {
62+
return labelMask
63+
}
64+
65+
arrayIndex := len(labelMask) - (bitIndex / 8) - 1
66+
bitPos := uint(bitIndex % 8)
67+
mask := uint8(1) << bitPos
68+
labelMask[arrayIndex] = mask
69+
return labelMask
70+
}
71+
72+
// clearLabelBit clears a specific bit in a 16-byte (128-bit) label and returns
73+
// a new 16-byte slice with the modified label. The original slice (currentLabel)
74+
// is not modified.
75+
// If currentLabel is not 16 bytes long, it returns a new, empty 16-byte slice.
76+
// If bitIndex is out of the valid range [0, 127], it returns a copy of the
77+
// original label.
78+
func clearLabelBit(currentLabel []byte, bitIndex int) []byte {
79+
newLabel := make([]byte, 16)
80+
if len(currentLabel) != 16 {
81+
return newLabel
82+
}
83+
84+
copy(newLabel, currentLabel)
85+
if bitIndex < 0 || bitIndex > 127 {
86+
return newLabel
87+
}
88+
arrayIndex := len(newLabel) - (bitIndex / 8) - 1
89+
bitPos := uint(bitIndex % 8)
90+
zeroMask := ^(uint8(1) << bitPos)
91+
newLabel[arrayIndex] &= zeroMask
92+
return newLabel
93+
}

pkg/dataplane/conntrack_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package dataplane
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
)
7+
8+
func TestGenerateLabelMask(t *testing.T) {
9+
// The expected results are derived from the nftables debug output,
10+
// serialized as a 16-byte Big-Endian array (MSW first, LSW last).
11+
tests := []struct {
12+
name string
13+
bitIndex int
14+
expected string // Expected 16-byte hex string
15+
}{
16+
{
17+
name: "Bit 10 (LSW)",
18+
bitIndex: 10,
19+
// Bit 10 is 2^10 = 0x400. This is in the LSW (last 8 bytes).
20+
expected: "00000000000000000000000000000400",
21+
},
22+
{
23+
name: "Bit 126 (MSW)",
24+
bitIndex: 126,
25+
// Bit 126 is 2^62 within the 64-bit MSW (first 8 bytes). 0x4000000000000000
26+
expected: "40000000000000000000000000000000",
27+
},
28+
{
29+
name: "Bit 127 (MSW)",
30+
bitIndex: 127,
31+
// Bit 127 is 2^63 within the 64-bit MSW (first 8 bytes). 0x8000000000000000
32+
expected: "80000000000000000000000000000000",
33+
},
34+
{
35+
name: "Bit 0 (LSW Start)",
36+
bitIndex: 0,
37+
// 2^0 = 0x1. In the LSW (last byte).
38+
expected: "00000000000000000000000000000001",
39+
},
40+
{
41+
name: "Bit 63 (LSW End)",
42+
bitIndex: 63,
43+
// 2^63 = 0x8000000000000000. In the LSW (last 8 bytes).
44+
expected: "00000000000000008000000000000000",
45+
},
46+
{
47+
name: "Bit 64 (MSW Start)",
48+
bitIndex: 64,
49+
// 2^0 (within the MSW). In the MSW (first 8 bytes).
50+
expected: "00000000000000010000000000000000",
51+
},
52+
{
53+
name: "Out of Range (128)",
54+
bitIndex: 128,
55+
// Expected 16 zero bytes: "00...00"
56+
expected: "00000000000000000000000000000000",
57+
},
58+
{
59+
name: "Out of Range (-1)",
60+
bitIndex: -1,
61+
// Expected 16 zero bytes: "00...00"
62+
expected: "00000000000000000000000000000000",
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
// Call the function
69+
result := generateLabelMask(tt.bitIndex)
70+
71+
// Convert result to hex string for easy comparison
72+
actualHex := hex.EncodeToString(result)
73+
74+
// Compare the actual result with the expected hex string
75+
if actualHex != tt.expected {
76+
t.Errorf("generateLabelMask() for index %d:\n Got: %v\n Want: %v", tt.bitIndex, actualHex, tt.expected)
77+
}
78+
})
79+
}
80+
}
81+
82+
// TestClearLabelBit tests the clearLabelBit function across various scenarios.
83+
func TestClearLabelBit(t *testing.T) {
84+
// Helper function to convert a hex string to a byte slice
85+
mustDecodeHex := func(s string) []byte {
86+
b, err := hex.DecodeString(s)
87+
if err != nil {
88+
panic(err)
89+
}
90+
return b
91+
}
92+
93+
// A base label with bits 10, 63, 64, and 127 set.
94+
// Bit 127 (MSW: 0x8000000000000000)
95+
// Bit 64 (MSW: 0x0000000000000001)
96+
// Bit 63 (LSW: 0x8000000000000000)
97+
// Bit 10 (LSW: 0x0000000000000400)
98+
// Base Hex: 80000000000000018000000000000400
99+
baseLabelHex := "80000000000000018000000000000400"
100+
baseLabel := mustDecodeHex(baseLabelHex)
101+
102+
tests := []struct {
103+
name string
104+
initialLabel []byte
105+
bitIndex int
106+
expectedHex string
107+
expectChange bool // Used to verify if the original array remains untouched
108+
}{
109+
{
110+
name: "Clear Bit 10 (LSW Middle)",
111+
initialLabel: baseLabel,
112+
bitIndex: 10,
113+
// Expected: Bit 10 (0x400) cleared -> 8000...018000...0000
114+
expectedHex: "80000000000000018000000000000000",
115+
expectChange: true,
116+
},
117+
{
118+
name: "Clear Bit 127 (MSW End)",
119+
initialLabel: baseLabel,
120+
bitIndex: 127,
121+
// Expected: Bit 127 (0x80...) cleared -> 0000...018000...0400
122+
expectedHex: "00000000000000018000000000000400",
123+
expectChange: true,
124+
},
125+
{
126+
name: "Clear Bit 63 (LSW End Boundary)",
127+
initialLabel: baseLabel,
128+
bitIndex: 63,
129+
// Expected: Bit 63 (0x80...) cleared -> 8000...010000...0400
130+
expectedHex: "80000000000000010000000000000400",
131+
expectChange: true,
132+
},
133+
{
134+
name: "Clear Bit 64 (MSW Start Boundary)",
135+
initialLabel: baseLabel,
136+
bitIndex: 64,
137+
// Expected: Bit 64 (0x01) cleared -> 8000...008000...0400
138+
expectedHex: "80000000000000008000000000000400",
139+
expectChange: true,
140+
},
141+
{
142+
name: "Clear Bit 0 (LSW Start Boundary)",
143+
initialLabel: mustDecodeHex("00000000000000000000000000000001"), // Only bit 0 set
144+
bitIndex: 0,
145+
// Expected: All zeros
146+
expectedHex: "00000000000000000000000000000000",
147+
expectChange: true,
148+
},
149+
{
150+
name: "Clear Bit Already Zero (Bit 50)",
151+
initialLabel: baseLabel,
152+
bitIndex: 50, // Bit 50 is zero in the base label
153+
expectedHex: baseLabelHex,
154+
expectChange: true, // A copy is still returned, but the content is the same
155+
},
156+
{
157+
name: "Out of Range (128)",
158+
initialLabel: baseLabel,
159+
bitIndex: 128,
160+
expectedHex: baseLabelHex,
161+
expectChange: true, // A copy is still returned, but the content is the same
162+
},
163+
{
164+
name: "Out of Range (-1)",
165+
initialLabel: baseLabel,
166+
bitIndex: -1,
167+
expectedHex: baseLabelHex,
168+
expectChange: true, // A copy is still returned, but the content is the same
169+
},
170+
{
171+
name: "Invalid Length (10 bytes)",
172+
initialLabel: mustDecodeHex("F0F0F0F0F0"), // Only 5 bytes
173+
bitIndex: 10,
174+
expectedHex: "00000000000000000000000000000000", // Should return 16 zero bytes
175+
expectChange: true,
176+
},
177+
}
178+
179+
for _, tt := range tests {
180+
t.Run(tt.name, func(t *testing.T) {
181+
// Save the original hex string for verification
182+
originalHex := hex.EncodeToString(tt.initialLabel)
183+
184+
// Execute the function
185+
result := clearLabelBit(tt.initialLabel, tt.bitIndex)
186+
187+
actualHex := hex.EncodeToString(result)
188+
if actualHex != tt.expectedHex {
189+
t.Errorf("Result Mismatch for index %d:\n Got: %s\n Want: %s", tt.bitIndex, actualHex, tt.expectedHex)
190+
}
191+
192+
if len(tt.initialLabel) == 16 && originalHex != hex.EncodeToString(tt.initialLabel) {
193+
t.Errorf("Original array was modified!\n Initial: %s\n After call: %s", originalHex, hex.EncodeToString(tt.initialLabel))
194+
}
195+
})
196+
}
197+
}

0 commit comments

Comments
 (0)