diff options
Diffstat (limited to 'dhcpv4/nclient4')
-rw-r--r-- | dhcpv4/nclient4/client.go | 29 | ||||
-rw-r--r-- | dhcpv4/nclient4/lease.go | 270 |
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 + +} |