summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4/nclient4
diff options
context:
space:
mode:
authorHu Jun <hujun.work@gmail.com>2020-06-17 21:06:01 -0700
committerHu Jun <hujun.work@gmail.com>2020-06-17 21:33:41 -0700
commitcaf50d65b1cb4e946441e1fc0ac178b8fa19b751 (patch)
treeab92f17e430ef67af2c996050b646f36b71cb366 /dhcpv4/nclient4
parent4b5a011e0a4c724e2afc8aaeb55edf2f2dbecebe (diff)
add lease&release support for nclient4
Signed-off-by: Hu Jun <hujun.work@gmail.com>
Diffstat (limited to 'dhcpv4/nclient4')
-rw-r--r--dhcpv4/nclient4/client.go29
-rw-r--r--dhcpv4/nclient4/lease.go270
2 files changed, 297 insertions, 2 deletions
diff --git a/dhcpv4/nclient4/client.go b/dhcpv4/nclient4/client.go
index a1596a5..d326ddb 100644
--- a/dhcpv4/nclient4/client.go
+++ b/dhcpv4/nclient4/client.go
@@ -162,6 +162,18 @@ type Client struct {
// TransactionID. receiveLoop uses this map to determine which channel
// to send a new DHCP message to.
pending map[dhcpv4.TransactionID]*pendingCh
+
+ //clientIdOptions is a list of DHCPv4 option code that DHCP server used to
+ //identify client other than the HWAddress,
+ //like client-id, option82/remote-id..etc
+ clientIdOptions dhcpv4.OptionCodeList
+
+ //lease info after DORA, nil before DORA
+ lease *DHCPv4ClientLease
+ //the handnler function for applying lease
+ leaseApplyHandler func(DHCPv4ClientLease, bool) error
+ //binding interface name
+ ifName string
}
// New returns a client usable with an unconfigured interface.
@@ -185,8 +197,12 @@ func new(iface string, conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts .
conn: conn,
logger: EmptyLogger{},
- done: make(chan struct{}),
- pending: make(map[dhcpv4.TransactionID]*pendingCh),
+ done: make(chan struct{}),
+ pending: make(map[dhcpv4.TransactionID]*pendingCh),
+ lease: nil,
+ leaseApplyHandler: defaultLeaseApplyHandler,
+ clientIdOptions: dhcpv4.OptionCodeList{},
+ ifName: iface,
}
for _, opt := range opts {
@@ -521,6 +537,15 @@ var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded")
// ClientHWAddr is returned.
func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcpv4.DHCPv4, match Matcher) (*dhcpv4.DHCPv4, error) {
var response *dhcpv4.DHCPv4
+ //check if the request packet has all options required by c.clientIdOptions
+ for _, optioncode := range c.clientIdOptions {
+ if len(p.Options.Get(optioncode)) == 0 {
+ err := fmt.Errorf("Option %v required for client identification is missing in request", optioncode)
+ return nil, err
+ }
+
+ }
+
err := c.retryFn(func(timeout time.Duration) error {
ch, rem, err := c.send(dest, p)
if err != nil {
diff --git a/dhcpv4/nclient4/lease.go b/dhcpv4/nclient4/lease.go
new file mode 100644
index 0000000..b5cb6b0
--- /dev/null
+++ b/dhcpv4/nclient4/lease.go
@@ -0,0 +1,270 @@
+/*
+An example, bind to eth0, using option82/remote-id for client identification:
+
+
+ package main
+
+ import (
+ "context"
+ "log"
+
+ "github.com/insomniacslk/dhcp/dhcpv4"
+ "github.com/insomniacslk/dhcp/dhcpv4/nclient4"
+ )
+
+ func main() {
+ ifname := "eth0"
+ remote_id := "client-1"
+ var idoptlist dhcpv4.OptionCodeList
+ //specify option82 is part of client identification used by DHCPv4 server
+ idoptlist.Add(dhcpv4.OptionRelayAgentInformation)
+ clnt_options := []nclient4.ClientOpt{nclient4.WithClientIdOptions(idoptlist), nclient4.WithDebugLogger()}
+ clnt, err := nclient4.New(ifname, clnt_options...)
+ if err != nil {
+ log.Fatalf("failed to create dhcpv4 client,%v", err)
+ }
+ //adding option82/remote-id option to discovery and request
+ remote_id_sub_opt := dhcpv4.OptGeneric(dhcpv4.AgentRemoteIDSubOption, []byte(remote_id))
+ option82 := dhcpv4.OptRelayAgentInfo(remote_id_sub_opt)
+ _, _, err = clnt.RequestSavingLease(context.Background(), dhcpv4.WithOption(option82))
+ if err != nil {
+ log.Fatal(err)
+ }
+ //print the lease
+ log.Printf("Got lease:\n%v", clnt.GetLease())
+ //release the lease
+ log.Print("Releasing lease...")
+ err = clnt.Release()
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Print("done")
+ }
+
+
+*/
+package nclient4
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "time"
+
+ "github.com/insomniacslk/dhcp/dhcpv4"
+ "github.com/vishvananda/netlink"
+)
+
+const (
+ //default lease time if server doesn't return lease time option or return zero
+ defaultLeaseTime = time.Hour
+)
+
+//DHCPv4ClientLease contains a DHCPv4 lease after DORA,
+//could be used for creating a new Client with NewWithLease()
+type DHCPv4ClientLease struct {
+ IfName string
+ MACAddr net.HardwareAddr
+ ServerAddr net.UDPAddr
+ AssignedIP net.IP
+ AssignedIPMask net.IPMask
+ CreationTime time.Time
+ LeaseDuration time.Duration
+ RenewInterval time.Duration
+ RebindInterval time.Duration
+ IdOptions dhcpv4.Options //DHCPv4 options to identify the client like client-id, option82/remote-id
+ AckOptions dhcpv4.Options //DHCPv4 options in ACK, could be used for applying lease
+
+}
+
+//return a string representation
+func (lease DHCPv4ClientLease) String() string {
+ const FMTSTR = "%-35s\t%-35s\n"
+ const TIME_FMT = "01/02/2006 15:04:05.000"
+ rstr := fmt.Sprintf(FMTSTR, fmt.Sprintf("Interface:%v", lease.IfName), fmt.Sprintf("MAC:%v", lease.MACAddr))
+ rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("Svr:%v", lease.ServerAddr.IP), fmt.Sprintf("Created:%v", lease.CreationTime.Format(TIME_FMT)))
+ prefix_len, _ := lease.AssignedIPMask.Size()
+ rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("IP:%v/%v", lease.AssignedIP, prefix_len), fmt.Sprintf("Lease time:%v", lease.LeaseDuration))
+ rstr += fmt.Sprintf(FMTSTR, fmt.Sprintf("Renew interval:%v", lease.RenewInterval), fmt.Sprintf("Rebind interval:%v", lease.RebindInterval))
+ rstr += fmt.Sprintf("Id options:\n%v", lease.IdOptions)
+ rstr += fmt.Sprintf("ACK options:\n%v", lease.AckOptions)
+ return rstr
+}
+
+// WithClientIdOptions configures a list of DHCPv4 option code that DHCP server
+// uses to identify client, beside the MAC address.
+func WithClientIdOptions(cidl dhcpv4.OptionCodeList) ClientOpt {
+ return func(c *Client) (err error) {
+ c.clientIdOptions = cidl
+ return
+ }
+}
+
+// WithApplyLeaseHandler specifies a handler function which is called when
+// Client.ApplyLease() is called; without this, a default handler function is called.
+// the default handler will add/remove the assigned address to/from the binding interface;
+// bool parameter is true when lease is applied, false when lease is released
+func WithApplyLeaseHandler(h func(DHCPv4ClientLease, bool) error) ClientOpt {
+ return func(c *Client) (err error) {
+ c.leaseApplyHandler = h
+ return
+ }
+}
+
+//default lease apply handler
+//add/remove address to/from binding interface
+func defaultLeaseApplyHandler(l DHCPv4ClientLease, enable bool) error {
+ link, err := netlink.LinkByName(l.IfName)
+ if err != nil {
+ return err
+ }
+ plen, _ := l.AssignedIPMask.Size()
+ prefix_str := fmt.Sprintf("%v/%v", l.AssignedIP, plen)
+ naddr, err := netlink.ParseAddr(prefix_str)
+ if err != nil {
+ return err
+ }
+ if enable {
+ err = netlink.AddrReplace(link, naddr)
+
+ } else {
+ err = netlink.AddrDel(link, naddr)
+ }
+ return err
+
+}
+
+//Apply the lease, calling the c.leaseApplyHandler
+func (c *Client) ApplyLease(enable bool) error {
+ if c.lease == nil {
+ return fmt.Errorf("no lease to apply")
+ }
+ return c.leaseApplyHandler(c.GetLease(), enable)
+}
+
+//return the lease
+func (c *Client) GetLease() (clease DHCPv4ClientLease) {
+ clease = *c.lease
+ clease.MACAddr = c.ifaceHWAddr
+ clease.IfName = c.ifName
+ clease.ServerAddr = *c.serverAddr
+ return
+}
+
+// RequestRequestSavingLease completes DORA handshake and store&apply the lease
+//
+// Note that modifiers will be applied *both* to Discover and Request packets.
+func (c *Client) RequestSavingLease(ctx context.Context, modifiers ...dhcpv4.Modifier) (offer, ack *dhcpv4.DHCPv4, err error) {
+ offer, err = c.DiscoverOffer(ctx, modifiers...)
+ if err != nil {
+ err = fmt.Errorf("unable to receive an offer: %w", err)
+ return
+ }
+
+ // TODO(chrisko): should this be unicast to the server?
+ request, err := dhcpv4.NewRequestFromOffer(offer, dhcpv4.PrependModifiers(modifiers,
+ dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)))...)
+ if err != nil {
+ err = fmt.Errorf("unable to create a request: %w", err)
+ return
+ }
+
+ ack, err = c.SendAndRead(ctx, c.serverAddr, request, nil)
+ if err != nil {
+ err = fmt.Errorf("got an error while processing the request: %w", err)
+ return
+ }
+ //save lease
+ c.lease = &DHCPv4ClientLease{}
+ c.lease.AssignedIP = ack.YourIPAddr
+ c.lease.AssignedIPMask = ack.SubnetMask()
+ c.lease.CreationTime = time.Now()
+ c.lease.LeaseDuration = ack.IPAddressLeaseTime(0)
+ if c.lease.LeaseDuration == 0 {
+ c.lease.LeaseDuration = defaultLeaseTime
+ c.logger.Printf("warning: server doesn't include Lease Time option or it is zero seconds, setting lease time to default %v", c.lease.LeaseDuration)
+
+ }
+ c.lease.RenewInterval = ack.IPAddressRenewalTime(0)
+ if c.lease.RenewInterval == 0 {
+ //setting default to half of lease time based on RFC2131,section 4.4.5
+ c.lease.RenewInterval = time.Duration(float64(c.lease.LeaseDuration) / 2)
+ c.logger.Printf("warning: server doesn't include Renew Time option or it is zero seconds, setting lease time to default %v", c.lease.RenewInterval)
+
+ }
+ c.lease.RebindInterval = ack.IPAddressRebindingTime(0)
+ if c.lease.RebindInterval == 0 {
+ //setting default to 0.875 of lease time based on RFC2131,section 4.4.5
+ c.lease.RebindInterval = time.Duration(float64(c.lease.LeaseDuration) * 0.875)
+ c.logger.Printf("warning: server doesn't include Renew Time option or it is zero seconds, setting lease time to default %v", c.lease.RebindInterval)
+
+ }
+ c.lease.IdOptions = dhcpv4.Options{}
+ for _, optioncode := range c.clientIdOptions {
+ v := request.Options.Get(optioncode)
+ c.lease.IdOptions.Update(dhcpv4.OptGeneric(optioncode, v))
+ }
+ c.lease.AckOptions = ack.Options
+ //update server address
+ c.serverAddr = &(net.UDPAddr{IP: ack.ServerIdentifier(), Port: 67})
+ c.ApplyLease(true)
+ return
+}
+
+//Release send DHCPv4 release messsage to server.
+//release is sent as unicast per RFC2131, section 4.4.4.
+//The lease need to be applied with c.ApplyLease(true) first before calling Release.
+func (c *Client) Release() error {
+ if c.lease == nil {
+ return fmt.Errorf("There is no lease to release")
+ }
+ req, err := dhcpv4.New()
+ if err != nil {
+ return err
+ }
+ //This is to make sure use same client identification options used during
+ //DORA, so that DHCP server could identify the required lease
+ req.Options = c.lease.IdOptions
+
+ req.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeRelease))
+ req.ClientHWAddr = c.ifaceHWAddr
+ req.ClientIPAddr = c.lease.AssignedIP
+ req.UpdateOption(dhcpv4.OptServerIdentifier(c.serverAddr.IP))
+ req.SetUnicast()
+ luaddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%v:%v", c.lease.AssignedIP, 68))
+ if err != nil {
+ return err
+ }
+
+ uniconn, err := net.DialUDP("udp4", luaddr, c.serverAddr)
+ if err != nil {
+ return err
+ }
+ _, err = uniconn.Write(req.ToBytes())
+ if err != nil {
+ return err
+ }
+ c.logger.PrintMessage("sent message:", req)
+ return c.ApplyLease(false)
+}
+
+//NewWithLease return a Client with populated lease.
+//this function could be used to release a saved lease.
+func NewWithLease(clease DHCPv4ClientLease, opts ...ClientOpt) (*Client, error) {
+ clnt_opt_list := []ClientOpt{
+ WithServerAddr(&clease.ServerAddr),
+ WithHWAddr(clease.MACAddr),
+ }
+ clnt_opt_list = append(clnt_opt_list, opts...)
+ clnt, err := New(clease.IfName, clnt_opt_list...)
+ if err != nil {
+ return nil, err
+ }
+ clnt.ifName = clease.IfName
+ clnt.lease = &clease
+ for optioncode, _ := range clease.IdOptions {
+ clnt.clientIdOptions.Add(dhcpv4.GenericOptionCode(optioncode))
+ }
+ return clnt, nil
+
+}