270 lines
7.0 KiB
Go
270 lines
7.0 KiB
Go
// Copyright 2018 Google Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package dhcp4 implements a DHCPv4 client.
|
|
package dhcp4
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/google/gopacket/layers"
|
|
"github.com/mdlayher/packet"
|
|
"github.com/rtr7/dhcp4"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type Config struct {
|
|
RenewAfter time.Time `json:"valid_until"`
|
|
ClientIP string `json:"client_ip"` // e.g. 85.195.207.62
|
|
SubnetMask string `json:"subnet_mask"` // e.g. 255.255.255.128
|
|
Router string `json:"router"` // e.g. 85.195.207.1
|
|
DNS []string `json:"dns"` // e.g. 77.109.128.2, 213.144.129.20
|
|
}
|
|
|
|
type Client struct {
|
|
Interface *net.Interface // e.g. net.InterfaceByName("eth0")
|
|
HWAddr net.HardwareAddr
|
|
|
|
err error
|
|
once sync.Once
|
|
onceErr error
|
|
connection net.PacketConn
|
|
hardwareAddr net.HardwareAddr
|
|
hostname string
|
|
cfg Config
|
|
timeNow func() time.Time
|
|
generateXID func() uint32
|
|
|
|
timeoutCount int
|
|
|
|
// last DHCPACK packet for renewal/release
|
|
Ack *layers.DHCPv4
|
|
}
|
|
|
|
func serverID(pkt *layers.DHCPv4) []layers.DHCPOption {
|
|
for _, o := range pkt.Options {
|
|
if o.Type == layers.DHCPOptServerID {
|
|
return []layers.DHCPOption{o}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) packet(xid uint32, opts []layers.DHCPOption) *layers.DHCPv4 {
|
|
return &layers.DHCPv4{
|
|
Operation: layers.DHCPOpRequest,
|
|
HardwareType: layers.LinkTypeEthernet,
|
|
HardwareLen: uint8(len(layers.EthernetBroadcast)),
|
|
HardwareOpts: 0, // clients set this to zero (used by relay agents)
|
|
Xid: xid,
|
|
Secs: 0, // TODO: fill in?
|
|
Flags: 0, // we can receive IP packets via unicast
|
|
ClientHWAddr: c.hardwareAddr,
|
|
ServerName: nil,
|
|
File: nil,
|
|
Options: opts,
|
|
}
|
|
}
|
|
|
|
var errNAK = errors.New("received DHCPNAK")
|
|
|
|
// ObtainOrRenew returns false when encountering a permanent error.
|
|
func (c *Client) ObtainOrRenew() bool {
|
|
c.once.Do(func() {
|
|
if c.timeNow == nil {
|
|
c.timeNow = time.Now
|
|
}
|
|
if c.connection == nil && c.Interface != nil {
|
|
conn, err := packet.Listen(c.Interface, packet.Datagram, syscall.ETH_P_IP, nil)
|
|
if err != nil {
|
|
c.onceErr = err
|
|
return
|
|
}
|
|
c.connection = conn
|
|
}
|
|
if c.connection == nil && c.Interface == nil {
|
|
c.onceErr = fmt.Errorf("c.Interface is nil")
|
|
return
|
|
}
|
|
if c.hardwareAddr == nil && c.HWAddr != nil {
|
|
c.hardwareAddr = c.HWAddr
|
|
}
|
|
if c.hardwareAddr == nil {
|
|
c.hardwareAddr = c.Interface.HardwareAddr
|
|
}
|
|
if c.generateXID == nil {
|
|
c.generateXID = dhcp4.XIDGenerator(c.hardwareAddr)
|
|
}
|
|
if c.hostname == "" {
|
|
var utsname unix.Utsname
|
|
if err := unix.Uname(&utsname); err != nil {
|
|
c.onceErr = err
|
|
return
|
|
}
|
|
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
|
|
}
|
|
})
|
|
if c.onceErr != nil {
|
|
c.err = c.onceErr
|
|
return false // permanent error
|
|
}
|
|
c.err = nil // clear previous error
|
|
ack, err := c.dhcpRequest()
|
|
if err != nil {
|
|
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
|
|
var serverip net.IP
|
|
for _, opt := range c.Ack.Options {
|
|
if opt.Type == layers.DHCPOptServerID {
|
|
serverip = opt.Data
|
|
}
|
|
}
|
|
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable: %v)", serverip)
|
|
c.timeoutCount++
|
|
if c.timeoutCount > 3 {
|
|
c.timeoutCount = 0
|
|
c.Ack = nil // start over at DHCPDISCOVER it has failed 3 times
|
|
}
|
|
return true // temporary error
|
|
}
|
|
if err == errNAK {
|
|
c.Ack = nil // start over at DHCPDISCOVER
|
|
}
|
|
c.err = fmt.Errorf("DHCP: %v", err)
|
|
return true // temporary error
|
|
}
|
|
c.Ack = ack
|
|
c.cfg.ClientIP = ack.YourClientIP.String()
|
|
lease := dhcp4.LeaseFromACK(ack)
|
|
if mask := lease.Netmask; len(mask) > 0 {
|
|
c.cfg.SubnetMask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
|
|
}
|
|
if len(lease.Router) > 0 {
|
|
c.cfg.Router = lease.Router.String()
|
|
}
|
|
if len(lease.DNS) > 0 {
|
|
c.cfg.DNS = make([]string, len(lease.DNS))
|
|
for idx, ip := range lease.DNS {
|
|
c.cfg.DNS[idx] = ip.String()
|
|
}
|
|
}
|
|
c.cfg.RenewAfter = c.timeNow().Add(lease.RenewalTime)
|
|
c.timeoutCount = 0
|
|
return true
|
|
}
|
|
|
|
func (c *Client) Release() error {
|
|
release := c.packet(c.generateXID(), append([]layers.DHCPOption{
|
|
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRelease),
|
|
}, serverID(c.Ack)...))
|
|
release.ClientIP = c.Ack.YourClientIP
|
|
if err := dhcp4.Write(c.connection, release); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.Ack = nil
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) Err() error {
|
|
return c.err
|
|
}
|
|
|
|
func (c *Client) Config() Config {
|
|
return c.cfg
|
|
}
|
|
|
|
func (c *Client) dhcpRequest() (*layers.DHCPv4, error) {
|
|
var last *layers.DHCPv4
|
|
|
|
if c.Ack != nil {
|
|
last = c.Ack
|
|
} else {
|
|
discover := c.packet(c.generateXID(), []layers.DHCPOption{
|
|
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeDiscover),
|
|
dhcp4.HostnameOpt(c.hostname),
|
|
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
|
|
dhcp4.ParamsRequestOpt(
|
|
layers.DHCPOptDNS,
|
|
layers.DHCPOptRouter,
|
|
layers.DHCPOptSubnetMask),
|
|
})
|
|
if err := dhcp4.Write(c.connection, discover); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Look for DHCPOFFER packet (described in RFC2131 4.3.1):
|
|
c.connection.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
for {
|
|
offer, err := dhcp4.Read(c.connection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if offer == nil {
|
|
continue // not a DHCPv4 packet
|
|
}
|
|
if offer.Xid != discover.Xid {
|
|
continue // broadcast reply for different DHCP transaction
|
|
}
|
|
if !dhcp4.HasMessageType(offer.Options, layers.DHCPMsgTypeOffer) {
|
|
continue
|
|
}
|
|
last = offer
|
|
break
|
|
}
|
|
}
|
|
|
|
// Build a DHCPREQUEST packet:
|
|
request := c.packet(last.Xid, append([]layers.DHCPOption{
|
|
dhcp4.MessageTypeOpt(layers.DHCPMsgTypeRequest),
|
|
dhcp4.RequestIPOpt(last.YourClientIP),
|
|
dhcp4.HostnameOpt(c.hostname),
|
|
dhcp4.ClientIDOpt(layers.LinkTypeEthernet, c.hardwareAddr),
|
|
dhcp4.ParamsRequestOpt(
|
|
layers.DHCPOptDNS,
|
|
layers.DHCPOptRouter,
|
|
layers.DHCPOptSubnetMask),
|
|
}, serverID(last)...))
|
|
if err := dhcp4.Write(c.connection, request); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.connection.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
for {
|
|
// Look for DHCPACK packet (described in RFC2131 4.3.1):
|
|
ack, err := dhcp4.Read(c.connection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ack == nil {
|
|
continue // not a DHCPv4 packet
|
|
}
|
|
if ack.Xid != request.Xid {
|
|
continue // broadcast reply for different DHCP transaction
|
|
}
|
|
if !dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeAck) {
|
|
if dhcp4.HasMessageType(ack.Options, layers.DHCPMsgTypeNak) {
|
|
return nil, errNAK
|
|
}
|
|
continue
|
|
}
|
|
return ack, nil
|
|
}
|
|
}
|