Browse code

d/libn/i/nftables: support general maps, sets

An nftables vmap is just a map whose element values are of type
`verdict`. Generalize VMap, VMapElement and Set to support any element
type.

Add a fluent API to build arbitrary tuple and mapping types from the
composition of other types or the 'typeof' an nftables expression.
Block the invalid composition of named types and `typeof` expressions at
compile time.

There are only a handful of contexts where the data type needs to be
specified: set and map definitions. Modelling set and map types as a
singular "nft type" does not align well with the semantics of nftables.
Map types are always composite types with a key and a value part. Set
types do not have a value part; it is an error to create a map with a
set type or vice versa. Encode this distinction into the Go type system
so it is a compile-time error to try to use a set type in a map context
or a map type in a set context.

Drop the 'NftType' prefix from the primitive set-type constants. The
prefix stutters with the package name and, as discussed above, it is not
accurate to call them "nft types." Verdicts cannot be used as set
elements or map keys. Provide dedicated methods to construct verdict-map
types from set types instead of modelling verdicts as types themselves.

Signed-off-by: Cory Snider <csnider@mirantis.com>

Cory Snider authored on 2026/06/09 10:30:44
Showing 4 changed files
... ...
@@ -67,31 +67,31 @@ func (n *network) configure(ctx context.Context, table nftables.Table, conf fire
67 67
 	tm.Create(nftables.Chain{Name: fwdInChain})
68 68
 	tm.Create(nftables.Chain{Name: fwdOutChain})
69 69
 
70
-	tm.Create(nftables.VMapElement{
71
-		VmapName: filtFwdInVMap,
72
-		Key:      n.config.IfName,
73
-		Verdict:  "jump " + fwdInChain,
70
+	tm.Create(nftables.MapElement{
71
+		MapName: filtFwdInVMap,
72
+		Key:     n.config.IfName,
73
+		Value:   "jump " + fwdInChain,
74 74
 	})
75
-	tm.Create(nftables.VMapElement{
76
-		VmapName: filtFwdOutVMap,
77
-		Key:      n.config.IfName,
78
-		Verdict:  "jump " + fwdOutChain,
75
+	tm.Create(nftables.MapElement{
76
+		MapName: filtFwdOutVMap,
77
+		Key:     n.config.IfName,
78
+		Value:   "jump " + fwdOutChain,
79 79
 	})
80 80
 
81 81
 	// NAT chain
82 82
 
83 83
 	tm.Create(nftables.Chain{Name: natPostRtInChain})
84
-	tm.Create(nftables.VMapElement{
85
-		VmapName: natPostroutingInVMap,
86
-		Key:      n.config.IfName,
87
-		Verdict:  "jump " + natPostRtInChain,
84
+	tm.Create(nftables.MapElement{
85
+		MapName: natPostroutingInVMap,
86
+		Key:     n.config.IfName,
87
+		Value:   "jump " + natPostRtInChain,
88 88
 	})
89 89
 
90 90
 	tm.Create(nftables.Chain{Name: chainNatPostRtOut(n.config.IfName)})
91
-	tm.Create(nftables.VMapElement{
92
-		VmapName: natPostroutingOutVMap,
93
-		Key:      n.config.IfName,
94
-		Verdict:  "jump " + chainNatPostRtOut(n.config.IfName),
91
+	tm.Create(nftables.MapElement{
92
+		MapName: natPostroutingOutVMap,
93
+		Key:     n.config.IfName,
94
+		Value:   "jump " + chainNatPostRtOut(n.config.IfName),
95 95
 	})
96 96
 
97 97
 	// Conntrack
... ...
@@ -105,9 +105,9 @@ func (nft *Nftabler) init(ctx context.Context, family nftables.Family) (nftables
105 105
 		Priority:  nftables.BaseChainPriorityFilter,
106 106
 	})
107 107
 	// Instantiate the verdict maps and add the jumps.
108
-	tm.Create(nftables.VMap{
108
+	tm.Create(nftables.Map{
109 109
 		Name:        filtFwdInVMap,
110
-		ElementType: nftables.NftTypeIfname,
110
+		ElementType: nftables.Ifname.VMap(),
111 111
 	})
112 112
 	tm.Create(nftables.Rule{
113 113
 		Chain: forwardChain,
... ...
@@ -115,9 +115,9 @@ func (nft *Nftabler) init(ctx context.Context, family nftables.Family) (nftables
115 115
 		Rule:  []string{"oifname vmap @", filtFwdInVMap},
116 116
 	})
117 117
 
118
-	tm.Create(nftables.VMap{
118
+	tm.Create(nftables.Map{
119 119
 		Name:        filtFwdOutVMap,
120
-		ElementType: nftables.NftTypeIfname,
120
+		ElementType: nftables.Ifname.VMap(),
121 121
 	})
122 122
 	tm.Create(nftables.Rule{
123 123
 		Chain: forwardChain,
... ...
@@ -135,9 +135,9 @@ func (nft *Nftabler) init(ctx context.Context, family nftables.Family) (nftables
135 135
 		Priority:  nftables.BaseChainPrioritySrcNAT,
136 136
 	})
137 137
 
138
-	tm.Create(nftables.VMap{
138
+	tm.Create(nftables.Map{
139 139
 		Name:        natPostroutingOutVMap,
140
-		ElementType: nftables.NftTypeIfname,
140
+		ElementType: nftables.Ifname.VMap(),
141 141
 	})
142 142
 	tm.Create(nftables.Rule{
143 143
 		Chain: postroutingChain,
... ...
@@ -145,9 +145,9 @@ func (nft *Nftabler) init(ctx context.Context, family nftables.Family) (nftables
145 145
 		Rule:  []string{"iifname vmap @", natPostroutingOutVMap},
146 146
 	})
147 147
 
148
-	tm.Create(nftables.VMap{
148
+	tm.Create(nftables.Map{
149 149
 		Name:        natPostroutingInVMap,
150
-		ElementType: nftables.NftTypeIfname,
150
+		ElementType: nftables.Ifname.VMap(),
151 151
 	})
152 152
 	tm.Create(nftables.Rule{
153 153
 		Chain: postroutingChain,
... ...
@@ -14,7 +14,7 @@
14 14
 //	// Apply the updates with ...
15 15
 //	err := tm.Apply(ctx)
16 16
 //
17
-// The objects are any of: [BaseChain], [Chain], [Rule], [VMap], [VMapElement],
17
+// The objects are any of: [BaseChain], [Chain], [Rule], [Map], [MapElement],
18 18
 // [Set], [SetElement]
19 19
 //
20 20
 // The modifier can be reused to apply the same set of commands again or, more
... ...
@@ -128,19 +128,86 @@ const (
128 128
 	IPv6 Family = "ip6"
129 129
 )
130 130
 
131
-// NftType enumerates nft types that can be used to define maps/sets etc.
132
-type NftType string
131
+type SetTyper interface {
132
+	setType() string
133
+}
134
+
135
+type MapTyper interface {
136
+	mapType() string
137
+}
138
+
139
+// SetType represents named nft types that can be used to define sets or
140
+// construct map types.
141
+type SetType string
133 142
 
134 143
 const (
135
-	NftTypeIPv4Addr    NftType = "ipv4_addr"
136
-	NftTypeIPv6Addr    NftType = "ipv6_addr"
137
-	NftTypeEtherAddr   NftType = "ether_addr"
138
-	NftTypeInetProto   NftType = "inet_proto"
139
-	NftTypeInetService NftType = "inet_service"
140
-	NftTypeMark        NftType = "mark"
141
-	NftTypeIfname      NftType = "ifname"
144
+	IPv4Addr    SetType = "ipv4_addr"
145
+	IPv6Addr    SetType = "ipv6_addr"
146
+	EtherAddr   SetType = "ether_addr"
147
+	InetProto   SetType = "inet_proto"
148
+	InetService SetType = "inet_service"
149
+	Mark        SetType = "mark"
150
+	Ifname      SetType = "ifname"
142 151
 )
143 152
 
153
+func (t SetType) setType() string {
154
+	return "type " + string(t)
155
+}
156
+
157
+// Concat returns the tuple type formed by concatenating t and u.
158
+func (t SetType) Concat(u SetType) SetType {
159
+	return SetType(string(t) + " . " + string(u))
160
+}
161
+
162
+// MapType represents the named type of a map element, which is a compound type
163
+// with a key and a value.
164
+type MapType string
165
+
166
+func (t MapType) mapType() string {
167
+	return "type " + string(t)
168
+}
169
+
170
+// MapTo returns the map type where t is the key type and v is the value type.
171
+func (t SetType) MapTo(v SetType) MapType {
172
+	return MapType(string(t) + " : " + string(v))
173
+}
174
+
175
+// VMap returns the map type whose elements have t as the key type and contain
176
+// a verdict as the value.
177
+func (t SetType) VMap() MapType {
178
+	return MapType(string(t) + " : verdict")
179
+}
180
+
181
+// Typeof represents an nft "typeof" expression.
182
+type Typeof string
183
+
184
+func (t Typeof) setType() string {
185
+	return "typeof " + string(t)
186
+}
187
+
188
+func (t Typeof) Concat(u Typeof) Typeof {
189
+	return Typeof(string(t) + " . " + string(u))
190
+}
191
+
192
+// MapTypeof represents the type of a map element defined by a "typeof"
193
+// expression.
194
+type MapTypeof string
195
+
196
+func (t MapTypeof) mapType() string {
197
+	return "typeof " + string(t)
198
+}
199
+
200
+// MapTo returns the map type where t is the key type and v is the value type.
201
+func (t Typeof) MapTo(v Typeof) MapTypeof {
202
+	return MapTypeof(string(t) + " : " + string(v))
203
+}
204
+
205
+// VMap returns the map type whose elements have t as the key type and contain
206
+// a verdict as the value.
207
+func (t Typeof) VMap() MapTypeof {
208
+	return MapTypeof(string(t) + " : verdict")
209
+}
210
+
144 211
 // Enable tries once to initialise nftables.
145 212
 func Enable() error {
146 213
 	enableOnce.Do(func() {
... ...
@@ -183,7 +250,7 @@ type table struct {
183 183
 	Name   string
184 184
 	Family Family
185 185
 
186
-	VMaps  map[string]*vMap
186
+	Maps   map[string]*nftMap
187 187
 	Sets   map[string]*set
188 188
 	Chains map[string]*chain
189 189
 
... ...
@@ -225,7 +292,7 @@ func NewTable(family Family, name string) (Table, error) {
225 225
 		t: &table{
226 226
 			Name:      name,
227 227
 			Family:    family,
228
-			VMaps:     map[string]*vMap{},
228
+			Maps:      map[string]*nftMap{},
229 229
 			Sets:      map[string]*set{},
230 230
 			Chains:    map[string]*chain{},
231 231
 			MustFlush: true,
... ...
@@ -277,13 +344,13 @@ func (t Table) Family() Family {
277 277
 //   - add new map/set elements
278 278
 const incrementalUpdateTemplText = `{{$family := .Family}}{{$tableName := .Name}}
279 279
 table {{$family}} {{$tableName}} {
280
-	{{range .VMaps}}map {{.Name}} {
281
-		type {{.ElementType}} : verdict
280
+	{{range .Maps}}map {{.Name}} {
281
+		{{.ElementTypeExpr}}
282 282
 		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
283 283
 	}
284 284
 	{{end}}
285 285
 	{{range .Sets}}set {{.Name}} {
286
-		type {{.ElementType}}
286
+		{{.ElementTypeExpr}}
287 287
 		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
288 288
 	}
289 289
 	{{end}}
... ...
@@ -292,13 +359,13 @@ table {{$family}} {{$tableName}} {
292 292
 	} ; {{end}}{{end}}
293 293
 }
294 294
 {{if .MustFlush}}flush table {{$family}} {{$tableName}}{{end}}
295
-{{range .VMaps}}{{if .MustFlush}}flush map {{$family}} {{$tableName}} {{.Name}}
295
+{{range .Maps}}{{if .MustFlush}}flush map {{$family}} {{$tableName}} {{.Name}}
296 296
 {{end}}{{end}}
297 297
 {{range .Sets}}{{if .MustFlush}}flush set {{$family}} {{$tableName}} {{.Name}}
298 298
 {{end}}{{end}}
299 299
 {{range .Chains}}{{if .MustFlush}}flush chain {{$family}} {{$tableName}} {{.Name}}
300 300
 {{end}}{{end}}
301
-{{range .VMaps}}{{if .DeletedElements}}delete element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .DeletedElements}}{{$k}}, {{end}} }
301
+{{range .Maps}}{{if .DeletedElements}}delete element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .DeletedElements}}{{$k}}, {{end}} }
302 302
 {{end}}{{end}}
303 303
 {{range .Sets}}{{if .DeletedElements}}delete element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .DeletedElements}}{{$k}}, {{end}} }
304 304
 {{end}}{{end}}
... ...
@@ -312,7 +379,7 @@ table {{$family}} {{$tableName}} {
312 312
 	}
313 313
 	{{end}}{{end}}
314 314
 }
315
-{{range .VMaps}}{{if .AddedElements}}add element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .AddedElements}}{{$k}} : {{$v}}, {{end}} }
315
+{{range .Maps}}{{if .AddedElements}}add element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .AddedElements}}{{$k}} : {{$v}}, {{end}} }
316 316
 {{end}}{{end}}
317 317
 {{range .Sets}}{{if .AddedElements}}add element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .AddedElements}}{{$k}}, {{end}} }
318 318
 {{end}}{{end}}
... ...
@@ -327,8 +394,8 @@ const reloadTemplText = `{{$family := .Family}}{{$tableName := .Name}}
327 327
 table {{$family}} {{$tableName}} {}
328 328
 delete table {{$family}} {{$tableName}}
329 329
 table {{$family}} {{$tableName}} {
330
-	{{range .VMaps}}map {{.Name}} {
331
-		type {{.ElementType}} : verdict
330
+	{{range .Maps}}map {{.Name}} {
331
+		{{.ElementTypeExpr}}
332 332
 		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
333 333
         {{if .Elements}}elements = {
334 334
 			{{range $k,$v := .Elements}}{{$k}} : {{$v}},
... ...
@@ -337,7 +404,7 @@ table {{$family}} {{$tableName}} {
337 337
 	}
338 338
 	{{end}}
339 339
 	{{range .Sets}}set {{.Name}} {
340
-		type {{.ElementType}}
340
+		{{.ElementTypeExpr}}
341 341
 		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
342 342
         {{if .Elements}}elements = {
343 343
 			{{range $k,$v := .Elements}}{{$k}},
... ...
@@ -736,15 +803,15 @@ func (ru Rule) delete(ctx context.Context, t *table) (bool, error) {
736 736
 }
737 737
 
738 738
 // ////////////////////////////
739
-// VMaps
739
+// Maps
740 740
 
741
-// vMap is the internal representation of an nftables verdict map.
741
+// nftMap is the internal representation of an nftables map (including verdict maps).
742 742
 // Its elements need to be exported for use by text/template, but they should only be
743 743
 // manipulated via exported methods.
744
-type vMap struct {
744
+type nftMap struct {
745 745
 	table           *table
746 746
 	Name            string
747
-	ElementType     NftType
747
+	ElementTypeExpr string
748 748
 	Flags           []string
749 749
 	Elements        map[string]string
750 750
 	AddedElements   map[string]string
... ...
@@ -752,120 +819,120 @@ type vMap struct {
752 752
 	MustFlush       bool
753 753
 }
754 754
 
755
-// VMap implements the [Obj] interface, it can be passed to a
756
-// [Modifier] to create or delete a verdict map.
757
-type VMap struct {
755
+// Map implements the [Obj] interface, it can be passed to a
756
+// [Modifier] to create or delete a map.
757
+type Map struct {
758 758
 	Name        string
759
-	ElementType NftType
759
+	ElementType MapTyper
760 760
 	Flags       []string
761 761
 }
762 762
 
763
-func (vm VMap) create(ctx context.Context, t *table) (bool, error) {
764
-	if vm.Name == "" {
765
-		return false, errors.New("vmap must have a name")
763
+func (m Map) create(ctx context.Context, t *table) (bool, error) {
764
+	if m.Name == "" {
765
+		return false, errors.New("map must have a name")
766 766
 	}
767
-	if _, ok := t.VMaps[vm.Name]; ok {
768
-		return false, fmt.Errorf("vmap '%s' already exists", vm.Name)
767
+	if _, ok := t.Maps[m.Name]; ok {
768
+		return false, fmt.Errorf("map '%s' already exists", m.Name)
769 769
 	}
770
-	if vm.ElementType == "" {
771
-		return false, fmt.Errorf("vmap '%s' has no element type", vm.Name)
770
+	if m.ElementType == nil {
771
+		return false, fmt.Errorf("map '%s' has no element type", m.Name)
772 772
 	}
773
-	v := &vMap{
773
+	nm := &nftMap{
774 774
 		table:           t,
775
-		Name:            vm.Name,
776
-		ElementType:     vm.ElementType,
777
-		Flags:           slices.Clone(vm.Flags),
775
+		Name:            m.Name,
776
+		ElementTypeExpr: m.ElementType.mapType(),
777
+		Flags:           slices.Clone(m.Flags),
778 778
 		Elements:        map[string]string{},
779 779
 		AddedElements:   map[string]string{},
780 780
 		DeletedElements: map[string]string{},
781 781
 		MustFlush:       true,
782 782
 	}
783
-	t.VMaps[v.Name] = v
783
+	t.Maps[nm.Name] = nm
784 784
 	log.G(ctx).WithFields(log.Fields{
785 785
 		"family": t.Family,
786 786
 		"table":  t.Name,
787
-		"vmap":   v.Name,
788
-	}).Debug("nftables: created interface vmap")
787
+		"map":    nm.Name,
788
+	}).Debug("nftables: created map")
789 789
 	return true, nil
790 790
 }
791 791
 
792
-func (vm VMap) delete(ctx context.Context, t *table) (bool, error) {
793
-	v := t.VMaps[vm.Name]
794
-	if v == nil {
795
-		return false, fmt.Errorf("cannot delete vmap '%s', it does not exist", vm.Name)
792
+func (m Map) delete(ctx context.Context, t *table) (bool, error) {
793
+	nm := t.Maps[m.Name]
794
+	if nm == nil {
795
+		return false, fmt.Errorf("cannot delete map '%s', it does not exist", m.Name)
796 796
 	}
797
-	if len(v.Elements) != 0 {
798
-		return false, fmt.Errorf("cannot delete vmap '%s', it contains %d elements", v.Name, len(v.Elements))
797
+	if len(nm.Elements) != 0 {
798
+		return false, fmt.Errorf("cannot delete map '%s', it contains %d elements", nm.Name, len(nm.Elements))
799 799
 	}
800
-	delete(t.VMaps, v.Name)
800
+	delete(t.Maps, nm.Name)
801 801
 	t.DeleteCommands = append(t.DeleteCommands,
802
-		fmt.Sprintf("delete map %s %s %s", t.Family, t.Name, v.Name))
802
+		fmt.Sprintf("delete map %s %s %s", t.Family, t.Name, nm.Name))
803 803
 	log.G(ctx).WithFields(log.Fields{
804 804
 		"family": t.Family,
805 805
 		"table":  t.Name,
806
-		"vmap":   v.Name,
807
-	}).Debug("nftables: deleted vmap")
806
+		"map":    nm.Name,
807
+	}).Debug("nftables: deleted map")
808 808
 	return true, nil
809 809
 }
810 810
 
811
-// VMapElement implements the [Obj] interface, it can be passed to a
812
-// [Modifier] to create or delete an entry in a verdict map.
813
-type VMapElement struct {
814
-	VmapName string
815
-	Key      string
816
-	Verdict  string
811
+// MapElement implements the [Obj] interface, it can be passed to a
812
+// [Modifier] to create or delete an entry in a map.
813
+type MapElement struct {
814
+	MapName string
815
+	Key     string
816
+	Value   string
817 817
 }
818 818
 
819
-func (ve VMapElement) create(ctx context.Context, t *table) (bool, error) {
820
-	if ve.VmapName == "" {
821
-		return false, errors.New("cannot add element to unnamed vmap")
819
+func (me MapElement) create(ctx context.Context, t *table) (bool, error) {
820
+	if me.MapName == "" {
821
+		return false, errors.New("cannot add element to unnamed map")
822 822
 	}
823
-	v := t.VMaps[ve.VmapName]
824
-	if v == nil {
825
-		return false, fmt.Errorf("cannot add to vmap '%s', it does not exist", ve.VmapName)
823
+	nm := t.Maps[me.MapName]
824
+	if nm == nil {
825
+		return false, fmt.Errorf("cannot add to map '%s', it does not exist", me.MapName)
826 826
 	}
827
-	if ve.Key == "" || ve.Verdict == "" {
828
-		return false, fmt.Errorf("cannot add to vmap '%s', element must have key and verdict", ve.VmapName)
827
+	if me.Key == "" || me.Value == "" {
828
+		return false, fmt.Errorf("cannot add to map '%s', element must have key and value", me.MapName)
829 829
 	}
830
-	if _, ok := v.Elements[ve.Key]; ok {
831
-		return false, fmt.Errorf("verdict map '%s' already contains element '%s'", ve.VmapName, ve.Key)
830
+	if _, ok := nm.Elements[me.Key]; ok {
831
+		return false, fmt.Errorf("map '%s' already contains element '%s'", me.MapName, me.Key)
832 832
 	}
833
-	v.Elements[ve.Key] = ve.Verdict
834
-	v.AddedElements[ve.Key] = ve.Verdict
835
-	delete(v.DeletedElements, ve.Key)
833
+	nm.Elements[me.Key] = me.Value
834
+	nm.AddedElements[me.Key] = me.Value
835
+	delete(nm.DeletedElements, me.Key)
836 836
 	log.G(ctx).WithFields(log.Fields{
837
-		"family":  t.Family,
838
-		"table":   t.Name,
839
-		"vmap":    ve.VmapName,
840
-		"key":     ve.Key,
841
-		"verdict": ve.Verdict,
842
-	}).Debug("nftables: added vmap element")
837
+		"family": t.Family,
838
+		"table":  t.Name,
839
+		"map":    me.MapName,
840
+		"key":    me.Key,
841
+		"value":  me.Value,
842
+	}).Debug("nftables: added map element")
843 843
 	return true, nil
844 844
 }
845 845
 
846
-func (ve VMapElement) delete(ctx context.Context, t *table) (bool, error) {
847
-	v := t.VMaps[ve.VmapName]
848
-	if v == nil {
849
-		return false, fmt.Errorf("cannot delete from vmap '%s', it does not exist", ve.VmapName)
846
+func (me MapElement) delete(ctx context.Context, t *table) (bool, error) {
847
+	nm := t.Maps[me.MapName]
848
+	if nm == nil {
849
+		return false, fmt.Errorf("cannot delete from map '%s', it does not exist", me.MapName)
850 850
 	}
851
-	oldVerdict, ok := v.Elements[ve.Key]
851
+	oldValue, ok := nm.Elements[me.Key]
852 852
 	if !ok {
853
-		return false, fmt.Errorf("verdict map '%s' does not contain element '%s'", ve.VmapName, ve.Key)
853
+		return false, fmt.Errorf("map '%s' does not contain element '%s'", me.MapName, me.Key)
854 854
 	}
855
-	if oldVerdict != ve.Verdict {
856
-		return false, fmt.Errorf("cannot delete verdict map '%s' element '%s', verdict was '%s', not '%s'",
857
-			ve.VmapName, ve.Key, oldVerdict, ve.Verdict)
855
+	if oldValue != me.Value {
856
+		return false, fmt.Errorf("cannot delete map '%s' element '%s', value was '%s', not '%s'",
857
+			me.MapName, me.Key, oldValue, me.Value)
858 858
 	}
859
-	delete(v.Elements, ve.Key)
860
-	delete(v.AddedElements, ve.Key)
861
-	v.DeletedElements[ve.Key] = ve.Verdict
859
+	delete(nm.Elements, me.Key)
860
+	delete(nm.AddedElements, me.Key)
861
+	nm.DeletedElements[me.Key] = me.Value
862 862
 	log.G(ctx).WithFields(log.Fields{
863
-		"family":  t.Family,
864
-		"table":   t.Name,
865
-		"vmap":    ve.VmapName,
866
-		"key":     ve.Key,
867
-		"verdict": ve.Verdict,
868
-	}).Debug("nftables: deleted vmap element")
863
+		"family": t.Family,
864
+		"table":  t.Name,
865
+		"map":    me.MapName,
866
+		"key":    me.Key,
867
+		"value":  me.Value,
868
+	}).Debug("nftables: deleted map element")
869 869
 	return true, nil
870 870
 }
871 871
 
... ...
@@ -878,7 +945,7 @@ func (ve VMapElement) delete(ctx context.Context, t *table) (bool, error) {
878 878
 type set struct {
879 879
 	table           *table
880 880
 	Name            string
881
-	ElementType     NftType
881
+	ElementTypeExpr string
882 882
 	Flags           []string
883 883
 	Elements        map[string]struct{}
884 884
 	AddedElements   map[string]struct{}
... ...
@@ -890,7 +957,7 @@ type set struct {
890 890
 // [Modifier] to create or delete a set.
891 891
 type Set struct {
892 892
 	Name        string
893
-	ElementType NftType
893
+	ElementType SetTyper
894 894
 	Flags       []string
895 895
 }
896 896
 
... ...
@@ -902,14 +969,14 @@ func (sd Set) create(ctx context.Context, t *table) (bool, error) {
902 902
 	if _, ok := t.Sets[sd.Name]; ok {
903 903
 		return false, fmt.Errorf("set '%s' already exists", sd.Name)
904 904
 	}
905
-	if sd.ElementType == "" {
905
+	if sd.ElementType == nil {
906 906
 		return false, fmt.Errorf("set '%s' must have a type", sd.Name)
907 907
 	}
908 908
 	s := &set{
909 909
 		table:           t,
910 910
 		Name:            sd.Name,
911 911
 		Elements:        map[string]struct{}{},
912
-		ElementType:     sd.ElementType,
912
+		ElementTypeExpr: sd.ElementType.setType(),
913 913
 		Flags:           slices.Clone(sd.Flags),
914 914
 		AddedElements:   map[string]struct{}{},
915 915
 		DeletedElements: map[string]struct{}{},
... ...
@@ -1041,7 +1108,7 @@ func (t *table) updatesApplied() {
1041 1041
 	for _, c := range t.Chains {
1042 1042
 		c.MustFlush = false
1043 1043
 	}
1044
-	for _, m := range t.VMaps {
1044
+	for _, m := range t.Maps {
1045 1045
 		m.AddedElements = map[string]string{}
1046 1046
 		m.DeletedElements = map[string]string{}
1047 1047
 		m.MustFlush = false
... ...
@@ -189,9 +189,9 @@ func TestVMap(t *testing.T) {
189 189
 
190 190
 	// Create a verdict map.
191 191
 	const mapName = "this_is_a_vmap"
192
-	tm.Create(VMap{Name: mapName, ElementType: NftTypeIfname})
193
-	tm.Create(VMapElement{VmapName: mapName, Key: "eth0", Verdict: "return"})
194
-	tm.Create(VMapElement{VmapName: mapName, Key: "eth1", Verdict: "drop"})
192
+	tm.Create(Map{Name: mapName, ElementType: Ifname.VMap()})
193
+	tm.Create(MapElement{MapName: mapName, Key: "eth0", Value: "return"})
194
+	tm.Create(MapElement{MapName: mapName, Key: "eth1", Value: "drop"})
195 195
 
196 196
 	// Update nftables and check what happened.
197 197
 	applyAndCheck(t, tbl, tm, t.Name()+"/created.golden")
... ...
@@ -217,10 +217,10 @@ func TestSet(t *testing.T) {
217 217
 	// Create a set in each table.
218 218
 	const set4Name = "set4"
219 219
 	tm4 := Modifier{}
220
-	tm4.Create(Set{Name: set4Name, ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}})
220
+	tm4.Create(Set{Name: set4Name, ElementType: IPv4Addr, Flags: []string{"interval"}})
221 221
 	const set6Name = "set6"
222 222
 	tm6 := Modifier{}
223
-	tm6.Create(Set{Name: set6Name, ElementType: NftTypeIPv6Addr, Flags: []string{"interval"}})
223
+	tm6.Create(Set{Name: set6Name, ElementType: IPv6Addr, Flags: []string{"interval"}})
224 224
 
225 225
 	// Add elements to each set.
226 226
 	tm4.Create(SetElement{SetName: set4Name, Element: "192.0.2.0/24"})
... ...
@@ -256,12 +256,12 @@ func TestReload(t *testing.T) {
256 256
 	tm.Create(Rule{Chain: bcName, Group: 0, Rule: []string{"counter"}})
257 257
 
258 258
 	const vmapName = "this_is_a_vmap"
259
-	tm.Create(VMap{Name: vmapName, ElementType: NftTypeIfname})
260
-	tm.Create(VMapElement{VmapName: vmapName, Key: "eth0", Verdict: "return"})
261
-	tm.Create(VMapElement{VmapName: vmapName, Key: "eth1", Verdict: "return"})
259
+	tm.Create(Map{Name: vmapName, ElementType: Ifname.VMap()})
260
+	tm.Create(MapElement{MapName: vmapName, Key: "eth0", Value: "return"})
261
+	tm.Create(MapElement{MapName: vmapName, Key: "eth1", Value: "return"})
262 262
 
263 263
 	const setName = "this_is_a_set"
264
-	tm.Create(Set{Name: setName, ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}})
264
+	tm.Create(Set{Name: setName, ElementType: IPv4Addr, Flags: []string{"interval"}})
265 265
 	tm.Create(SetElement{SetName: setName, Element: "192.0.2.0/24"})
266 266
 
267 267
 	applyAndCheck(t, tbl, tm, t.Name()+"/created.golden")
... ...
@@ -463,110 +463,110 @@ func TestValidation(t *testing.T) {
463 463
 			},
464 464
 			expErr: "chain 'achain', cannot add empty rule",
465 465
 		},
466
-		// VMap
466
+		// Map (verdict)
467 467
 		{
468
-			name: "duplicate vmap",
468
+			name: "duplicate map",
469 469
 			cmds: []command{
470
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
471
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
470
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
471
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
472 472
 			},
473
-			expErr: "vmap 'avmap' already exists",
473
+			expErr: "map 'avmap' already exists",
474 474
 		},
475 475
 		{
476
-			name: "delete nonexistent vmap",
476
+			name: "delete nonexistent map",
477 477
 			cmds: []command{
478
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}, delete: true},
478
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}, delete: true},
479 479
 			},
480
-			expErr: "cannot delete vmap 'avmap', it does not exist",
480
+			expErr: "cannot delete map 'avmap', it does not exist",
481 481
 		},
482 482
 		{
483
-			name:   "missing vmap name",
484
-			cmds:   []command{{obj: VMap{ElementType: NftTypeIfname}}},
485
-			expErr: "vmap must have a name",
483
+			name:   "missing map name",
484
+			cmds:   []command{{obj: Map{ElementType: Ifname.VMap()}}},
485
+			expErr: "map must have a name",
486 486
 		},
487 487
 		{
488
-			name:   "missing vmap element type",
489
-			cmds:   []command{{obj: VMap{Name: "avmap"}}},
490
-			expErr: "vmap 'avmap' has no element type",
488
+			name:   "missing map element type",
489
+			cmds:   []command{{obj: Map{Name: "avmap"}}},
490
+			expErr: "map 'avmap' has no element type",
491 491
 		},
492 492
 		{
493
-			name: "delete non-empty vmap",
493
+			name: "delete non-empty map",
494 494
 			cmds: []command{
495
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
496
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0", Verdict: "drop"}},
497
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}, delete: true},
495
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
496
+				{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
497
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}, delete: true},
498 498
 			},
499
-			expErr: "cannot delete vmap 'avmap', it contains 1 elements",
499
+			expErr: "cannot delete map 'avmap', it contains 1 elements",
500 500
 		},
501
-		// VMapElement
501
+		// MapElement
502 502
 		{
503
-			name: "duplicate vmap element",
503
+			name: "duplicate map element",
504 504
 			cmds: []command{
505
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
506
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0", Verdict: "drop"}},
507
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0", Verdict: "drop"}},
505
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
506
+				{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
507
+				{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
508 508
 			},
509
-			expErr: "verdict map 'avmap' already contains element 'eth0'",
509
+			expErr: "map 'avmap' already contains element 'eth0'",
510 510
 		},
511 511
 		{
512
-			name: "add to vmap that does not exist",
512
+			name: "add to map that does not exist",
513 513
 			cmds: []command{
514
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0", Verdict: "drop"}},
514
+				{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}},
515 515
 			},
516
-			expErr: "cannot add to vmap 'avmap', it does not exist",
516
+			expErr: "cannot add to map 'avmap', it does not exist",
517 517
 		},
518 518
 		{
519
-			name: "delete nonexistent vmap element",
519
+			name: "delete nonexistent map element",
520 520
 			cmds: []command{
521
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
522
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0", Verdict: "drop"}, delete: true},
521
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
522
+				{obj: MapElement{MapName: "avmap", Key: "eth0", Value: "drop"}, delete: true},
523 523
 			},
524
-			expErr: "verdict map 'avmap' does not contain element 'eth0'",
524
+			expErr: "map 'avmap' does not contain element 'eth0'",
525 525
 		},
526 526
 		{
527
-			name: "vmap element with no named vmap",
527
+			name: "map element with no named map",
528 528
 			cmds: []command{
529
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
530
-				{obj: VMapElement{Key: "eth0", Verdict: "drop"}},
529
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
530
+				{obj: MapElement{Key: "eth0", Value: "drop"}},
531 531
 			},
532
-			expErr: "cannot add element to unnamed vmap",
532
+			expErr: "cannot add element to unnamed map",
533 533
 		},
534 534
 		{
535
-			name: "vmap element with no key",
535
+			name: "map element with no key",
536 536
 			cmds: []command{
537
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
538
-				{obj: VMapElement{VmapName: "avmap", Verdict: "drop"}},
537
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
538
+				{obj: MapElement{MapName: "avmap", Value: "drop"}},
539 539
 			},
540
-			expErr: "cannot add to vmap 'avmap', element must have key and verdict",
540
+			expErr: "cannot add to map 'avmap', element must have key and value",
541 541
 		},
542 542
 		{
543
-			name: "vmap element with no verdict",
543
+			name: "map element with no value",
544 544
 			cmds: []command{
545
-				{obj: VMap{Name: "avmap", ElementType: NftTypeIfname}},
546
-				{obj: VMapElement{VmapName: "avmap", Key: "eth0"}},
545
+				{obj: Map{Name: "avmap", ElementType: Ifname.VMap()}},
546
+				{obj: MapElement{MapName: "avmap", Key: "eth0"}},
547 547
 			},
548
-			expErr: "cannot add to vmap 'avmap', element must have key and verdict",
548
+			expErr: "cannot add to map 'avmap', element must have key and value",
549 549
 		},
550 550
 		// Set
551 551
 		{
552 552
 			name: "duplicate set",
553 553
 			cmds: []command{
554
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
555
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
554
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
555
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
556 556
 			},
557 557
 			expErr: "set 'aset' already exists",
558 558
 		},
559 559
 		{
560 560
 			name: "delete nonexistent set",
561 561
 			cmds: []command{
562
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}, delete: true},
562
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}, delete: true},
563 563
 			},
564 564
 			expErr: "cannot delete set 'aset', it does not exist",
565 565
 		},
566 566
 		{
567 567
 			name: "missing set name",
568 568
 			cmds: []command{
569
-				{obj: Set{ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
569
+				{obj: Set{ElementType: IPv4Addr, Flags: []string{"interval"}}},
570 570
 			},
571 571
 			expErr: "set must have a name",
572 572
 		},
... ...
@@ -580,9 +580,9 @@ func TestValidation(t *testing.T) {
580 580
 		{
581 581
 			name: "delete non-empty set",
582 582
 			cmds: []command{
583
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
583
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
584 584
 				{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
585
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}, delete: true},
585
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}, delete: true},
586 586
 			},
587 587
 			expErr: "cannot delete set 'aset', it contains 1 elements",
588 588
 		},
... ...
@@ -590,7 +590,7 @@ func TestValidation(t *testing.T) {
590 590
 		{
591 591
 			name: "duplicate set element",
592 592
 			cmds: []command{
593
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
593
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
594 594
 				{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
595 595
 				{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}},
596 596
 			},
... ...
@@ -599,7 +599,7 @@ func TestValidation(t *testing.T) {
599 599
 		{
600 600
 			name: "delete nonexistent set element",
601 601
 			cmds: []command{
602
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
602
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
603 603
 				{obj: SetElement{SetName: "aset", Element: "192.0.2.0/24"}, delete: true},
604 604
 			},
605 605
 			expErr: "cannot delete '192.0.2.0/24' from set 'aset', it does not exist",
... ...
@@ -607,7 +607,7 @@ func TestValidation(t *testing.T) {
607 607
 		{
608 608
 			name: "add set element to unnamed set",
609 609
 			cmds: []command{
610
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
610
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
611 611
 				{obj: SetElement{Element: "192.0.2.0/24"}},
612 612
 			},
613 613
 			expErr: "cannot add to set '', it does not exist",
... ...
@@ -615,7 +615,7 @@ func TestValidation(t *testing.T) {
615 615
 		{
616 616
 			name: "add set element with no element",
617 617
 			cmds: []command{
618
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
618
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
619 619
 				{obj: SetElement{SetName: "aset"}},
620 620
 			},
621 621
 			expErr: "cannot add to set 'aset', element not specified",
... ...
@@ -623,7 +623,7 @@ func TestValidation(t *testing.T) {
623 623
 		{
624 624
 			name: "mismatched set element type",
625 625
 			cmds: []command{
626
-				{obj: Set{Name: "aset", ElementType: NftTypeIPv4Addr, Flags: []string{"interval"}}},
626
+				{obj: Set{Name: "aset", ElementType: IPv4Addr, Flags: []string{"interval"}}},
627 627
 				{obj: SetElement{SetName: "aset", Element: "2001:db8::/64"}},
628 628
 			},
629 629
 			expErr: "Address family for hostname not supported",