diff --git a/pkg/tcpip/nftables/nftables.go b/pkg/tcpip/nftables/nftables.go index db4a77fc9..07511438f 100644 --- a/pkg/tcpip/nftables/nftables.go +++ b/pkg/tcpip/nftables/nftables.go @@ -575,6 +575,33 @@ type Operation interface { evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) } +// Ensures all operations implement the Operation interface at compile time. +var ( + _ Operation = (*Immediate)(nil) +) + +// Immediate is an operation that sets the data in a register. +type Immediate struct { + data RegisterData // Data to set the destination register to. + dreg uint8 // Number of the destination register. +} + +// NewImmediate creates a new Immediate operation. +func NewImmediate(dreg uint8, data RegisterData) (*Immediate, error) { + if err := data.ValidateRegister(dreg); err != nil { + return nil, err + } + return &Immediate{dreg: dreg, data: data}, nil +} + +// TypeString for Immediate returns "Immediate" as the string operation type. +func (op *Immediate) TypeString() string { return "Immediate" } + +// evaluate for Immediate sets the data in the destination register. +func (op Immediate) evaluate(regs *RegisterSet, pkt *stack.PacketBuffer) { + op.data.StoreData(regs, op.dreg) +} + // // Register and Register-Related Implementations. // Note: Registers are represented by type uint8 for the register number. @@ -1072,15 +1099,15 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, hook Hook, pkt *stack.Pac } // Terminates immediately on netfilter terminal verdicts. - switch regs.verdict.Code { + switch regs.Verdict().Code { case VC(linux.NF_ACCEPT), VC(linux.NF_DROP), VC(linux.NF_STOLEN), VC(linux.NF_QUEUE): - return regs.verdict, nil + return regs.Verdict(), nil } } // Returns policy verdict of the last base chain evaluated if no terminal // verdict was issued. - switch regs.verdict.Code { + switch regs.Verdict().Code { case VC(linux.NFT_CONTINUE), VC(linux.NFT_RETURN): if bc.GetBaseChainInfo().PolicyDrop { return Verdict{Code: VC(linux.NF_DROP)}, nil @@ -1088,7 +1115,7 @@ func (nf *NFTables) EvaluateHook(family AddressFamily, hook Hook, pkt *stack.Pac return Verdict{Code: VC(linux.NF_ACCEPT)}, nil } - panic(fmt.Sprintf("unexpected verdict from hook evaluation: %d", regs.verdict.Code)) + panic(fmt.Sprintf("unexpected verdict from hook evaluation: %d", regs.Verdict().Code)) } // diff --git a/pkg/tcpip/nftables/nftables_test.go b/pkg/tcpip/nftables/nftables_test.go index 30d612187..8c139cffc 100644 --- a/pkg/tcpip/nftables/nftables_test.go +++ b/pkg/tcpip/nftables/nftables_test.go @@ -23,6 +23,8 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/stack" ) +const arbitraryTargetChain = "target_chain" + // makeTestingPacket creates an arbitrary packet for testing. func makeTestingPacket() *stack.PacketBuffer { return stack.NewPacketBuffer(stack.PacketBufferOptions{ @@ -84,6 +86,190 @@ func TestAcceptAllForSupportedHooks(t *testing.T) { } } +// TestEvaluateImmediate tests that the Immediate operation correctly sets the +// register value and behaves as expected during evaluation. +func TestEvaluateImmediate(t *testing.T) { + for _, test := range []struct { + tname string + baseOp1 Operation // will be nil if unused + baseOp2 Operation // will be nil if unused + targetOp Operation // will be nil if unused + verdict Verdict + }{ + { + tname: "no operations", + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "immediately accept", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "immediately drop", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "immediately continue with base chain policy accept", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "immediately return with base chain policy accept", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_RETURN)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "immediately jump to target chain that accepts", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "immediately jump to target chain that drops", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "immediately jump to target chain that continues with second rule that accepts", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "immediately jump to target chain that continues with second rule that drops", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_JUMP), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "immediately goto to target chain that accepts", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "immediately goto to target chain that drops", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "immediately goto to target chain that continues with second rule that accepts", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "immediately goto to target chain that continues with second rule that drops", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_GOTO), ChainName: arbitraryTargetChain})), + targetOp: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "add data to register then accept", + baseOp1: mustCreateImmediate(t, linux.NFT_REG32_13, NewBytesData([]byte{0, 1, 2, 3})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "add data to register then drop", + baseOp1: mustCreateImmediate(t, linux.NFT_REG32_15, NewBytesData([]byte{0, 1, 2, 3})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "add data to register then continue", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_4, NewBytesData([]byte{0, 1, 2, 3})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NFT_CONTINUE)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, // from base chain policy + }, + { + tname: "multiple accepts", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "multiple drops", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + { + tname: "immediately accept then drop", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + verdict: Verdict{Code: VC(linux.NF_ACCEPT)}, + }, + { + tname: "immediately drop then accept", + baseOp1: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_DROP)})), + baseOp2: mustCreateImmediate(t, linux.NFT_REG_VERDICT, NewVerdictData(Verdict{Code: VC(linux.NF_ACCEPT)})), + verdict: Verdict{Code: VC(linux.NF_DROP)}, + }, + } { + t.Run(test.tname, func(t *testing.T) { + // Sets up an NFTables object with a base chain (for 2 rules) and another + // target chain (for 1 rule). + nf := NewNFTables() + tab, err := nf.AddTable(IP, "test", "test table", false) + if err != nil { + t.Fatalf("unexpected error for AddTable: %v", err) + } + bc, err := tab.AddChain("base_chain", nil, "test chain", false) + if err != nil { + t.Fatalf("unexpected error for AddChain: %v", err) + } + priority, err := NewStandardPriority("filter", IP, Prerouting) + if err != nil { + t.Fatalf("unexpected error for NewStandardPriority: %v", err) + } + bc.SetBaseChainInfo(&BaseChainInfo{ + BcType: BaseChainTypeFilter, + Hook: Prerouting, + Priority: priority, + }) + tc, err := tab.AddChain(arbitraryTargetChain, nil, "test chain", false) + if err != nil { + t.Fatalf("unexpected error for AddChain: %v", err) + } + + // Adds testing rules and operations. + if test.baseOp1 != nil { + rule1 := &Rule{} + bc.AddRule(rule1) + rule1.AddOperation(test.baseOp1) + } + if test.baseOp2 != nil { + rule2 := &Rule{} + bc.AddRule(rule2) + rule2.AddOperation(test.baseOp2) + } + if test.targetOp != nil { + ruleTarget := &Rule{} + tc.AddRule(ruleTarget) + ruleTarget.AddOperation(test.targetOp) + } + + pkt := makeTestingPacket() + v, err := nf.EvaluateHook(IP, Prerouting, pkt) + if err != nil { + t.Fatalf("unexpected error for EvaluateHook: %v", err) + } + if v.Code != test.verdict.Code { + t.Fatalf("expected verdict %s, got %s", test.verdict.String(), v.String()) + } + }) + } +} + // packetResultString compares 2 packets by equality and returns a string // representation. func packetResultString(initial, final *stack.PacketBuffer) string { @@ -95,3 +281,12 @@ func packetResultString(initial, final *stack.PacketBuffer) string { } return "modified" } + +// mustCreateImmediate wraps the NewImmediate function for brevity. +func mustCreateImmediate(t *testing.T, dreg uint8, data RegisterData) *Immediate { + imm, err := NewImmediate(dreg, data) + if err != nil { + t.Fatalf("failed to create immediate: %v", err) + } + return imm +}