Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/plugin: implement RDNSS wildcard syntax #25

Merged
merged 1 commit into from
Jun 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ func TestParse(t *testing.T) {
source_lla = true
preference = "low"

[[interfaces.rdnss]]
servers = ["::"]

[[interfaces]]
name = "eth2"
verbose = true
Expand Down Expand Up @@ -219,7 +222,13 @@ func TestParse(t *testing.T) {
RetransmitTimer: 5 * time.Second,
DefaultLifetime: 8 * time.Second,
Preference: ndp.Low,
Plugins: []plugin.Plugin{&plugin.LLA{}},
Plugins: []plugin.Plugin{
&plugin.RDNSS{
Lifetime: 8 * time.Second,
Servers: []netaddr.IP{netaddr.IPv6Unspecified()},
},
&plugin.LLA{},
},
},
{
Name: "eth2",
Expand Down Expand Up @@ -299,7 +308,7 @@ func TestParseDefaults(t *testing.T) {
prefix = "2001:db8:ffff::/64"

[[interfaces.rdnss]]
servers = ["2001:db8::1", "2001:db8::2"]
servers = ["::"]

[[interfaces.dnssl]]
domain_names = ["foo.example.com"]
Expand Down
8 changes: 7 additions & 1 deletion internal/config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,18 @@ preference = "medium"

# RDNSS: attaches a NDP Recursive DNS Servers option to the router advertisement.
[[interfaces.rdnss]]
# The DNS servers which should be advertised. Explicit IPv6 addresses may be
# specified, but the special :: wildcard will choose a suitable IPv6 address
# (preferring Unique Local Addresses, then Global Unicast Addresses, then
# Link-Local Addresses) from this interface to serve in the event that the DNS
# server resides on the same interface as CoreRAD.
servers = ["::"]

# The maximum time these RDNSS addresses may be used for name resolution.
# An empty string or 0 means these servers should no longer be used.
# "auto" will compute a sane default. "infinite" means these servers should
# be used forever.
lifetime = "auto"
servers = ["2001:db8::1", "2001:db8::2"]

# DNSSL: attaches a NDP DNS Search List option to the router advertisement.
[[interfaces.dnssl]]
Expand Down
2 changes: 2 additions & 0 deletions internal/config/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ func parseRDNSS(d rawRDNSS, maxInterval time.Duration) (*plugin.RDNSS, error) {
}

if len(d.Servers) == 0 {
// TODO(mdlayher): should this imply the :: wildcard support? If so
// consider also implying ::/64 for empty prefix option.
return nil, errors.New("must specify one or more DNS server IPv6 addresses")
}

Expand Down
13 changes: 13 additions & 0 deletions internal/config/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,19 @@ func Test_parseRDNSS(t *testing.T) {
},
ok: true,
},
{
name: "OK wildcard server",
s: `
[[interfaces]]
[[interfaces.rdnss]]
servers = ["::"]
`,
r: &plugin.RDNSS{
Lifetime: 20 * time.Minute,
Servers: []netaddr.IP{netaddr.IPv6Unspecified()},
},
ok: true,
},
}

for _, tt := range tests {
Expand Down
147 changes: 141 additions & 6 deletions internal/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package plugin

import (
"errors"
"fmt"
"net"
"sort"
Expand Down Expand Up @@ -393,8 +394,12 @@ func (r *Route) lifetime() time.Duration {

// RDNSS configures a NDP Recursive DNS Servers option.
type RDNSS struct {
// Parameters from configuration.
Lifetime time.Duration
Servers []netaddr.IP

// Functions which can be swapped for tests.
Addrs func() ([]net.Addr, error)
}

// Name implements Plugin.
Expand All @@ -407,26 +412,148 @@ func (r *RDNSS) String() string {
ips = append(ips, s.String())
}

return fmt.Sprintf("servers: [%s], lifetime: %s",
strings.Join(ips, ", "), durString(r.Lifetime))
servers := fmt.Sprintf("[%s]", strings.Join(ips, ", "))
if r.wildcard() {
// Make a best-effort to note the current server if the user is using
// the wildcard syntax. If this returns an error, we'll return "::"
// with no further information.
if s, err := r.currentServer(); err == nil {
servers = fmt.Sprintf(":: [%s]", s.String())
}
}

return fmt.Sprintf("servers: %s, lifetime: %s", servers, durString(r.Lifetime))
}

// Prepare implements Plugin.
func (*RDNSS) Prepare(_ *net.Interface) error { return nil }
func (r *RDNSS) Prepare(ifi *net.Interface) error {
// Fetch addresses from the specified interface whenever invoked.
r.Addrs = ifi.Addrs
return nil
}

// Apply implements Plugin.
func (r *RDNSS) Apply(ra *ndp.RouterAdvertisement) error {
ips := make([]net.IP, 0, len(r.Servers))
for _, s := range r.Servers {
if !r.wildcard() {
// User specified exact servers so apply them directly.
r.applyServers(r.Servers, ra)
return nil
}

// User specified the :: wildcard syntax, automatically choose a DNS server
// address from this interface.
server, err := r.currentServer()
if err != nil {
return err
}

// Produce a RecursiveDNSServers option for this server.
r.applyServers([]netaddr.IP{server}, ra)
return nil
}

// applyServers unpacks servers into an ndp.RecursiveDNSServer option within ra.
func (r *RDNSS) applyServers(servers []netaddr.IP, ra *ndp.RouterAdvertisement) {
ips := make([]net.IP, 0, len(servers))
for _, s := range servers {
ips = append(ips, s.IPAddr().IP)
}

ra.Options = append(ra.Options, &ndp.RecursiveDNSServer{
Lifetime: r.Lifetime,
Servers: ips,
})
}

return nil
// wildcard determines if the RDNSS option is configured with the :: wildcard
// syntax.
func (r *RDNSS) wildcard() bool {
// TODO(mdlayher): allow both wildcard and non-wildcard servers?
return len(r.Servers) == 1 && r.Servers[0] == netaddr.IPv6Unspecified()
}

// currentServer fetches the current DNS server IP from the interface.
func (r *RDNSS) currentServer() (netaddr.IP, error) {
// Expand :: to one of the IPv6 addresses on this interface.
addrs, err := r.Addrs()
if err != nil {
return netaddr.IP{}, fmt.Errorf("failed to fetch IP addresses: %v", err)
}

var ips []netaddr.IP
for _, a := range addrs {
ipn, ok := a.(*net.IPNet)
if !ok {
continue
}

ipp, ok := netaddr.FromStdIPNet(ipn)
if !ok {
panicf("corerad: invalid net.IPNet: %+v", a)
}

// Only consider IPv6 addresses.
if ipp.IP().Is4() {
continue
}

ips = append(ips, ipp.IP())
}

switch len(ips) {
case 0:
// No IPv6 addresses, cannot use wildcard syntax.
return netaddr.IP{}, errors.New("interface has no IPv6 addresses")
case 1:
// One IPv6 address, use that one.
return ips[0], nil
}

// More than one IPv6 address was found. Now that we've gathered the
// addresses on this interface, select one as follows:
//
// 1) Unique Local Address (ULA)
// - if assigned, high probability of use for internal-only services.
// 2) Global Unicast Address (GUA)
// - de-facto choice when ULA is not available.
// 3) Link-Local Address (LLA)
// - last resort, doesn't work across subnets but since this machine is
// also running CoreRAD that may not be a problem.
//
// In the event of a tie, the lesser address by byte comparison wins.
//
// TODO(mdlayher): actually consider OS-specific data like
// temporary/deprecated address flags.
//
// TODO(mdlayher): infer permanence of an address from EUI-64 format.
sort.SliceStable(ips, func(i, j int) bool {
// Prefer ULA.
if isI, isJ := isULA(ips[i]), isULA(ips[j]); isI && !isJ {
return true
} else if isJ && !isI {
return false
}

// Prefer GUA.
if isI, isJ := isGUA(ips[i]), isGUA(ips[j]); isI && !isJ {
return true
} else if isJ && !isI {
return false
}

// Prefer LLA.
if isI, isJ := ips[i].IsLinkLocalUnicast(), ips[j].IsLinkLocalUnicast(); isI && !isJ {
return true
} else if isJ && !isI {
return false
}

// Tie-breaker: prefer lowest address.
return ips[i].Less(ips[j])
})

// The first address wins.
return ips[0], nil
}

// durString converts a time.Duration into a string while also recognizing
Expand All @@ -440,6 +567,14 @@ func durString(d time.Duration) string {
}
}

// TODO(mdlayher): upstream into inet.af/netaddr.

func isGUA(ip netaddr.IP) bool { return ip.IPAddr().IP.IsGlobalUnicast() }

var ula = netaddr.MustParseIPPrefix("fc00::/7")

func isULA(ip netaddr.IP) bool { return ula.Contains(ip) }

func panicf(format string, a ...interface{}) {
panic(fmt.Sprintf(format, a...))
}
Loading