From 02f28345d100f1d0b7e6775c6ef1dea40294cf91 Mon Sep 17 00:00:00 2001 From: houyuwushang Date: Mon, 29 Jun 2026 12:15:16 +0800 Subject: [PATCH] plugin/transfer: configure notify source address (#8192) Signed-off-by: houyuwushang --- plugin/transfer/README.md | 18 +++++++++++++++++- plugin/transfer/notify.go | 11 ++++++++++- plugin/transfer/notify_test.go | 27 +++++++++++++++++++++++++++ plugin/transfer/setup.go | 15 +++++++++++++++ plugin/transfer/setup_test.go | 34 ++++++++++++++++++++++++++++++++-- plugin/transfer/transfer.go | 5 +++-- 6 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 plugin/transfer/notify_test.go diff --git a/plugin/transfer/README.md b/plugin/transfer/README.md index 43c1623ae..4b21d937f 100644 --- a/plugin/transfer/README.md +++ b/plugin/transfer/README.md @@ -22,6 +22,7 @@ use this plugin. ~~~ transfer [ZONE...] { to ADDRESS... + source ADDRESS } ~~~ @@ -35,6 +36,10 @@ transfer [ZONE...] { an IP address and port e.g. `1.2.3.4`, `12:34::56`, `1.2.3.4:5300`, `[12:34::56]:5300`. `to` may be specified multiple times. + * `source` **ADDRESS** is the local IP address to use when sending zone change + notifications to the configured `to` addresses. It does not change which + clients are allowed to request AXFR or IXFR transfers. + You can use the _acl_ plugin to further restrict hosts permitted to receive a zone transfer. See example below. @@ -56,4 +61,15 @@ Use in conjunction with the _acl_ plugin to restrict access to subnet 10.1.0.0/1 ... ``` -Each plugin that can use _transfer_ includes an example of use in their respective documentation. \ No newline at end of file +Send NOTIFY messages from a specific local address. + +``` +... + transfer { + to 2001:db8::1 + source 2001:db8::53 + } +... +``` + +Each plugin that can use _transfer_ includes an example of use in their respective documentation. diff --git a/plugin/transfer/notify.go b/plugin/transfer/notify.go index af5bada46..9a938ae51 100644 --- a/plugin/transfer/notify.go +++ b/plugin/transfer/notify.go @@ -2,6 +2,7 @@ package transfer import ( "fmt" + "net" "github.com/coredns/coredns/plugin/pkg/rcode" @@ -16,13 +17,13 @@ func (t *Transfer) Notify(zone string) error { m := new(dns.Msg) m.SetNotify(zone) - c := new(dns.Client) x := longestMatch(t.xfrs, zone) if x == nil { // return without error if there is no matching zone return nil } + c := notifyClient(x) var err1 error for _, t := range x.to { @@ -37,6 +38,14 @@ func (t *Transfer) Notify(zone string) error { return err1 // this only captures the last error } +func notifyClient(x *xfr) *dns.Client { + c := new(dns.Client) + if x != nil && x.source != nil { + c.Dialer = &net.Dialer{LocalAddr: &net.UDPAddr{IP: x.source}} + } + return c +} + func sendNotify(c *dns.Client, m *dns.Msg, s string) error { var err error var ret *dns.Msg diff --git a/plugin/transfer/notify_test.go b/plugin/transfer/notify_test.go new file mode 100644 index 000000000..214f74e5e --- /dev/null +++ b/plugin/transfer/notify_test.go @@ -0,0 +1,27 @@ +package transfer + +import ( + "net" + "testing" +) + +func TestNotifyClientSource(t *testing.T) { + c := notifyClient(&xfr{}) + if c.Dialer != nil { + t.Fatalf("expected no dialer without a source address, got %#v", c.Dialer) + } + + source := net.ParseIP("2001:db8::53") + c = notifyClient(&xfr{source: source}) + if c.Dialer == nil { + t.Fatal("expected dialer for source address") + } + + addr, ok := c.Dialer.LocalAddr.(*net.UDPAddr) + if !ok { + t.Fatalf("expected UDP local address, got %T", c.Dialer.LocalAddr) + } + if !addr.IP.Equal(source) { + t.Fatalf("expected source address %s, got %s", source, addr.IP) + } +} diff --git a/plugin/transfer/setup.go b/plugin/transfer/setup.go index cd7d2091f..bb9c737a4 100644 --- a/plugin/transfer/setup.go +++ b/plugin/transfer/setup.go @@ -1,6 +1,8 @@ package transfer import ( + "net" + "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" @@ -68,6 +70,19 @@ func parseTransfer(c *caddy.Controller) (*Transfer, error) { } x.to = append(x.to, normalized) } + case "source": + args := c.RemainingArgs() + if len(args) != 1 { + return nil, c.ArgErr() + } + if x.source != nil { + return nil, plugin.Error("transfer", c.Err("source already specified")) + } + ip := net.ParseIP(args[0]) + if ip == nil { + return nil, plugin.Error("transfer", c.Errf("invalid source address %q", args[0])) + } + x.source = ip default: return nil, plugin.Error("transfer", c.Errf("unknown property %q", c.Val())) } diff --git a/plugin/transfer/setup_test.go b/plugin/transfer/setup_test.go index a0eb39f87..555471549 100644 --- a/plugin/transfer/setup_test.go +++ b/plugin/transfer/setup_test.go @@ -1,6 +1,7 @@ package transfer import ( + "net" "testing" "github.com/coredns/caddy" @@ -15,6 +16,7 @@ func TestParse(t *testing.T) { }{ {`transfer example.net example.org { to 1.2.3.4 5.6.7.8:1053 [1::2]:34 [fe80::1%eth0]:53 + source 2001:db8::53 } transfer example.com example.edu { to * 1.2.3.4 @@ -23,8 +25,9 @@ func TestParse(t *testing.T) { false, &Transfer{ xfrs: []*xfr{{ - Zones: []string{"example.net.", "example.org."}, - to: []string{"1.2.3.4:53", "5.6.7.8:1053", "[1::2]:34", "[fe80::1%eth0]:53"}, + Zones: []string{"example.net.", "example.org."}, + to: []string{"1.2.3.4:53", "5.6.7.8:1053", "[1::2]:34", "[fe80::1%eth0]:53"}, + source: net.ParseIP("2001:db8::53"), }, { Zones: []string{"example.com.", "example.edu."}, to: []string{"*", "1.2.3.4:53"}, @@ -45,6 +48,23 @@ func TestParse(t *testing.T) { true, nil, }, + {`transfer example.net example.org { + to 1.2.3.4 + source example.org + }`, + nil, + true, + nil, + }, + {`transfer example.net example.org { + to 1.2.3.4 + source 192.0.2.53 + source 2001:db8::53 + }`, + nil, + true, + nil, + }, { ` transfer example.com example.edu { @@ -109,10 +129,20 @@ func TestParse(t *testing.T) { t.Errorf("Test %d expected %v in 'to', got %v", i, tc.exp.xfrs[j].to[k], to) } } + if !sameIP(tc.exp.xfrs[j].source, x.source) { + t.Errorf("Test %d expected source %v, got %v", i, tc.exp.xfrs[j].source, x.source) + } } } } +func sameIP(a, b net.IP) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + return a.Equal(b) +} + func TestSetup(t *testing.T) { c := caddy.NewTestController("dns", "transfer") if err := setup(c); err == nil { diff --git a/plugin/transfer/transfer.go b/plugin/transfer/transfer.go index d4d26c982..dc59f5dff 100644 --- a/plugin/transfer/transfer.go +++ b/plugin/transfer/transfer.go @@ -23,8 +23,9 @@ type Transfer struct { } type xfr struct { - Zones []string - to []string + Zones []string + to []string + source net.IP } // Transferer may be implemented by plugins to enable zone transfers