diff options
-rw-r--r-- | dhcpv4/nclient4/example_lease_test.go | 44 | ||||
-rw-r--r-- | dhcpv4/nclient4/lease.go | 22 | ||||
-rw-r--r-- | dhcpv4/nclient4/lease_test.go | 345 |
3 files changed, 362 insertions, 49 deletions
diff --git a/dhcpv4/nclient4/example_lease_test.go b/dhcpv4/nclient4/example_lease_test.go index 69095a8..3639c4e 100644 --- a/dhcpv4/nclient4/example_lease_test.go +++ b/dhcpv4/nclient4/example_lease_test.go @@ -1,60 +1,32 @@ -//this is an example for nclient4 with lease/release - package nclient4_test import ( "context" - "fmt" "log" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" - "github.com/vishvananda/netlink" ) -//applyLease adding the assigned ip to the interface specified by ifname -func applyLease(lease *nclient4.Lease, ifname string) error { - link, err := netlink.LinkByName(ifname) - if err != nil { - return err - } - prefixlen := 32 - if ipmask := lease.ACK.SubnetMask(); ipmask != nil { - prefixlen, _ = ipmask.Size() - - } - prefixstr := fmt.Sprintf("%v/%v", lease.ACK.YourIPAddr, prefixlen) - naddr, err := netlink.ParseAddr(prefixstr) - if err != nil { - return err - } - err = netlink.AddrReplace(link, naddr) - return err - -} - -func main() { - ifname := "eth1.200" - remoteid := "client-1" +func Example() { + ifname := "enp0s10" + clntid := "client-1" var idoptlist dhcpv4.OptionCodeList - //specify option82 is part of client identification used by DHCPv4 server - idoptlist.Add(dhcpv4.OptionRelayAgentInformation) + //specify option61 is part of client identification used by DHCPv4 server + idoptlist.Add(dhcpv4.OptionClientIdentifier) clntOptions := []nclient4.ClientOpt{nclient4.WithClientIDOptions(idoptlist), nclient4.WithDebugLogger()} clnt, err := nclient4.New(ifname, clntOptions...) if err != nil { log.Fatalf("failed to create dhcpv4 client,%v", err) } - //adding option82/remote-id option to discovery and request - remoteidsubopt := dhcpv4.OptGeneric(dhcpv4.AgentRemoteIDSubOption, []byte(remoteid)) - option82 := dhcpv4.OptRelayAgentInfo(remoteidsubopt) - _, lease, err := clnt.Request(context.Background(), dhcpv4.WithOption(option82)) + //add option61 to discovery and request + option61 := dhcpv4.OptClientIdentifier([]byte(clntid)) + _, lease, err := clnt.Request(context.Background(), dhcpv4.WithOption(option61)) if err != nil { log.Fatal(err) } //print the lease log.Printf("Got lease:\n%+v", lease) - //apply the lease - applyLease(lease, ifname) //release the lease log.Print("Releasing lease...") err = clnt.Release(lease) diff --git a/dhcpv4/nclient4/lease.go b/dhcpv4/nclient4/lease.go index 83c59e6..9f9d67d 100644 --- a/dhcpv4/nclient4/lease.go +++ b/dhcpv4/nclient4/lease.go @@ -29,7 +29,8 @@ func WithClientIDOptions(cidl dhcpv4.OptionCodeList) ClientOpt { //Release send DHCPv4 release messsage to server, based on specified lease. //release is sent as unicast per RFC2131, section 4.4.4. -//This function requires assigned address has been added on the binding interface. +//Note: some DHCP server requries of using assigned IP address as source IP, +//use nclient4.WithUnicast to create client for such case. func (c *Client) Release(lease *Lease) error { if lease == nil { return fmt.Errorf("lease is nil") @@ -45,21 +46,16 @@ func (c *Client) Release(lease *Lease) error { req.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeRelease)) req.ClientHWAddr = lease.ACK.ClientHWAddr req.ClientIPAddr = lease.ACK.YourIPAddr - req.UpdateOption(dhcpv4.OptServerIdentifier(lease.ACK.ServerIPAddr)) + req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionServerIdentifier, lease.ACK.Options.Get(dhcpv4.OptionServerIdentifier))) req.SetUnicast() - luaddr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%v:%v", lease.ACK.YourIPAddr, 68)) if err != nil { return err } - - uniconn, err := net.DialUDP("udp4", luaddr, &net.UDPAddr{IP: lease.ACK.ServerIPAddr, Port: 67}) - if err != nil { - return err - } - _, err = uniconn.Write(req.ToBytes()) - if err != nil { - return err + timeout := time.Now().Add(3 * time.Second) + c.conn.SetWriteDeadline(timeout) + _, err = c.conn.WriteTo(req.ToBytes(), &net.UDPAddr{IP: lease.ACK.Options.Get(dhcpv4.OptionServerIdentifier), Port: 67}) + if err == nil { + c.logger.PrintMessage("sent message:", req) } - c.logger.PrintMessage("sent message:", req) - return nil + return err } diff --git a/dhcpv4/nclient4/lease_test.go b/dhcpv4/nclient4/lease_test.go new file mode 100644 index 0000000..ef58d97 --- /dev/null +++ b/dhcpv4/nclient4/lease_test.go @@ -0,0 +1,345 @@ +// this tests nclient4 with lease and release + +package nclient4 + +import ( + "bytes" + "context" + "fmt" + "log" + "net" + "testing" + "time" + + "github.com/hugelgupf/socketpair" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/server4" +) + +type testLeaseKey struct { + mac net.HardwareAddr + idOptions dhcpv4.Options +} + +func (lk testLeaseKey) compare(b testLeaseKey) bool { + if !bytes.Equal(lk.idOptions.ToBytes(), b.idOptions.ToBytes()) { + return false + } + for i := 0; i < 6; i++ { + if lk.mac[i] != b.mac[i] { + return false + } + } + return true +} + +//this represents one test case +type testServerLease struct { + key *testLeaseKey + assignedAddr net.IP + ShouldFail bool //expected result +} + +func newtestServerLease(cip net.IP, m net.HardwareAddr, idops dhcpv4.Options) *testServerLease { + r := &testServerLease{} + r.assignedAddr = cip + r.key = &testLeaseKey{} + r.key.mac = m + r.key.idOptions = idops + return r +} + +type testServerLeaseList struct { + list []*testServerLease + clientIDOptions dhcpv4.OptionCodeList +} + +func newtestServerLeaseList(l dhcpv4.OptionCodeList) *testServerLeaseList { + r := &testServerLeaseList{} + r.clientIDOptions = l + return r +} + +func (sll testServerLeaseList) get(k *testLeaseKey) *testServerLease { + for i := range sll.list { + if sll.list[i].key.compare(*k) { + return sll.list[i] + } + } + return nil +} + +func (sll *testServerLeaseList) getKey(m *dhcpv4.DHCPv4) *testLeaseKey { + key := &testLeaseKey{} + key.mac = m.ClientHWAddr + key.idOptions = make(dhcpv4.Options) + for _, optioncode := range sll.clientIDOptions { + v := m.Options.Get(optioncode) + key.idOptions.Update(dhcpv4.OptGeneric(optioncode, v)) + } + return key + +} + +//use following setting to handle DORA +//server-id: 1.2.3.4 +//subnet-mask: /24 +//return address from sll.list +func (sll *testServerLeaseList) testLeaseDORAHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { + reply, err := dhcpv4.NewReplyFromRequest(m) + if err != nil { + return fmt.Errorf("NewReplyFromRequest failed: %v", err) + } + svrIP := net.ParseIP("1.2.3.4") + reply.UpdateOption(dhcpv4.OptServerIdentifier(svrIP.To4())) + reply.UpdateOption(dhcpv4.OptSubnetMask(net.IPv4Mask(255, 255, 255, 0))) + //build lease key + key := sll.getKey(m) + clease := sll.get(key) + if clease == nil { + return fmt.Errorf("unable to find the lease") + } + reply.YourIPAddr = clease.assignedAddr + switch mt := m.MessageType(); mt { + case dhcpv4.MessageTypeDiscover: + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) + case dhcpv4.MessageTypeRequest: + reply.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) + + default: + return fmt.Errorf("Unhandled message type: %v", mt) + } + + if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil { + return fmt.Errorf("Cannot reply to client: %v", err) + } + return nil +} + +//return check list for options must and may in the release msg according to RFC2131,section 4.4.1 +func (sll *testServerLeaseList) getCheckList() (mustHaveOpts, mayHaveOpts map[uint8]bool) { + mustHaveOpts = make(map[uint8]bool) + mayHaveOpts = make(map[uint8]bool) + mustHaveOpts[dhcpv4.OptionDHCPMessageType.Code()] = false + mustHaveOpts[dhcpv4.OptionServerIdentifier.Code()] = false + + for _, o := range sll.clientIDOptions { + mustHaveOpts[o.Code()] = false + } + mayHaveOpts[dhcpv4.OptionClassIdentifier.Code()] = false + mayHaveOpts[dhcpv4.OptionMessage.Code()] = false + return + +} + +//check request message according to RFC2131, section 4.4.1 +func (sll *testServerLeaseList) testLeaseReleaseHandle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) error { + + if m.HopCount != 0 { + return fmt.Errorf("hop count is %v, should be 0", m.HopCount) + } + if m.NumSeconds != 0 { + return fmt.Errorf("seconds is %v, should be 0", m.NumSeconds) + } + if m.Flags != 0 { + return fmt.Errorf("flags is %v, should be 0", m.Flags) + } + key := sll.getKey(m) + clease := sll.get(key) + if clease == nil { + return fmt.Errorf("can't find the lease") + } + if !m.ClientIPAddr.Equal(clease.assignedAddr) { + return fmt.Errorf("client IP is %v, expecting %v", m.ClientIPAddr, clease.assignedAddr) + } + if !m.YourIPAddr.Equal(net.ParseIP("0.0.0.0")) { + return fmt.Errorf("your IP is %v, expect 0", m.YourIPAddr) + } + if !m.GatewayIPAddr.Equal(net.ParseIP("0.0.0.0")) { + return fmt.Errorf("gateway IP is %v, expect 0", m.GatewayIPAddr) + } + mustlist, maylist := sll.getCheckList() + for o := range m.Options { + foundInMust := false + foundInMay := false + if _, ok := mustlist[o]; ok { + mustlist[o] = true + foundInMust = true + } + if _, ok := maylist[o]; ok { + foundInMay = true + } + if !foundInMay && !foundInMust { + return fmt.Errorf("option %v is not allowed in DHCP release msg", o) + } + } + for o, got := range mustlist { + if !got { + return fmt.Errorf("option %v is missing in DHCP release msg", o) + } + } + + if !net.IP(m.Options.Get(dhcpv4.OptionServerIdentifier)).Equal(net.ParseIP("1.2.3.4")) { + return fmt.Errorf("release misses servier id option=1.2.3.4") + } + return nil +} + +func (sll *testServerLeaseList) handle(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) { + if m == nil { + log.Fatal("Packet is nil!") + } + if m.OpCode != dhcpv4.OpcodeBootRequest { + log.Fatal("Not a BootRequest!") + } + var err error + switch m.MessageType() { + case dhcpv4.MessageTypeDiscover, dhcpv4.MessageTypeRequest: + err = sll.testLeaseDORAHandle(conn, peer, m) + if err != nil { + log.Printf("svr failed to handle DORA,%v", err) + } + + case dhcpv4.MessageTypeRelease: + err = sll.testLeaseReleaseHandle(conn, peer, m) + if err != nil { + log.Printf("svr failed to handle release,%v", err) + } + default: + log.Printf("svr got unexpeceted message type %v", m.MessageType()) + } +} + +func (sll *testServerLeaseList) runTest(t *testing.T) { + for _, l := range sll.list { + t.Logf("running lease test case for mac %v", l.key.mac) + // Fake PacketConn connection. + //note can't reuse conn between different clients, because there is currently + //no way to stop a client's reciev loop + clientRawConn, serverRawConn, err := socketpair.PacketSocketPair() + if err != nil { + panic(err) + } + clientConn := NewBroadcastUDPConn(clientRawConn, &net.UDPAddr{Port: ClientPort}) + serverConn := NewBroadcastUDPConn(serverRawConn, &net.UDPAddr{Port: ServerPort}) + s, err := server4.NewServer("", nil, sll.handle, server4.WithConn(serverConn)) + if err != nil { + t.Fatal(err) + } + go func() { + _ = s.Serve() + }() + clnt, err := testCreateClientWithServerLease(clientConn, l) + if err != nil { + t.Fatal(err) + } + modList := []dhcpv4.Modifier{} + for op, val := range l.key.idOptions { + modList = append(modList, dhcpv4.WithOption(dhcpv4.OptGeneric(dhcpv4.GenericOptionCode(op), val))) + } + chkerr := func(err error, shouldfail bool, t *testing.T) bool { + if err != nil { + if !shouldfail { + t.Fatal(err) + } else { + t.Logf("case failed as expected,%v", err) + return false + } + } + return true + } + + _, lease, err := clnt.Request(context.Background(), modList...) + keepgoing := chkerr(err, l.ShouldFail, t) + if keepgoing { + err = clnt.Release(lease) + chkerr(err, l.ShouldFail, t) + } + } + +} + +func testCreateClientWithServerLease(conn net.PacketConn, sl *testServerLease) (*Client, error) { + clntModList := []ClientOpt{WithRetry(1), WithTimeout(2 * time.Second)} + clntModList = append(clntModList, WithHWAddr(sl.key.mac)) + var idoptlist dhcpv4.OptionCodeList + for op := range sl.key.idOptions { + idoptlist.Add(dhcpv4.GenericOptionCode(op)) + } + clntModList = append(clntModList, WithClientIDOptions(idoptlist)) + clnt, err := NewWithConn(conn, sl.key.mac, clntModList...) + if err != nil { + return nil, fmt.Errorf("failed to create dhcpv4 client,%v", err) + } + return clnt, nil +} + +func TestLease(t *testing.T) { + //test data set 1 + var idoptlist dhcpv4.OptionCodeList + idoptlist.Add(dhcpv4.OptionClientIdentifier) + sll := newtestServerLeaseList(idoptlist) + sll.list = []*testServerLease{ + &testServerLease{ + assignedAddr: net.ParseIP("192.168.1.1"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 1}, + idOptions: dhcpv4.Options{ + uint8(dhcpv4.OptionClientIdentifier): []byte("client-1"), + }, + }, + }, + + &testServerLease{ + assignedAddr: net.ParseIP("192.168.1.2"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 2}, + idOptions: dhcpv4.Options{ + uint8(dhcpv4.OptionClientIdentifier): []byte("client-2"), + }, + }, + }, + //negative case + &testServerLease{ + assignedAddr: net.ParseIP("192.168.2.2"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 3}, + idOptions: dhcpv4.Options{}, + }, + ShouldFail: true, + }, + } + sll.runTest(t) + //test data set 2 + idoptlist = dhcpv4.OptionCodeList{} + sll = newtestServerLeaseList(idoptlist) + sll.list = []*testServerLease{ + &testServerLease{ + assignedAddr: net.ParseIP("192.168.2.1"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 2, 1}, + idOptions: dhcpv4.Options{}, + }, + }, + + &testServerLease{ + assignedAddr: net.ParseIP("192.168.2.2"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 2, 2}, + idOptions: dhcpv4.Options{}, + }, + }, + //negative case + &testServerLease{ + assignedAddr: net.ParseIP("192.168.2.2"), + key: &testLeaseKey{ + mac: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 1, 3}, + idOptions: dhcpv4.Options{ + uint8(dhcpv4.OptionClientIdentifier): []byte("client-fake"), + }, + }, + ShouldFail: true, + }, + } + sll.runTest(t) +} |