gokrazy/cmd/dhcp/dhcp.go
Michael Stapelberg 6127f9566e dhcp: set client id (option 61), some routers need it
E.g. /etc/kresd/dhcp_host_domain_ng.sh on a turris omnia incorrectly deletes
DHCP hostname mappings when more than one client does not send a client id.
2017-09-05 09:52:53 +02:00

275 lines
6.8 KiB
Go

// Only build for (linux AND (amd64 OR arm64)) due to using
// linux-specific syscalls with uint64 for “unsigned long”:
// +build linux
// +build amd64 arm64
// dhcp is a minimal DHCP client for gokrazy.
package main
import (
"encoding/binary"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
"github.com/d2g/dhcp4"
"github.com/d2g/dhcp4client"
"github.com/gokrazy/gokrazy/internal/iface"
)
func parseDHCPDuration(b []byte) time.Duration {
return time.Duration(binary.BigEndian.Uint32(b)) * time.Second
}
var (
defaultDst = net.IP([]byte{0, 0, 0, 0})
defaultNetmask = net.IPMask([]byte{0, 0, 0, 0})
hardwareAddr net.HardwareAddr
)
func addHostname(p *dhcp4.Packet) {
var utsname unix.Utsname
if err := unix.Uname(&utsname); err != nil {
log.Fatal(err)
}
nnb := make([]byte, 0, len(utsname.Nodename))
for _, i := range utsname.Nodename {
if i == 0 {
break
}
nnb = append(nnb, byte(i))
}
p.AddOption(dhcp4.OptionHostName, nnb)
}
func addClientId(p *dhcp4.Packet) {
id := make([]byte, len(hardwareAddr)+1)
id[0] = 1 // hardware type ethernet, https://tools.ietf.org/html/rfc1700
copy(id[1:], hardwareAddr)
p.AddOption(dhcp4.OptionClientIdentifier, id)
}
// dhcpRequest is a copy of (dhcp4client/Client).Request which
// includes the hostname.
func dhcpRequest(c *dhcp4client.Client) (bool, dhcp4.Packet, error) {
discoveryPacket := c.DiscoverPacket()
addHostname(&discoveryPacket)
addClientId(&discoveryPacket)
discoveryPacket.PadToMinSize()
if err := c.SendPacket(discoveryPacket); err != nil {
return false, discoveryPacket, err
}
offerPacket, err := c.GetOffer(&discoveryPacket)
if err != nil {
return false, offerPacket, err
}
requestPacket := c.RequestPacket(&offerPacket)
addHostname(&requestPacket)
addClientId(&requestPacket)
requestPacket.PadToMinSize()
if err := c.SendPacket(requestPacket); err != nil {
return false, requestPacket, err
}
acknowledgement, err := c.GetAcknowledgement(&requestPacket)
if err != nil {
return false, acknowledgement, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
}
return true, acknowledgement, nil
}
// dhcpRenew is a copy of (dhcp4client/Client).Renew which
// includes the hostname.
func dhcpRenew(c *dhcp4client.Client, packet dhcp4.Packet) (bool, dhcp4.Packet, error) {
addHostname(&packet)
addClientId(&packet)
packet.PadToMinSize()
if err := c.SendPacket(packet); err != nil {
return false, packet, err
}
acknowledgement, err := c.GetAcknowledgement(&packet)
if err != nil {
return false, acknowledgement, err
}
acknowledgementOptions := acknowledgement.ParseOptions()
if dhcp4.MessageType(acknowledgementOptions[dhcp4.OptionDHCPMessageType][0]) != dhcp4.ACK {
return false, acknowledgement, nil
}
return true, acknowledgement, nil
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
eth0, err := net.InterfaceByName("eth0")
if err != nil {
log.Fatal(err)
}
cs, err := iface.NewConfigSocket("eth0")
if err != nil {
log.Fatalf("config socket: %v", err)
}
defer cs.Close()
// Ensure the interface is up so that we can send DHCP packets.
if err := cs.Up(); err != nil {
log.Fatal(err)
}
// Wait for up to 10 seconds for the link to indicate it has a
// carrier.
for i := 0; i < 10; i++ {
b, err := ioutil.ReadFile("/sys/class/net/eth0/carrier")
if err == nil && strings.TrimSpace(string(b)) == "1" {
break
}
time.Sleep(1 * time.Second)
}
hardwareAddr = eth0.HardwareAddr
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 {
log.Fatal(err)
}
ok, ack, err := dhcpRequest(dhcp)
for {
if err != nil {
log.Fatal(err)
}
if !ok {
log.Fatal("received DHCPNAK")
}
opts := ack.ParseOptions()
// DHCPACK (described in RFC2131 4.3.1)
// - yiaddr: IP address assigned to client
details := []string{
fmt.Sprintf("IP %v", ack.YIAddr()),
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
ipnet := net.IPNet{
IP: ack.YIAddr(),
Mask: net.IPMask(b),
}
details[0] = fmt.Sprintf("IP %v", ipnet.String())
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
details = append(details, fmt.Sprintf("broadcast %v", net.IP(b)))
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
details = append(details, fmt.Sprintf("router %v", net.IP(b)))
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
details = append(details, fmt.Sprintf("DNS %v", net.IP(b)))
}
log.Printf("DHCPACK: %v", strings.Join(details, ", "))
if err := cs.SetAddress(ack.YIAddr()); err != nil {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionSubnetMask]; ok {
if err := cs.SetNetmask(net.IPMask(b)); err != nil {
log.Fatalf("setNetmask(%v): %v", net.IPMask(b), err)
}
}
if b, ok := opts[dhcp4.OptionBroadcastAddress]; ok {
if err := cs.SetBroadcast(net.IP(b)); err != nil {
log.Fatalf("setBroadcast(%v): %v", net.IP(b), err)
}
}
if err := cs.Up(); err != nil {
log.Fatal(err)
}
if b, ok := opts[dhcp4.OptionRouter]; ok {
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
if errno == syscall.EEXIST {
if errno := cs.DelRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Printf("delRoute(%v): %v", net.IP(b), errno)
}
if errno := cs.AddRoute(defaultDst, net.IP(b), defaultNetmask); errno != 0 {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
}
} else {
log.Fatalf("addRoute(%v): %v", net.IP(b), errno)
}
}
}
if b, ok := opts[dhcp4.OptionDomainNameServer]; ok {
// Get the symlink out of the way, if any.
if err := os.Remove("/etc/resolv.conf"); err != nil && !os.IsNotExist(err) {
log.Fatalf("resolv.conf: %v", err)
}
if err := ioutil.WriteFile("/etc/resolv.conf", []byte(fmt.Sprintf("nameserver %v\n", net.IP(b))), 0644); err != nil {
log.Fatalf("resolv.conf: %v", err)
}
}
// Notify init of new addresses
p, _ := os.FindProcess(1)
if err := p.Signal(syscall.SIGHUP); err != nil {
log.Printf("send SIGHUP to init: %v", err)
}
leaseTime := 10 * time.Minute // seems sensible as a fallback
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))
}
}