Skip to content

Commit

Permalink
feat: implement IPv6 DHCP client in networkd
Browse files Browse the repository at this point in the history
This renames existing 'DHCP' implementation to `DHCP4`, new client is
`DHCP6`.

For now, `DHCP6` is disabled by default and should be explicitly enabled
with the config.

QEMU testbed for IPv6 is going to be pushed as separate PR.

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
  • Loading branch information
smira authored and talos-bot committed Feb 5, 2021
1 parent 5855b8d commit 6cf98a7
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import (

const dhcpReceivedRouteMetric uint32 = 1024

// DHCP implements the Addressing interface.
type DHCP struct {
// DHCP4 implements the Addressing interface.
type DHCP4 struct {
Offer *dhcpv4.DHCPv4
Ack *dhcpv4.DHCPv4
NetIf *net.Interface
Expand All @@ -34,39 +34,39 @@ type DHCP struct {
}

// Name returns back the name of the address method.
func (d *DHCP) Name() string {
return "dhcp"
func (d *DHCP4) Name() string {
return "dhcp4"
}

// Link returns the underlying net.Interface that this address
// method is configured for.
func (d *DHCP) Link() *net.Interface {
func (d *DHCP4) Link() *net.Interface {
return d.NetIf
}

// Discover handles the DHCP client exchange stores the DHCP Ack.
func (d *DHCP) Discover(ctx context.Context, link *net.Interface) error {
func (d *DHCP4) Discover(ctx context.Context, link *net.Interface) error {
d.NetIf = link
err := d.discover(ctx)

return err
}

// Address returns back the IP address from the received DHCP offer.
func (d *DHCP) Address() *net.IPNet {
func (d *DHCP4) Address() *net.IPNet {
return &net.IPNet{
IP: d.Ack.YourIPAddr,
Mask: d.Mask(),
}
}

// Mask returns the netmask from the DHCP offer.
func (d *DHCP) Mask() net.IPMask {
func (d *DHCP4) Mask() net.IPMask {
return d.Ack.SubnetMask()
}

// MTU returs the MTU size from the DHCP offer.
func (d *DHCP) MTU() uint32 {
func (d *DHCP4) MTU() uint32 {
mtuReturn := uint32(d.NetIf.MTU)

if d.Ack != nil {
Expand All @@ -86,7 +86,7 @@ func (d *DHCP) MTU() uint32 {
}

// TTL denotes how long a DHCP offer is valid for.
func (d *DHCP) TTL() time.Duration {
func (d *DHCP4) TTL() time.Duration {
if d.Ack == nil {
return 0
}
Expand All @@ -95,21 +95,17 @@ func (d *DHCP) TTL() time.Duration {
}

// Family qualifies the address as ipv4 or ipv6.
func (d *DHCP) Family() int {
if d.Ack.YourIPAddr.To4() != nil {
return unix.AF_INET
}

return unix.AF_INET6
func (d *DHCP4) Family() int {
return unix.AF_INET
}

// Scope sets the address scope.
func (d *DHCP) Scope() uint8 {
func (d *DHCP4) Scope() uint8 {
return unix.RT_SCOPE_UNIVERSE
}

// Valid denotes if this address method should be used.
func (d *DHCP) Valid() bool {
func (d *DHCP4) Valid() bool {
return d.Ack != nil
}

Expand All @@ -118,7 +114,7 @@ func (d *DHCP) Valid() bool {
// rfc3442:
// If the DHCP server returns both a Classless Static Routes option and
// a Router option, the DHCP client MUST ignore the Router option.
func (d *DHCP) Routes() (routes []*Route) {
func (d *DHCP4) Routes() (routes []*Route) {
metric := dhcpReceivedRouteMetric

if d.DHCPOptions != nil && d.DHCPOptions.RouteMetric() != 0 {
Expand Down Expand Up @@ -170,12 +166,12 @@ func (d *DHCP) Routes() (routes []*Route) {
}

// Resolvers returns the DNS resolvers from the DHCP offer.
func (d *DHCP) Resolvers() []net.IP {
func (d *DHCP4) Resolvers() []net.IP {
return d.Ack.DNS()
}

// Hostname returns the hostname from the DHCP offer.
func (d *DHCP) Hostname() (hostname string) {
func (d *DHCP4) Hostname() (hostname string) {
if d.Ack.HostName() == "" {
hostname = fmt.Sprintf("%s-%s", "talos", strings.ReplaceAll(d.Address().IP.String(), ".", "-"))
} else {
Expand All @@ -190,7 +186,7 @@ func (d *DHCP) Hostname() (hostname string) {
}

// discover handles the actual DHCP conversation.
func (d *DHCP) discover(ctx context.Context) error {
func (d *DHCP4) discover(ctx context.Context) error {
opts := []dhcpv4.OptionCode{
dhcpv4.OptionClasslessStaticRoute,
dhcpv4.OptionDomainNameServer,
Expand Down
203 changes: 203 additions & 0 deletions internal/app/networkd/pkg/address/dhcp6.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package address

import (
"context"
"fmt"
"log"
"net"
"strings"
"time"

"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/jsimonetti/rtnetlink"
"github.com/talos-systems/go-retry/retry"
"golang.org/x/sys/unix"
)

// DHCP6 implements the Addressing interface.
type DHCP6 struct {
Reply *dhcpv6.Message
NetIf *net.Interface
Mtu int
}

// Name returns back the name of the address method.
func (d *DHCP6) Name() string {
return "dhcp6"
}

// Link returns the underlying net.Interface that this address
// method is configured for.
func (d *DHCP6) Link() *net.Interface {
return d.NetIf
}

// Discover handles the DHCP client exchange stores the DHCP Ack.
func (d *DHCP6) Discover(ctx context.Context, link *net.Interface) error {
d.NetIf = link
err := d.discover(ctx)

return err
}

// Address returns back the IP address from the received DHCP offer.
func (d *DHCP6) Address() *net.IPNet {
if d.Reply.Options.OneIANA() == nil {
return nil
}

return &net.IPNet{
IP: d.Reply.Options.OneIANA().Options.OneAddress().IPv6Addr,
Mask: net.CIDRMask(128, 128),
}
}

// Mask returns the netmask from the DHCP offer.
func (d *DHCP6) Mask() net.IPMask {
return net.CIDRMask(128, 128)
}

// MTU returs the MTU size from the DHCP offer.
func (d *DHCP6) MTU() uint32 {
if d.Mtu > 0 {
return uint32(d.Mtu)
}

return uint32(d.NetIf.MTU)
}

// TTL denotes how long a DHCP offer is valid for.
func (d *DHCP6) TTL() time.Duration {
if d.Reply == nil {
return 0
}

return d.Reply.Options.OneIANA().Options.OneAddress().ValidLifetime
}

// Family qualifies the address as ipv4 or ipv6.
func (d *DHCP6) Family() int {
return unix.AF_INET6
}

// Scope sets the address scope.
func (d *DHCP6) Scope() uint8 {
return unix.RT_SCOPE_UNIVERSE
}

// Valid denotes if this address method should be used.
func (d *DHCP6) Valid() bool {
return d.Reply != nil && d.Reply.Options.OneIANA() != nil
}

// Routes is not supported on IPv6.
func (d *DHCP6) Routes() (routes []*Route) {
return nil
}

// Resolvers returns the DNS resolvers from the DHCP offer.
func (d *DHCP6) Resolvers() []net.IP {
return d.Reply.Options.DNS()
}

// Hostname returns the hostname from the DHCP offer.
func (d *DHCP6) Hostname() (hostname string) {
fqdn := d.Reply.Options.FQDN()

if fqdn != nil && fqdn.DomainName != nil {
hostname = strings.Join(fqdn.DomainName.Labels, ".")
} else {
hostname = fmt.Sprintf("%s-%s", "talos", strings.ReplaceAll(d.Address().IP.String(), ":", ""))
}

return hostname
}

// discover handles the actual DHCP conversation.
func (d *DHCP6) discover(ctx context.Context) error {
if err := waitIPv6LinkReady(d.NetIf); err != nil {
log.Printf("failed waiting for IPv6 readiness: %s", err)

return err
}

cli, err := nclient6.New(d.NetIf.Name)
if err != nil {
log.Printf("failed to create dhcp6 client: %s", err)

return err
}

// nolint: errcheck
defer cli.Close()

reply, err := cli.RapidSolicit(ctx)
if err != nil {
// TODO: Make this a well defined error so we can make it not fatal
log.Printf("failed dhcp6 request for %q: %v", d.NetIf.Name, err)

return err
}

log.Printf("DHCP6 REPLY on %q: %s", d.NetIf.Name, collapseSummary(reply.Summary()))

d.Reply = reply

return nil
}

func waitIPv6LinkReady(iface *net.Interface) error {
conn, err := rtnetlink.Dial(nil)
if err != nil {
return err
}

defer conn.Close() //nolint: errcheck

return retry.Constant(30*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(func() error {
ready, err := isIPv6LinkReady(iface, conn)
if err != nil {
return retry.UnexpectedError(err)
}

if !ready {
return retry.ExpectedError(fmt.Errorf("IPv6 address is still tentative"))
}

return nil
})
}

// isIPv6LinkReady returns true if the interface has a link-local address
// which is not tentative.
func isIPv6LinkReady(iface *net.Interface, conn *rtnetlink.Conn) (bool, error) {
addrs, err := conn.Address.List()
if err != nil {
return false, err
}

for _, addr := range addrs {
if addr.Index != uint32(iface.Index) {
continue
}

if addr.Family != unix.AF_INET6 {
continue
}

if addr.Attributes.Address.IsLinkLocalUnicast() && (addr.Flags&unix.IFA_F_TENTATIVE == 0) {
if addr.Flags&unix.IFA_F_DADFAILED != 0 {
log.Printf("DADFAILED for %v, continuing anyhow", addr.Attributes.Address)
}

return true, nil
}
}

return false, nil
}
11 changes: 9 additions & 2 deletions internal/app/networkd/pkg/networkd/netconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,15 @@ func buildOptions(device config.Device, hostname string) (name string, opts []ni

opts = append(opts, nic.WithAddressing(s))
case device.DHCP():
d := &address.DHCP{DHCPOptions: device.DHCPOptions(), RouteList: device.Routes(), Mtu: device.MTU()}
opts = append(opts, nic.WithAddressing(d))
if device.DHCPOptions().IPv4() {
d := &address.DHCP4{DHCPOptions: device.DHCPOptions(), RouteList: device.Routes(), Mtu: device.MTU()}
opts = append(opts, nic.WithAddressing(d))
}

if device.DHCPOptions().IPv6() {
d := &address.DHCP6{Mtu: device.MTU()}
opts = append(opts, nic.WithAddressing(d))
}
default:
// Allow master interface without any addressing if VLANs exist
if len(device.Vlans()) > 0 {
Expand Down
6 changes: 3 additions & 3 deletions internal/app/networkd/pkg/networkd/networkd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (suite *NetworkdSuite) TestHostname() {
suite.Require().NoError(err)

nwd.Interfaces["eth0"].AddressMethod = []address.Addressing{
&address.DHCP{
&address.DHCP4{
Ack: &dhcpv4.DHCPv4{
YourIPAddr: net.ParseIP("192.168.0.11"),
Options: dhcpv4.Options{
Expand All @@ -131,7 +131,7 @@ func (suite *NetworkdSuite) TestHostname() {
suite.Require().NoError(err)

nwd.Interfaces["eth0"].AddressMethod = []address.Addressing{
&address.DHCP{
&address.DHCP4{
Ack: &dhcpv4.DHCPv4{
YourIPAddr: net.ParseIP("192.168.0.11"),
Options: dhcpv4.Options{
Expand All @@ -148,7 +148,7 @@ func (suite *NetworkdSuite) TestHostname() {

// DHCP without OptionHostname and with OptionDomainName
nwd.Interfaces["eth0"].AddressMethod = []address.Addressing{
&address.DHCP{
&address.DHCP4{
Ack: &dhcpv4.DHCPv4{
YourIPAddr: net.ParseIP("192.168.0.11"),
Options: dhcpv4.Options{
Expand Down
2 changes: 1 addition & 1 deletion internal/app/networkd/pkg/nic/nic.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func New(setters ...Option) (*NetworkInterface, error) {
// If no addressing methods have been configured, default to DHCP.
// If VLANs exist do not force DHCP on master device
if len(iface.AddressMethod) == 0 && len(iface.Vlans) == 0 {
iface.AddressMethod = append(iface.AddressMethod, &address.DHCP{})
iface.AddressMethod = append(iface.AddressMethod, &address.DHCP4{}) // TODO: enable DHCPv6 by default?
}

// Handle netlink connection
Expand Down
2 changes: 1 addition & 1 deletion internal/app/networkd/pkg/nic/vlan_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func WithVlanDhcp(id uint16) Option {
return func(n *NetworkInterface) (err error) {
for _, vlan := range n.Vlans {
if vlan.ID == id {
vlan.AddressMethod = append(vlan.AddressMethod, &address.DHCP{})
vlan.AddressMethod = append(vlan.AddressMethod, &address.DHCP4{}) // TODO: should we enable DHCP6 by default?

return nil
}
Expand Down

0 comments on commit 6cf98a7

Please sign in to comment.