Skip to content

Commit cdcf4f4

Browse files
feat(firewall): allow FirewallConfiguration rules to match named sets
1 parent 6b0033a commit cdcf4f4

File tree

15 files changed

+570
-12
lines changed

15 files changed

+570
-12
lines changed

apis/networking/v1beta1/firewall/common_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const (
2626
IPValueTypeVoid IPValueType = "void"
2727
// IPValueTypeRange is a string representing a range of IPs (eg. 10.0.0.1-10.0.0.20).
2828
IPValueTypeRange IPValueType = "range"
29+
// IPValueTypeNamedSet is a string representing the name of an IP set (eg. @my_ip_set).
30+
IPValueTypeNamedSet IPValueType = "namedset"
2931
)
3032

3133
// PortValueType is the type of the match value.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2019-2025 The Liqo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package firewall
16+
17+
// SetDataType is the type of a set element
18+
// +kubebuilder:validation:Enum="ipv4_addr"
19+
type SetDataType string
20+
21+
// Possible SetDataType values.
22+
const (
23+
SetTypeIPAddr SetDataType = "ipv4_addr"
24+
)
25+
26+
// Set represents a nftables set
27+
// +kubebuilder:object:generate=true
28+
type Set struct {
29+
// Name is the name of the set.
30+
// +kubebuilder:validation:MinLength=1
31+
// +kubebuilder:validation:MaxLength=200
32+
// +kubebuilder:validation:Pattern=`^[a-zA-Z][a-zA-Z0-9/\\_.]*$`
33+
Name string `json:"name"`
34+
35+
// KeyType is the type of the set keys.
36+
KeyType SetDataType `json:"keyType"`
37+
38+
// DataType is the type of the set data.
39+
// +kubebuilder:validation:Optional
40+
DataType *SetDataType `json:"dataType,omitempty"`
41+
42+
// Elements are the elements of the set.
43+
// +kubebuilder:validation:Optional
44+
Elements []SetElement `json:"elements,omitempty"`
45+
}
46+
47+
// SetElement represents an element of a nftables set
48+
// +kubebuilder:object:generate=true
49+
type SetElement struct {
50+
// Key is the key of the set element.
51+
Key string `json:"key"`
52+
53+
// Data is the data of the set element.
54+
// +kubebuilder:validation:Optional
55+
Data *string `json:"data,omitempty"`
56+
}

apis/networking/v1beta1/firewall/table_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ type Table struct {
3939
// Family is the family of the table.
4040
// +kubebuilder:validation:Enum="INET";"IPV4";"IPV6";"ARP";"NETDEV";"BRIDGE"
4141
Family *TableFamily `json:"family"`
42+
// Sets is a list of sets to be applied to the table.
43+
// +kubebuilder:validation:Optional
44+
Sets []Set `json:"sets,omitempty"`
4245
}

deployments/liqo/charts/liqo-crds/crds/networking.liqo.io_firewallconfigurations.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,48 @@ spec:
418418
name:
419419
description: Name is the name of the table.
420420
type: string
421+
sets:
422+
description: Sets is a list of sets to be applied to the table.
423+
items:
424+
description: Set represents a nftables set
425+
properties:
426+
dataType:
427+
description: DataType is the type of the set data.
428+
enum:
429+
- ipv4_addr
430+
type: string
431+
elements:
432+
description: Elements are the elements of the set.
433+
items:
434+
description: SetElement represents an element of a nftables
435+
set
436+
properties:
437+
data:
438+
description: Data is the data of the set element.
439+
type: string
440+
key:
441+
description: Key is the key of the set element.
442+
type: string
443+
required:
444+
- key
445+
type: object
446+
type: array
447+
keyType:
448+
description: KeyType is the type of the set keys.
449+
enum:
450+
- ipv4_addr
451+
type: string
452+
name:
453+
description: Name is the name of the set.
454+
maxLength: 200
455+
minLength: 1
456+
pattern: ^[a-zA-Z][a-zA-Z0-9/\\_.]*$
457+
type: string
458+
required:
459+
- keyType
460+
- name
461+
type: object
462+
type: array
421463
required:
422464
- family
423465
- name

pkg/firewall/firewallconfiguration_controller.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func (r *FirewallConfigurationReconciler) Reconcile(ctx context.Context, req ctr
138138
return ctrl.Result{}, err
139139
}
140140

