dhcp: switch to github.com/rtr7/dhcp4

All existing DHCPv4 packages I looked at were unappealing for one reason or
another, so we’re now using a little helper to glue github.com/google/gopacket
and github.com/mdlayher/raw together, which suffices for our use-case and gives
us more control.
This commit is contained in:
Michael Stapelberg 2018-11-21 08:43:49 +01:00
parent a6ce446055
commit 14287515bc

View File

@ -9,7 +9,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -19,101 +18,109 @@ import (
"syscall" "syscall"
"time" "time"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
"github.com/gokrazy/gokrazy/internal/iface" "github.com/gokrazy/gokrazy/internal/iface"
"github.com/google/gopacket/layers"
"github.com/mdlayher/raw"
"github.com/rtr7/dhcp4"
"golang.org/x/sys/unix"
) )
func parseDHCPDuration(b []byte) time.Duration {
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
}
var ( var (
defaultDst = net.IP([]byte{0, 0, 0, 0}) defaultDst = net.IP([]byte{0, 0, 0, 0})
defaultNetmask = net.IPMask([]byte{0, 0, 0, 0}) defaultNetmask = net.IPMask([]byte{0, 0, 0, 0})
hardwareAddr net.HardwareAddr
) )
func addHostname(p *dhcp4.Packet) { type client struct {
var utsname unix.Utsname hostname string
if err := unix.Uname(&utsname); err != nil { hardwareAddr net.HardwareAddr
log.Fatal(err) generateXID func() uint32
} conn net.PacketConn
nnb := utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)]
p.AddOption(dhcp4.OptionHostName, nnb)
} }
func addClientId(p *dhcp4.Packet) { func (c *client) packet(xid uint32, opts []layers.DHCPOption) *layers.DHCPv4 {
id := make([]byte, len(hardwareAddr)+1) return &layers.DHCPv4{
id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700 Operation: layers.DHCPOpRequest,
copy(id[1:], hardwareAddr) HardwareType: layers.LinkTypeEthernet,
p.AddOption(dhcp4.OptionClientIdentifier, id) HardwareLen: uint8(len(layers.EthernetBroadcast)),
HardwareOpts: 0, // TODO: document
Xid: xid,
Secs: 0, // TODO: fill in?
Flags: 0, // TODO: document
ClientHWAddr: c.hardwareAddr,
ServerName: nil,
File: nil,
Options: opts,
}
} }
// dhcpRequest is a copy of (dhcp4client/Client).Request which func (c *client) discover() (*layers.DHCPv4, error) {
// includes the hostname. discover := c.packet(c.generateXID(), []layers.DHCPOption{
func dhcpRequest(c *dhcp4client.Client) (bool, dhcp4.Packet, error) { dhcp4.MessageTypeOpt(layers.DHCPMsgTypeDiscover),
discoveryPacket := c.DiscoverPacket() dhcp4.HostnameOpt(c.hostname),
addHostname(&discoveryPacket) dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
addClientId(&discoveryPacket) dhcp4.ParamsRequestOpt(
discoveryPacket.PadToMinSize() layers.DHCPOptDNS,
layers.DHCPOptRouter,
if err := c.SendPacket(discoveryPacket); err != nil { layers.DHCPOptSubnetMask),
return false, discoveryPacket, err })
if err := dhcp4.Write(c.conn, discover); err != nil {
return nil, err
} }
offerPacket, err := c.GetOffer(&discoveryPacket) // Look for DHCPOFFER packet (TODO: RFC)
c.conn.SetDeadline(time.Now().Add(5 * time.Second))
for {
offer, err := dhcp4.Read(c.conn)
if err != nil { if err != nil {
return false, offerPacket, err return nil, err
} }
if offer == nil {
requestPacket := c.RequestPacket(&offerPacket) continue // not a DHCPv4 packet
addHostname(&requestPacket)
addClientId(&requestPacket)
requestPacket.PadToMinSize()
if err := c.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
} }
if offer.Xid != discover.Xid {
acknowledgement, err := c.GetAcknowledgement(&requestPacket) continue // broadcast reply for different DHCP transaction
if err != nil {
return false, acknowledgement, err
} }
if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
acknowledgementOptions := acknowledgement.ParseOptions() continue
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { }
return false, acknowledgement, nil return offer, nil
} }
return true, acknowledgement, nil
} }
// dhcpRenew is a copy of (dhcp4client/Client).Renew which func (c *client) request(last *layers.DHCPv4) (*layers.DHCPv4, error) {
// includes the hostname. // Build a DHCPREQUEST packet:
func dhcpRenew(c *dhcp4client.Client, packet dhcp4.Packet) (bool, dhcp4.Packet, error) { request := c.packet(last.Xid, append([]layers.DHCPOption{
addHostname(&packet) dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRequest),
addClientId(&packet) dhcp4.RequestIPOpt(last.YourClientIP),
packet.PadToMinSize() dhcp4.HostnameOpt(c.hostname),
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
if err := c.SendPacket(packet); err != nil { dhcp4.ParamsRequestOpt(
return false, packet, err layers.DHCPOptDNS,
layers.DHCPOptRouter,
layers.DHCPOptSubnetMask),
}, dhcp4.ServerID(last.Options)...))
if err := dhcp4.Write(c.conn, request); err != nil {
return nil, err
} }
acknowledgement, err := c.GetAcknowledgement(&packet) c.conn.SetDeadline(time.Now().Add(10 * time.Second))
for {
// Look for DHCPACK packet (described in RFC2131 4.3.1):
ack, err := dhcp4.Read(c.conn)
if err != nil { if err != nil {
return false, acknowledgement, err return nil, err
} }
if ack == nil {
acknowledgementOptions := acknowledgement.ParseOptions() continue // not a DHCPv4 packet
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK { }
return false, acknowledgement, nil if ack.Xid != request.Xid {
continue // broadcast reply for different DHCP transaction
}
if !dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeAck) {
continue
}
return ack, nil
} }
return true, acknowledgement, nil
} }
func main() { func main() {
@ -122,6 +129,12 @@ func main() {
// NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be // NOTE: cannot gokrazy.WaitForClock() here, since the clock can only be
// initialized once the network is up. // initialized once the network is up.
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
hostname := string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
eth0, err := net.InterfaceByName("eth0") eth0, err := net.InterfaceByName("eth0")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -148,73 +161,67 @@ func main() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
hardwareAddr = eth0.HardwareAddr conn, err := raw.ListenPacket(eth0, syscall.ETH_P_IP, &raw.Config{
LinuxSockDGRAM: true,
pktsock, err := dhcp4client.NewPacketSock(eth0.Index) })
if err != nil {
log.Fatal(err)
}
dhcp, err := dhcp4client.New(
dhcp4client.HardwareAddr(eth0.HardwareAddr),
dhcp4client.Timeout(5*time.Second),
dhcp4client.Broadcast(false),
dhcp4client.Connection(pktsock),
)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
ok, ack, err := dhcpRequest(dhcp) c := &client{
hostname: hostname,
hardwareAddr: eth0.HardwareAddr,
generateXID: dhcp4.XIDGenerator(eth0.HardwareAddr),
conn: conn,
}
offer, err := c.discover()
if err != nil {
log.Fatal(err)
}
last := offer
for { for {
last, err = c.request(last)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !ok {
log.Fatal("received DHCPNAK")
}
opts := ack.ParseOptions()
// DHCPACK (described in RFC2131 4.3.1) lease := dhcp4.LeaseFromACK(last)
// - yiaddr: IP address assigned to client
// Log the received DHCPACK packet:
details := []string{ details := []string{
fmt.Sprintf("IP %v", ack.YIAddr()), fmt.Sprintf("IP %v", lease.IP),
} }
if len(lease.Netmask) > 0 {
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
ipnet := net.IPNet{ ipnet := net.IPNet{
IP: ack.YIAddr(), IP: lease.IP,
Mask: net.IPMask(b), Mask: lease.Netmask,
} }
details[0] = fmt.Sprintf("IP %v", ipnet.String()) details[0] = fmt.Sprintf("IP %v", ipnet.String())
} }
if len(lease.Router) > 0 {
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok { details = append(details, fmt.Sprintf("router %v", lease.Router))
details = append(details, fmt.Sprintf("broadcast %v", net.IP(b)))
} }
if len(lease.DNS) > 0 {
if b, ok := opts[dhcp4.OptionRouter]; ok { details = append(details, fmt.Sprintf("DNS %v", lease.DNS))
details = append(details, fmt.Sprintf("router %v", net.IP(b)))
} }
if len(lease.Broadcast) > 0 {
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok { details = append(details, fmt.Sprintf("broadcast %v", lease.Broadcast))
details = append(details, fmt.Sprintf("DNS %v", net.IP(b)))
} }
log.Printf("DHCPACK: %v", strings.Join(details, ", ")) log.Printf("DHCPACK: %v", strings.Join(details, ", "))
if err := cs.SetAddress(ack.YIAddr()); err != nil { // Apply the received settings:
if err := cs.SetAddress(lease.IP); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(lease.Netmask) > 0 {
if b, ok := opts[dhcp4.OptionSubnetMask]; ok { if err := cs.SetNetmask(lease.Netmask); err != nil {
if err := cs.SetNetmask(net.IPMask(b)); err != nil { log.Fatalf("setNetmask(%v): %v", lease.Netmask, err)
log.Fatalf("setNetmask(%v): %v", net.IPMask(b), err)
} }
} }
if b := lease.Broadcast; len(b) > 0 {
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok { if err := cs.SetBroadcast(b); err != nil {
if err := cs.SetBroadcast(net.IP(b)); err != nil { log.Fatalf("setBroadcast(%v): %v", b, err)
log.Fatalf("setBroadcast(%v): %v", net.IP(b), err)
} }
} }
@ -222,22 +229,22 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
if b, ok := opts[dhcp4.OptionRouter]; ok { if r := lease.Router; len(r) > 0 {
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 { if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
if errno == syscall.EEXIST { if errno == syscall.EEXIST {
if errno := cs.DelRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 { if errno := cs.DelRoute(defaultDst, r, defaultNetmask); errno != 0 {
log.Printf("delRoute(%v): %v", net.IP(b), errno) log.Printf("delRoute(%v): %v", r, errno)
} }
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 { if errno := cs.AddRoute(defaultDst, r, defaultNetmask); errno != 0 {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno) log.Fatalf("addRoute(%v): %v", r, errno)
} }
} else { } else {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno) log.Fatalf("addRoute(%v): %v", r, errno)
} }
} }
} }
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok { if len(lease.DNS) > 0 {
resolvConf := "/etc/resolv.conf" resolvConf := "/etc/resolv.conf"
if dest, err := os.Readlink("/etc/resolv.conf"); err == nil && dest == "/tmp/resolv.conf" { if dest, err := os.Readlink("/etc/resolv.conf"); err == nil && dest == "/tmp/resolv.conf" {
resolvConf = "/tmp/resolv.conf" resolvConf = "/tmp/resolv.conf"
@ -247,11 +254,13 @@ func main() {
log.Fatalf("resolv.conf: %v", err) log.Fatalf("resolv.conf: %v", err)
} }
var lines []string var lines []string
if domain, ok := opts[dhcp4.OptionDomainName]; ok { if domain := lease.Domain; domain != "" {
lines = append(lines, fmt.Sprintf("domain %s", string(domain))) lines = append(lines, fmt.Sprintf("domain %s", domain))
lines = append(lines, fmt.Sprintf("search %s", string(domain))) lines = append(lines, fmt.Sprintf("search %s", domain))
}
for _, ns := range lease.DNS {
lines = append(lines, fmt.Sprintf("nameserver %v", ns))
} }
lines = append(lines, fmt.Sprintf("nameserver %v", net.IP(b)))
if err := ioutil.WriteFile(resolvConf, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil { if err := ioutil.WriteFile(resolvConf, []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
log.Fatalf("resolv.conf: %v", err) log.Fatalf("resolv.conf: %v", err)
} }
@ -263,19 +272,6 @@ func main() {
log.Printf("send SIGHUP to init: %v", err) log.Printf("send SIGHUP to init: %v", err)
} }
leaseTime := 10 * time.Minute // seems sensible as a fallback time.Sleep(lease.RenewalTime)
if b, ok := opts[dhcp4.OptionIPAddressLeaseTime]; ok && len(b) == 4 {
leaseTime = parseDHCPDuration(b)
}
// As per RFC 2131 section 4.4.5:
// renewal time defaults to 50% of the lease time
renewalTime := time.Duration(float64(leaseTime) * 0.5)
if b, ok := opts[dhcp4.OptionRenewalTimeValue]; ok && len(b) == 4 {
renewalTime = parseDHCPDuration(b)
}
time.Sleep(renewalTime)
ok, ack, err = dhcpRenew(dhcp, dhcp.RenewalRequestPacket(&ack))
} }
} }