Skip to content

Commit dddc943

Browse files
authored
Merge pull request moby#49977 from robmry/nftables_util_updates
nftables: util updates, including table reload
2 parents b3160e8 + 350bb51 commit dddc943

File tree

6 files changed

+212
-19
lines changed

6 files changed

+212
-19
lines changed

libnetwork/internal/nftables/nftables_linux.go

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5761
var (
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.
257299
func (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 ...
654744
func (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.
698792
func 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
}

libnetwork/internal/nftables/nftables_linux_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func testSetup(t *testing.T) func() {
3636
func disable() {
3737
incrementalUpdateTempl = nil
3838
nftPath = ""
39+
reloadTempl = nil
3940
enableOnce = sync.Once{}
4041
}
4142

@@ -44,7 +45,7 @@ func applyAndCheck(t *testing.T, tbl TableRef, goldenFilename string) {
4445
err := tbl.Apply(context.Background())
4546
assert.Check(t, err)
4647
res := icmd.RunCommand("nft", "list", "ruleset")
47-
assert.Check(t, is.Equal(res.ExitCode, 0))
48+
res.Assert(t, icmd.Success)
4849
golden.Assert(t, res.Combined(), goldenFilename)
4950
}
5051

@@ -250,3 +251,49 @@ func TestSet(t *testing.T) {
250251
applyAndCheck(t, tbl4, t.Name()+"_deleted4.golden")
251252
applyAndCheck(t, tbl6, t.Name()+"_deleted46.golden")
252253
}
254+
255+
func TestReload(t *testing.T) {
256+
defer testSetup(t)()
257+
258+
// Create a table with some stuff in it.
259+
const tableName = "this_is_a_table"
260+
tbl, err := NewTable(IPv4, tableName)
261+
assert.NilError(t, err)
262+
bc, err := tbl.BaseChain("a_base_chain", BaseChainTypeFilter, BaseChainHookForward, BaseChainPriorityFilter)
263+
assert.NilError(t, err)
264+
err = bc.AppendRule(0, "counter")
265+
assert.NilError(t, err)
266+
m := tbl.InterfaceVMap("this_is_a_vmap")
267+
err = m.AddElement("eth0", "return")
268+
assert.Check(t, err)
269+
err = m.AddElement("eth1", "return")
270+
assert.Check(t, err)
271+
err = tbl.PrefixSet("set4").AddElement("192.0.2.0/24")
272+
assert.Check(t, err)
273+
applyAndCheck(t, tbl, t.Name()+"_created.golden")
274+
275+
// Delete the underlying nftables table.
276+
deleteTable := func() {
277+
t.Helper()
278+
res := icmd.RunCommand("nft", "delete", "table", string(IPv4), tableName)
279+
res.Assert(t, icmd.Success)
280+
res = icmd.RunCommand("nft", "list", "ruleset")
281+
res.Assert(t, icmd.Success)
282+
assert.Check(t, is.Equal(res.Combined(), ""))
283+
}
284+
deleteTable()
285+
286+
// Reconstruct the nftables table.
287+
err = tbl.Reload(context.Background())
288+
assert.Check(t, err)
289+
applyAndCheck(t, tbl, t.Name()+"_reloaded.golden")
290+
291+
// Delete again.
292+
deleteTable()
293+
294+
// Check implicit/recovery reload - only deleting something that's gone missing
295+
// from a vmap/set will trigger this.
296+
err = m.DeleteElement("eth1")
297+
assert.Check(t, err)
298+
applyAndCheck(t, tbl, t.Name()+"_recovered.golden")
299+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
table ip this_is_a_table {
2+
map this_is_a_vmap {
3+
type ifname : verdict
4+
elements = { "eth0" : return,
5+
"eth1" : return }
6+
}
7+
8+
set set4 {
9+
type ipv4_addr
10+
flags interval
11+
elements = { 192.0.2.0/24 }
12+
}
13+
14+
chain a_base_chain {
15+
type filter hook forward priority filter; policy accept;
16+
counter packets 0 bytes 0
17+
}
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
table ip this_is_a_table {
2+
map this_is_a_vmap {
3+
type ifname : verdict
4+
elements = { "eth0" : return }
5+
}
6+
7+
set set4 {
8+
type ipv4_addr
9+
flags interval
10+
elements = { 192.0.2.0/24 }
11+
}
12+
13+
chain a_base_chain {
14+
type filter hook forward priority filter; policy accept;
15+
counter packets 0 bytes 0
16+
}
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
table ip this_is_a_table {
2+
map this_is_a_vmap {
3+
type ifname : verdict
4+
elements = { "eth0" : return,
5+
"eth1" : return }
6+
}
7+
8+
set set4 {
9+
type ipv4_addr
10+
flags interval
11+
elements = { 192.0.2.0/24 }
12+
}
13+
14+
chain a_base_chain {
15+
type filter hook forward priority filter; policy accept;
16+
counter packets 0 bytes 0
17+
}
18+
}

libnetwork/internal/nftables/testdata/TestTable_created.golden

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)