From 6e88dd9f8199cdaedb9625b2ce3c5861bfc7ab43 Mon Sep 17 00:00:00 2001 From: Jared Baur Date: Sun, 10 Mar 2024 17:22:03 -0700 Subject: [PATCH] internal/*: add PREF64 support Adds initial PREF64 support. Multiple prefixes can be added on an interface, defaulting to a single prefix of the RFC 6052 reserved value "64:ff9b::/96" if the config's TOML section is defined but the prefix value is either unset or empty. --- go.mod | 10 ++++---- go.sum | 20 ++++++++-------- internal/config/config.go | 5 ++++ internal/config/config_test.go | 4 ++++ internal/config/plugin.go | 22 +++++++++++++++++ internal/config/reference.toml | 6 +++++ internal/plugin/plugin.go | 44 ++++++++++++++++++++++++++++++++++ internal/plugin/plugin_test.go | 30 ++++++++++++++++++++--- 8 files changed, 124 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index dda90f9..ab9ca5a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/mdlayher/corerad go 1.21 require ( - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/jsimonetti/rtnetlink v1.3.3 github.com/mdlayher/metricslite v0.0.0-20220406114248-d75c70dd4887 github.com/mdlayher/ndp v1.0.1 @@ -13,9 +13,9 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/prometheus/client_golang v1.16.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 - golang.org/x/net v0.11.0 + golang.org/x/net v0.22.0 golang.org/x/sync v0.3.0 - golang.org/x/sys v0.9.0 + golang.org/x/sys v0.18.0 ) require ( @@ -28,6 +28,8 @@ require ( github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.30.0 // indirect ) + +replace github.com/mdlayher/ndp => github.com/jmbaur/ndp v0.0.0-20240312135007-dce8f215d01a diff --git a/go.sum b/go.sum index d487486..f9c5a72 100644 --- a/go.sum +++ b/go.sum @@ -115,8 +115,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -133,6 +133,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmbaur/ndp v0.0.0-20240312135007-dce8f215d01a h1:JvdIAGXWZeRJ/2xWT7HYK/l4m3GkAly3U/G1/iJ28Js= +github.com/jmbaur/ndp v0.0.0-20240312135007-dce8f215d01a/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -158,8 +160,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mdlayher/metricslite v0.0.0-20220406114248-d75c70dd4887 h1:bQHlyj7L8//jVylGe6HYuTqfUweTtAqHmhuOLy6C3vs= github.com/mdlayher/metricslite v0.0.0-20220406114248-d75c70dd4887/go.mod h1:BqYH//q1ULAuVmKB/whePnSt4JCyGBV7bxZU6CjKR1s= -github.com/mdlayher/ndp v1.0.1 h1:+yAD79/BWyFlvAoeG5ncPS0ItlHP/eVbH7bQ6/+LVA4= -github.com/mdlayher/ndp v1.0.1/go.mod h1:rf3wKaWhAYJEXFKpgF8kQ2AxypxVbfNcZbqoAo6fVzk= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/schedgroup v1.0.0 h1:MJS37Rkver2jHaRV5WE5xszN56xZt5yQlqNjtBI7Hsc= @@ -294,8 +294,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -353,8 +353,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -364,8 +364,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/config/config.go b/internal/config/config.go index 2c59537..b5b2875 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,7 @@ type rawInterface struct { Routes []rawRoute `toml:"route"` RDNSS []rawRDNSS `toml:"rdnss"` DNSSL []rawDNSSL `toml:"dnssl"` + PREF64 []rawPREF64 `toml:"pref64"` MTU int `toml:"mtu"` SourceLLA *bool `toml:"source_lla"` CaptivePortal string `toml:"captive_portal"` @@ -117,6 +118,10 @@ type rawRDNSS struct { Servers []string `toml:"servers"` } +type rawPREF64 struct { + Prefix *string `toml:"prefix"` +} + // Config specifies the configuration for CoreRAD. type Config struct { // User-specified. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 6b0c88d..f45a445 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -154,6 +154,9 @@ func TestParse(t *testing.T) { lifetime = "auto" domain_names = ["lan.example.com"] + [[interfaces.pref64]] + prefix = "" + [[interfaces]] name = "eth1" min_interval = "auto" @@ -244,6 +247,7 @@ func TestParse(t *testing.T) { }, plugin.NewMTU(1500), &plugin.LLA{}, + plugin.NewPREF64(netip.MustParsePrefix("64:ff9b::/96"), 10 * time.Minute), }, }, { diff --git a/internal/config/plugin.go b/internal/config/plugin.go index a91ae15..ca2423e 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -24,6 +24,10 @@ import ( "golang.org/x/exp/slices" ) +// The Well-Known Prefix for IPv4 to IPv6 translation, as specified in RFC +// 6052 Section 2.1. +const defaultPREF64Prefix = "64:ff9b::/96" + // parsePlugin parses raw plugin configuration into a slice of plugins. func parsePlugins(ifi rawInterface, maxInterval time.Duration, epoch time.Time) ([]plugin.Plugin, error) { prefixes := make([]*plugin.Prefix, 0, len(ifi.Prefixes)) @@ -128,6 +132,24 @@ func parsePlugins(ifi rawInterface, maxInterval time.Duration, epoch time.Time) plugins = append(plugins, cp) } + fallbackPREF64 := defaultPREF64Prefix + for _, p := range ifi.PREF64 { + if p.Prefix == nil { + p.Prefix = &fallbackPREF64 + } else if *p.Prefix == "" { + p.Prefix = &fallbackPREF64 + } + + prefix, err := netip.ParsePrefix(*p.Prefix) + if err != nil { + return nil, err + } + + pref64 := plugin.NewPREF64(prefix, maxInterval) + + plugins = append(plugins, pref64) + } + return plugins, nil } diff --git a/internal/config/reference.toml b/internal/config/reference.toml index c1ae14d..7c678a7 100644 --- a/internal/config/reference.toml +++ b/internal/config/reference.toml @@ -209,6 +209,12 @@ preference = "medium" lifetime = "auto" domain_names = ["foo.example.com"] + # Enable PREF64 on the interface using the given prefix. Note that this often + # requires further configuration of the network for NAT64 and/or DNS64. If an + # empty string is given, the default prefix of "64:ff9b::/96" is assumed. + [[interfaces.pref64]] + prefix = "64:ff9b::/96" + # Enable or disable the debug HTTP server for facilities such as Prometheus # metrics and pprof support. # diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 939d442..6eff845 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -79,6 +79,50 @@ func (cp *CaptivePortal) Apply(ra *ndp.RouterAdvertisement) error { return nil } +type PREF64 struct { + Inner *ndp.PREF64 +} + +func NewPREF64(prefix netip.Prefix, maxInterval time.Duration) *PREF64 { + // Calculate the scaled lifetime using MaxRtrAdvInterval. + // See https://datatracker.ietf.org/doc/html/rfc8781#section-4.1-2 + maxLifetime := 8191 * 8 * time.Second + + lifetime := maxLifetime + + if int(maxInterval.Seconds())*3 < int(lifetime.Seconds()) { + + lifetimeSeconds := int(maxInterval.Seconds()) + + if r := int(lifetimeSeconds) % 8; r > 0 { + lifetimeSeconds += 8 - r + } + + lifetime = time.Duration(lifetimeSeconds) * time.Second + } + + return &PREF64{Inner: &ndp.PREF64{Prefix: prefix, Lifetime: lifetime}} +} + +// Name implements Plugin. +func (*PREF64) Name() string { return "pref64" } + +// String implements Plugin. +func (p *PREF64) String() string { + return fmt.Sprintf("%s, lifetime: %s", p.Inner.Prefix, p.Inner.Lifetime) +} + +// Prepare implements Plugin. +// func (*PREF64) Prepare(_ *net.Interface) error { return nil } +func (*PREF64) Prepare(_ *net.Interface) error { return nil } + +// Apply implements Plugin. +func (p *PREF64) Apply(ra *ndp.RouterAdvertisement) error { + ra.Options = append(ra.Options, p.Inner) + + return nil +} + // DNSSL configures a NDP DNS Search List option. type DNSSL struct { Lifetime time.Duration diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 00bc876..4e960f2 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -50,6 +50,16 @@ func TestPluginString(t *testing.T) { p: UnrestrictedPortal(), s: `URI: "urn:ietf:params:capport:unrestricted"`, }, + { + name: "PREF64", + p: &PREF64{ + &ndp.PREF64{ + Lifetime: time.Minute * 10, + Prefix: netip.MustParsePrefix("2001:db8::/96"), + }, + }, + s: "2001:db8::/96, lifetime: 10m0s", + }, { name: "DNSSL", p: &DNSSL{ @@ -184,6 +194,19 @@ func TestBuild(t *testing.T) { }, ok: true, }, + { + name: "PREF64", + plugin: NewPREF64(netip.MustParsePrefix("2001:db8::/96"), 24*time.Hour), + ra: &ndp.RouterAdvertisement{ + Options: []ndp.Option{ + &ndp.PREF64{ + Lifetime: 8191 * 8 * time.Second, // max lifetime + Prefix: netip.MustParsePrefix("2001:db8::/96"), + }, + }, + }, + ok: true, + }, { name: "DNSSL", plugin: &DNSSL{ @@ -641,7 +664,7 @@ func TestBuild(t *testing.T) { return } - if diff := cmp.Diff(tt.ra, ra, cmp.Comparer(addrEqual)); diff != "" { + if diff := cmp.Diff(tt.ra, ra, cmp.Comparer(addrEqual), cmp.Comparer(prefixEqual)); diff != "" { t.Fatalf("unexpected RA (-want +got):\n%s", diff) } }) @@ -806,8 +829,9 @@ func Test_betterRDNSS(t *testing.T) { } } -func addrEqual(x, y netip.Addr) bool { return x == y } -func ipEqual(x, y system.IP) bool { return x == y } +func addrEqual(x, y netip.Addr) bool { return x == y } +func ipEqual(x, y system.IP) bool { return x == y } +func prefixEqual(x, y netip.Prefix) bool { return x == y } func mustCaptivePortal(uri string) *ndp.CaptivePortal { cp, err := ndp.NewCaptivePortal(uri)