141-
// We need to flush the updates to allow the recreation of updated chains/rules.
141+
// We need to flush the updates to allow the recreation of updated chains/rules and the usage of sets in rules.
142142
if err = r.NftConnection.Flush(); err != nil {
143143
return ctrl.Result{}, err
144144
}
@@ -148,6 +148,12 @@ func (r *FirewallConfigurationReconciler) Reconcile(ctx context.Context, req ctr
148148
// Enforce table existence.
149149
table := addTable(r.NftConnection, &fwcfg.Spec.Table)
150150

151+
// Add the missing sets
152+
if err = addSets(r.NftConnection, fwcfg.Spec.Table.Sets, table); err != nil {
153+
return ctrl.Result{}, err
154+
}
155+
156+
// Add the missing chains and rules.
151157
if err = addChains(r.NftConnection, fwcfg.Spec.Table.Chains, table); err != nil {
152158
return ctrl.Result{}, err
153159
}

pkg/firewall/set.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright 2019-2025 The Liqo Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package firewall
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/google/nftables"
21+
firewallapi "github.com/liqotech/liqo/apis/networking/v1beta1/firewall"
22+
"github.com/liqotech/liqo/pkg/firewall/utils"
23+
)
24+
25+
// cleanSets removes the sets that are no longer used in the firewall configuration and updates the existing ones if their elements differ from the wanted elements.
26+
func cleanSets(nftconn *nftables.Conn, table *firewallapi.Table) error {
27+
// Find the table by name and family
28+
nftTables, err := nftconn.ListTablesOfFamily(getTableFamily(*table.Family))
29+
if err != nil {
30+
return err
31+
}
32+
33+
var nftTable *nftables.Table
34+
for _, t := range nftTables {
35+
if t.Name == *table.Name {
36+
nftTable = t
37+
break
38+
}
39+
}
40+
41+
if nftTable == nil {
42+
// Table does not exist, nothing to clean
43+
return nil
44+
}
45+
46+
// Get all existing sets in the table
47+
nftSets, err := nftconn.GetSets(nftTable)
48+
if err != nil {
49+
return err
50+
}
51+
52+
// Remove sets that are no longer used.
53+
if err := removeOutdatedSets(nftconn, nftSets, table); err != nil {
54+
return err
55+
}
56+
57+
// Update existing sets if their elements differ from the wanted elements.
58+
if err := updateSetElements(nftconn, nftSets, table); err != nil {
59+
return err
60+
}
61+
62+
return nil
63+
}
64+
65+
// removeOutdatedSets removes the sets that are no longer used in the firewall configuration.
66+
func removeOutdatedSets(nftconn *nftables.Conn, nftSets []*nftables.Set, table *firewallapi.Table) error {
67+
// Delete sets that are not used anymore in the table
68+
usedSetNames := make(map[string]interface{})
69+
for _, set := range table.Sets {
70+
usedSetNames[set.Name] = nil
71+
}
72+
73+
for _, nftSet := range nftSets {
74+
if _, used := usedSetNames[nftSet.Name]; !used {
75+
// Set is not used anymore, delete it
76+
nftconn.DelSet(nftSet)
77+
}
78+
}
79+
80+
return nil
81+
}
82+
83+
// updateSetElements updates the elements of an existing set, if they differ from the wanted elements.
84+
func updateSetElements(nftconn *nftables.Conn, nftSets []*nftables.Set, table *firewallapi.Table) error {
85+
for _, set := range table.Sets {
86+
// Find the corresponding nftables.Set
87+
var nftSet *nftables.Set
88+
for _, ns := range nftSets {
89+
if ns.Name == set.Name {
90+
nftSet = ns
91+
break
92+
}
93+
}
94+
95+
if nftSet == nil {
96+
// Set does not exist, skip
97+
continue
98+
}
99+
100+
// Get existing elements of the set
101+
existingElements, err := nftconn.GetSetElements(nftSet)
102+
if err != nil {
103+
return err
104+
}
105+
106+
// Get wanted elements of the set
107+
wantedElements, err := genSetElements(&set)
108+
if err != nil {
109+
return err
110+
}
111+
112+
// Check if the set is outdated
113+
if isSetOutdated(existingElements, wantedElements) {
114+
// Remove the existing elements and add the wanted ones
115+
if err := nftconn.SetDeleteElements(nftSet, existingElements); err != nil {
116+
return err
117+
}
118+
if err := nftconn.SetAddElements(nftSet, wantedElements); err != nil {
119+
return err
120+
}
121+
}
122+
}
123+
124+
return nil
125+
}
126+
127+
// isSetOutdated checks if the existing set elements differ from the wanted set elements.
128+
func isSetOutdated(existingElements []nftables.SetElement, wantedElements []nftables.SetElement) bool {
129+
// If the lengths differ, the set is outdated.
130+
if len(existingElements) != len(wantedElements) {
131+
return true
132+
}
133+
134+
// Build a map of existing elements for quick lookup.
135+
existingElementsMap := make(map[string]string)
136+
for _, element := range existingElements {
137+
existingElementsMap[string(element.Key)] = string(element.Val)
138+
}
139+
140+
// Check if any wanted element is missing or differs in value.
141+
for _, wantedElement := range wantedElements {
142+
val, exists := existingElementsMap[string(wantedElement.Key)]
143+
if !exists || val != string(wantedElement.Val) {
144+
return true
145+
}
146+
}
147+
148+
return false
149+
}
150+
151+
// addSets adds the sets that are missing in the nftables configuration.
152+
func addSets(nftconn *nftables.Conn, sets []firewallapi.Set, nftTable *nftables.Table) error {
153+
nftSets, err := nftconn.GetSets(nftTable)
154+
if err != nil {
155+
return err
156+
}
157+
existingSetNames := make(map[string]struct{})
158+
for _, nftSet := range nftSets {
159+
existingSetNames[nftSet.Name] = struct{}{}
160+
}
161+
162+
for _, set := range sets {
163+
if _, exists := existingSetNames[set.Name]; !exists {
164+
_, err := addSet(nftconn, nftTable, &set)
165+
if err != nil {
166+
return err
167+
}
168+
}
169+
}
170+
return nil
171+
}
172+
173+
// addSet adds a new set to the nftables configuration.
174+
func addSet(nftconn *nftables.Conn, table *nftables.Table, set *firewallapi.Set) (*nftables.Set, error) {
175+
dataType, err := getSetDataType(set.DataType)
176+
if err != nil {
177+
return nil, err
178+
}
179+
180+
keyType, err := getSetDataType(&set.KeyType)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
nftSet := &nftables.Set{
186+
Table: table,
187+
Name: set.Name,
188+
KeyType: keyType,
189+
DataType: dataType,
190+
}
191+
192+
setData, err := genSetElements(set)
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
err = nftconn.AddSet(nftSet, setData)
198+
if err != nil {
199+
return nil, err
200+
}
201+
202+
return nftSet, nil
203+
}
204+
205+
func genSetElements(set *firewallapi.Set) ([]nftables.SetElement, error) {
206+
setData := make([]nftables.SetElement, len(set.Elements))
207+
for i, element := range set.Elements {
208+
data, err := utils.ConvertSetData(element.Data, set.DataType)
209+
if err != nil {
210+
return nil, fmt.Errorf("unable to convert set element data: %v", err)
211+
}
212+
213+
key, err := utils.ConvertSetData(&element.Key, &set.KeyType)
214+
if err != nil {
215+
return nil, fmt.Errorf("unable to convert set element key: %v", err)
216+
}
217+
218+
setData[i] = nftables.SetElement{
219+
Key: key,
220+
Val: data,
221+
}
222+
}
223+
224+
return setData, nil
225+
}
226+
227+
func getSetDataType(dataType *firewallapi.SetDataType) (nftables.SetDatatype, error) {
228+
if dataType == nil {
229+
return nftables.SetDatatype{}, nil
230+
}
231+
232+
switch *dataType {
233+
case firewallapi.SetTypeIPAddr:
234+
return nftables.TypeIPAddr, nil
235+
default:
236+
return nftables.SetDatatype{}, fmt.Errorf("unsupported set data type: %s", *dataType)
237+
}
238+
}

pkg/firewall/table.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ func cleanTable(nftconn *nftables.Conn, table *firewallapi.Table) error {
7878
return err
7979
}
8080
}
81+
82+
// Delete sets that are not used anymore in the table.
83+
if err := cleanSets(nftconn, table); err != nil {
84+
return err
85+
}
86+
8187
return nil
8288
}
8389

0 commit comments

Comments
 (0)