plugin/tsig: add require_opcode directive for opcode-based TSIG (#7828)

Extend the tsig plugin to require TSIG signatures based on DNS opcodes,
similar to the existing qtype-based requirement.

The new require_opcode directive accepts opcode names (QUERY, IQUERY,
STATUS, NOTIFY, UPDATE) or the special values "all" and "none".

This is useful for requiring TSIG on dynamic update (UPDATE) or zone
transfer notification (NOTIFY) requests while allowing unsigned queries.

Example:
```
  tsig {
    secret key. NoTCJU+DMqFWywaPyxSijrDEA/eC3nK0xi3AMEZuPVk=
    require_opcode UPDATE NOTIFY
  }
```

Signed-off-by: Seena Fallah <seenafallah@gmail.com>
This commit is contained in:
Seena Fallah
2026-03-27 20:05:49 +01:00
committed by GitHub
parent 0918e88368
commit 471d62926d
5 changed files with 228 additions and 42 deletions

View File

@@ -32,49 +32,85 @@ key "name2.key." {
defer cleanup()
tests := []struct {
input string
shouldErr bool
expectedZones []string
expectedQTypes qTypes
expectedSecrets map[string]string
expectedAll bool
input string
shouldErr bool
expectedZones []string
expectedQTypes qTypes
expectedOpCodes opCodes
expectedSecrets map[string]string
expectedAllTypes bool
expectedAllOpcodes bool
}{
{
input: "tsig {\n " + secretConfig + "}",
expectedZones: []string{"."},
expectedQTypes: defaultQTypes,
expectedOpCodes: defaultOpCodes,
expectedSecrets: secrets,
},
{
input: "tsig {\n secrets " + secretsFile + "\n}",
expectedZones: []string{"."},
expectedQTypes: defaultQTypes,
expectedOpCodes: defaultOpCodes,
expectedSecrets: secrets,
},
{
input: "tsig example.com {\n " + secretConfig + "}",
expectedZones: []string{"example.com."},
expectedQTypes: defaultQTypes,
expectedOpCodes: defaultOpCodes,
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " require all \n}",
expectedZones: []string{"."},
expectedQTypes: qTypes{},
expectedAll: true,
expectedSecrets: secrets,
input: "tsig {\n " + secretConfig + " require all \n}",
expectedZones: []string{"."},
expectedQTypes: qTypes{},
expectedOpCodes: defaultOpCodes,
expectedAllTypes: true,
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " require none \n}",
expectedZones: []string{"."},
expectedQTypes: qTypes{},
expectedAll: false,
expectedOpCodes: defaultOpCodes,
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " \n require A AAAA \n}",
expectedZones: []string{"."},
expectedQTypes: qTypes{dns.TypeA: {}, dns.TypeAAAA: {}},
expectedOpCodes: defaultOpCodes,
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " \n require_opcode UPDATE NOTIFY \n}",
expectedZones: []string{"."},
expectedQTypes: defaultQTypes,
expectedOpCodes: opCodes{dns.OpcodeUpdate: {}, dns.OpcodeNotify: {}},
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " \n require_opcode all \n}",
expectedZones: []string{"."},
expectedQTypes: defaultQTypes,
expectedOpCodes: opCodes{},
expectedAllOpcodes: true,
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " \n require_opcode none \n}",
expectedZones: []string{"."},
expectedQTypes: defaultQTypes,
expectedOpCodes: opCodes{},
expectedSecrets: secrets,
},
{
input: "tsig {\n " + secretConfig + " \n require AXFR \n require_opcode UPDATE \n}",
expectedZones: []string{"."},
expectedQTypes: qTypes{dns.TypeAXFR: {}},
expectedOpCodes: opCodes{dns.OpcodeUpdate: {}},
expectedSecrets: secrets,
},
{
@@ -93,6 +129,14 @@ key "name2.key." {
input: "tsig {\n require invalid-qtype \n}",
shouldErr: true,
},
{
input: "tsig {\n require_opcode \n}",
shouldErr: true,
},
{
input: "tsig {\n require_opcode INVALID \n}",
shouldErr: true,
},
}
serverBlockKeys := []string{"."}
@@ -121,8 +165,12 @@ key "name2.key." {
}
}
if test.expectedAll != ts.all {
t.Errorf("Test %d expected require all to be '%v', but got '%v'.", i, test.expectedAll, ts.all)
if test.expectedAllTypes != ts.allTypes {
t.Errorf("Test %d expected require all types to be '%v', but got '%v'.", i, test.expectedAllTypes, ts.allTypes)
}
if test.expectedAllOpcodes != ts.allOpcodes {
t.Errorf("Test %d expected require all opcodes to be '%v', but got '%v'.", i, test.expectedAllOpcodes, ts.allOpcodes)
}
if len(test.expectedQTypes) != len(ts.types) {
@@ -135,6 +183,16 @@ key "name2.key." {
}
}
if len(test.expectedOpCodes) != len(ts.opcodes) {
t.Fatalf("Test %d expected required opcodes '%v', but got '%v'.", i, test.expectedOpCodes, ts.opcodes)
}
for op := range test.expectedOpCodes {
if _, ok := ts.opcodes[op]; !ok {
t.Errorf("Test %d required opcodes '%v', but got '%v'.", i, test.expectedOpCodes, ts.opcodes)
break
}
}
if len(test.expectedSecrets) != len(ts.secrets) {
t.Fatalf("Test %d expected secrets '%v', but got '%v'.", i, test.expectedSecrets, ts.secrets)
}