@@ -52,14 +52,20 @@ import (
5252 "text/template"
5353
5454 "github.com/containerd/log"
55+ "go.opentelemetry.io/otel"
5556)
5657
58+ // Prefix for OTEL span names.
59+ const spanPrefix = "libnetwork.internal.nftables"
60+
5761var (
5862 // nftPath is the path of the "nft" tool, set by [Enable] and left empty if the tool
5963 // is not present - in which case, nftables is disabled.
6064 nftPath string
6165 // incrementalUpdateTempl is a parsed text/template, used to apply incremental updates.
6266 incrementalUpdateTempl * template.Template
67+ // reloadTempl is a parsed text/template, used to apply a whole table.
68+ reloadTempl * template.Template
6369 // enableOnce is used by [Enable] to avoid checking the path for "nft" more than once.
6470 enableOnce sync.Once
6571)
@@ -252,15 +258,55 @@ table {{$family}} {{$tableName}} {
252258{{end}}{{end}}
253259`
254260
261+ // reloadTemplText is used with text/template to generate an nftables command file
262+ // (which will be applied atomically), to fully re-create a table.
263+ //
264+ // It first declares the table so if it doesn't already exist, it can be deleted.
265+ // Then it deletes the table and re-creates it.
266+ const reloadTemplText = `{{$family := .Family}}{{$tableName := .Name}}
267+ table {{$family}} {{$tableName}} {}
268+ delete table {{$family}} {{$tableName}}
269+ table {{$family}} {{$tableName}} {
270+ {{range .VMaps}}map {{.Name}} {
271+ type {{.ElementType}} : verdict
272+ {{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
273+ {{if .Elements}}elements = {
274+ {{range $k,$v := .Elements}}{{$k}} : {{$v}},
275+ {{end -}}
276+ }{{end}}
277+ }
278+ {{end}}
279+ {{range .Sets}}set {{.Name}} {
280+ type {{.ElementType}}
281+ {{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
282+ {{if .Elements}}elements = {
283+ {{range $k,$v := .Elements}}{{$k}},
284+ {{end -}}
285+ }{{end}}
286+ }
287+ {{end}}
288+ {{range .Chains}}chain {{.Name}} {
289+ {{if .ChainType}}type {{.ChainType}} hook {{.Hook}} priority {{.Priority}}; policy {{.Policy}}{{end}}
290+ {{range .Rules}}{{.}}
291+ {{end}}
292+ }
293+ {{end}}
294+ }
295+ `
296+
255297// Apply makes incremental updates to nftables, corresponding to changes to the [TableRef]
256298// since Apply was last called.
257299func (t TableRef ) Apply (ctx context.Context ) error {
258- var buf bytes.Buffer
300+ if ! Enabled () {
301+ return errors .New ("nftables is not enabled" )
302+ }
259303
260304 // Update nftables.
305+ var buf bytes.Buffer
261306 if err := incrementalUpdateTempl .Execute (& buf , t .t ); err != nil {
262307 return fmt .Errorf ("failed to execute template nft ruleset: %w" , err )
263308 }
309+
264310 if err := nftApply (ctx , buf .Bytes ()); err != nil {
265311 // On error, log a line-numbered version of the generated "nft" input (because
266312 // nft error messages refer to line numbers).
@@ -271,25 +317,51 @@ func (t TableRef) Apply(ctx context.Context) error {
271317 sb .Write (line )
272318 }
273319 log .G (ctx ).Error ("nftables: failed to update nftables:\n " , sb .String (), "\n " , err )
274- return err
320+
321+ // It's possible something destructive has happened to nftables. For example, in
322+ // integration-cli tests, tests start daemons in the same netns as the integration
323+ // test's own daemon. They don't always use their own daemon, but they tend to leave
324+ // behind networks for the test infrastructure to clean up between tests. Starting
325+ // a daemon flushes the "docker-bridges" table, so the cleanup fails to delete a
326+ // rule that's been flushed. So, try reloading the whole table to get back in-sync.
327+ return t .Reload (ctx )
275328 }
276329
277330 // Note that updates have been applied.
278- t .t .DeleteChainCommands = t .t .DeleteChainCommands [:0 ]
279- for _ , c := range t .t .Chains {
280- c .Dirty = false
331+ t .t .updatesApplied ()
332+ return nil
333+ }
334+
335+ // Reload deletes the table, then re-creates it, atomically.
336+ func (t TableRef ) Reload (ctx context.Context ) error {
337+ if ! Enabled () {
338+ return errors .New ("nftables is not enabled" )
281339 }
282- for _ , m := range t .t .VMaps {
283- m .Dirty = false
284- m .AddedElements = map [string ]string {}
285- m .DeletedElements = map [string ]struct {}{}
340+
341+ ctx = log .WithLogger (ctx , log .G (ctx ).WithFields (log.Fields {"table" : t .t .Name , "family" : t .t .Family }))
342+ log .G (ctx ).Warn ("nftables: reloading table" )
343+
344+ // Build the update.
345+ var buf bytes.Buffer
346+ if err := reloadTempl .Execute (& buf , t .t ); err != nil {
347+ return fmt .Errorf ("failed to execute reload template: %w" , err )
286348 }
287- for _ , s := range t .t .Sets {
288- s .Dirty = false
289- s .AddedElements = map [string ]struct {}{}
290- s .DeletedElements = map [string ]struct {}{}
349+
350+ if err := nftApply (ctx , buf .Bytes ()); err != nil {
351+ // On error, log a line-numbered version of the generated "nft" input (because
352+ // nft error messages refer to line numbers).
353+ var sb strings.Builder
354+ for i , line := range bytes .SplitAfter (buf .Bytes (), []byte ("\n " )) {
355+ sb .WriteString (strconv .Itoa (i + 1 ))
356+ sb .WriteString (":\t " )
357+ sb .Write (line )
358+ }
359+ log .G (ctx ).Error ("nftables: failed to reload nftable:\n " , sb .String (), "\n " , err )
360+ return err
291361 }
292- t .t .Dirty = false
362+
363+ // Note that updates have been applied.
364+ t .t .updatesApplied ()
293365 return nil
294366}
295367
@@ -650,6 +722,24 @@ func (s SetRef) DeleteElement(element string) error {
650722// ////////////////////////////
651723// Internal
652724
725+ func (t * table ) updatesApplied () {
726+ t .DeleteChainCommands = t .DeleteChainCommands [:0 ]
727+ for _ , c := range t .Chains {
728+ c .Dirty = false
729+ }
730+ for _ , m := range t .VMaps {
731+ m .Dirty = false
732+ m .AddedElements = map [string ]string {}
733+ m .DeletedElements = map [string ]struct {}{}
734+ }
735+ for _ , s := range t .Sets {
736+ s .Dirty = false
737+ s .AddedElements = map [string ]struct {}{}
738+ s .DeletedElements = map [string ]struct {}{}
739+ }
740+ t .Dirty = false
741+ }
742+
653743/* Can't make text/template range over this, not sure why ...
654744func (c *chain) Rules() iter.Seq[string] {
655745 groups := make([]int, 0, len(c.ruleGroups))
@@ -691,11 +781,18 @@ func parseTemplate() error {
691781 if err != nil {
692782 return fmt .Errorf ("parsing 'incrementalUpdateTemplText': %w" , err )
693783 }
784+ reloadTempl , err = template .New ("ruleset" ).Parse (reloadTemplText )
785+ if err != nil {
786+ return fmt .Errorf ("parsing 'reloadTemplText': %w" , err )
787+ }
694788 return nil
695789}
696790
697791// nftApply runs the "nft" command.
698792func nftApply (ctx context.Context , nftCmd []byte ) error {
793+ ctx , span := otel .Tracer ("" ).Start (ctx , spanPrefix + ".nftApply" )
794+ defer span .End ()
795+
699796 if ! Enabled () {
700797 return errors .New ("nftables is not enabled" )
701798 }
0 commit comments