From c412f00cb893fc1c9727761448ea351ec8d5c9f2 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Sun, 15 May 2016 17:22:01 -0700 Subject: [PATCH] dhcp4: add consts for common options, and option getters. --- dhcp4/options.go | 166 +++++++++++++++++++++++++++++++++++++----- dhcp4/options_test.go | 16 ++-- dhcp4/packet.go | 23 +++--- 3 files changed, 168 insertions(+), 37 deletions(-) diff --git a/dhcp4/options.go b/dhcp4/options.go index de639f9..1f644fd 100644 --- a/dhcp4/options.go +++ b/dhcp4/options.go @@ -20,16 +20,61 @@ import ( "errors" "fmt" "io" + "net" "sort" ) +// Option is a DHCP option. +type Option byte + +// Some of the more commonly seen DHCP options. Refer to +// http://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml +// for the full authoritative list. +const ( + OptSubnetMask Option = 1 // IPMask + OptTimeOffset = 2 // int32 + OptRouters = 3 // IPs + OptDNSServers = 6 // IPs + OptHostname = 12 // string + OptBootFileSize = 13 // uint16 + OptDomainName = 15 // string + OptBroadcastAddr = 28 // IP + OptNTPServers = 42 // IP + OptVendorSpecific = 43 // []byte + OptRequestedIP = 50 // IP + OptLeaseTime = 51 // uint32 + OptOverload = 52 // byte + OptServerIdentifier = 54 // IP + OptRequestedOptions = 55 // []byte + OptMessage = 56 // string + OptMaximumMessageSize = 57 // uint16 + OptRenewalTime = 58 // uint32 + OptRebindingTime = 59 // uint32 + OptVendorIdentifier = 60 // string + OptClientIdentifier = 61 // string + OptFQDN = 81 // string + + // You shouldn't need to use the following directly. Instead, + // refer to the fields in the Packet struct, and Marshal/Unmarshal + // will handle encoding for you. + + OptTFTPServer = 66 // string + OptBootFile = 67 // string + OptDHCPMessageType = 53 // byte +) + // Options stores DHCP options. -type Options map[int][]byte +type Options map[Option][]byte + +var ( + errOptionNotPresent = errors.New("option not present in Options") + errOptionWrongSize = errors.New("option value is the wrong size") +) // Unmarshal parses DHCP options into o. func (o Options) Unmarshal(bs []byte) error { for len(bs) > 0 { - opt := int(bs[0]) + opt := Option(bs[0]) switch opt { case 0: // Padding byte @@ -97,13 +142,13 @@ func (o Options) marshalLimited(w io.Writer, nBytes int, skip52 bool) (Options, if n <= 0 || n >= 255 { return nil, fmt.Errorf("invalid DHCP option number %d", n) } - ks = append(ks, n) + ks = append(ks, int(n)) } sort.Ints(ks) ret := make(Options) for _, n := range ks { - opt := o[n] + opt := o[Option(n)] if len(opt) > 255 { return nil, fmt.Errorf("DHCP option %d has value >255 bytes", n) } @@ -111,7 +156,7 @@ func (o Options) marshalLimited(w io.Writer, nBytes int, skip52 bool) (Options, // If space is limited, verify that we can fit the option plus // the final end-of-options marker. if nBytes > 0 && ((skip52 && n == 52) || len(opt)+3 > nBytes) { - ret[n] = opt + ret[Option(n)] = opt continue } @@ -129,21 +174,108 @@ func (o Options) marshalLimited(w io.Writer, nBytes int, skip52 bool) (Options, return ret, nil } -// Byte returns the value of single-byte option n, if the option value -// is indeed a single byte. -func (o Options) Byte(n int) (v byte, ok bool) { +// String returns the value of option n as a byte slice. +func (o Options) Bytes(n Option) ([]byte, error) { bs := o[n] - if bs == nil || len(bs) != 1 { - return 0, false + if bs == nil { + return nil, errOptionNotPresent } - return bs[0], true + return bs, nil } -// Uint16 returns the value of option n interpreted as a uint16. -func (o Options) Uint16(n int) (v uint16, ok bool) { - bs := o[n] - if bs == nil || len(bs) != 2 { - return 0, false +// String returns the value of option n as a string. +func (o Options) String(n Option) (string, error) { + bs, err := o.Bytes(n) + if err != nil { + return "", err } - return binary.BigEndian.Uint16(bs[:2]), true + return string(bs), err +} + +// Byte returns the value of option n as a byte. +func (o Options) Byte(n Option) (byte, error) { + bs, err := o.Bytes(n) + if err != nil { + return 0, err + } + if len(bs) != 1 { + return 0, errOptionWrongSize + } + return bs[0], nil +} + +// Uint16 returns the value of option n as a uint16. +func (o Options) Uint16(n Option) (uint16, error) { + bs, err := o.Bytes(n) + if err != nil { + return 0, err + } + if len(bs) != 2 { + return 0, errOptionWrongSize + } + return binary.BigEndian.Uint16(bs), nil +} + +// Uint32 returns the value of option n as a uint32. +func (o Options) Uint32(n Option) (uint32, error) { + bs, err := o.Bytes(n) + if err != nil { + return 0, err + } + if len(bs) != 4 { + return 0, errOptionWrongSize + } + return binary.BigEndian.Uint32(bs), nil +} + +// Int32 returns the value of option n as an int32. +func (o Options) Int32(n Option) (int32, error) { + bs, err := o.Bytes(n) + if err != nil { + return 0, err + } + if len(bs) != 4 { + return 0, errOptionWrongSize + } + return int32(binary.BigEndian.Uint32(bs)), nil +} + +// IPs returns the value of option n as a list of IPv4 addresses. +func (o Options) IPs(n Option) ([]net.IP, error) { + bs, err := o.Bytes(n) + if err != nil { + return nil, err + } + if len(bs) < 4 || len(bs)%4 != 0 { + return nil, errOptionWrongSize + } + ret := make([]net.IP, len(bs)/4) + for i := 0; i < len(bs); i += 4 { + ret = append(ret, net.IP(bs[i:i+4])) + } + return ret, nil +} + +// IP returns the value of option n as an IPv4 address. +func (o Options) IP(n Option) (net.IP, error) { + ips, err := o.IPs(n) + if err != nil { + return nil, err + } + if len(ips) != 1 { + return nil, errOptionWrongSize + } + return ips[0], nil +} + +// IPMask returns the value of option n as a net.IPMask. +func (o Options) IPMask(n Option) (net.IPMask, error) { + bs := o[n] + if bs == nil { + return nil, fmt.Errorf("option %d not found", n) + } + if len(bs) != 4 { + return nil, fmt.Errorf("option %d is the wrong size for an IPMask", n) + } + return net.IPMask(bs), nil } diff --git a/dhcp4/options_test.go b/dhcp4/options_test.go index 1f33520..5fcf71d 100644 --- a/dhcp4/options_test.go +++ b/dhcp4/options_test.go @@ -23,22 +23,22 @@ func TestOptionReading(t *testing.T) { 3: []byte{0, 1}, } - b, ok := o.Byte(2) - if !ok { - t.Fatalf("Option 2 should be a valid byte") + b, err := o.Byte(2) + if err != nil { + t.Fatalf("Option 2 should be a valid byte, but got: %s", err) } if b != 3 { t.Fatalf("Wanted value 3 for option 2, got %d", b) } - b, ok = o.Byte(3) - if ok { + b, err = o.Byte(3) + if err == nil { t.Fatalf("Option 3 shouldn't be a valid byte") } - u, ok := o.Uint16(3) - if !ok { - t.Fatalf("Option 3 should be a valid byte") + u, err := o.Uint16(3) + if err != nil { + t.Fatalf("Option 3 should be a valid byte, but got: %s", err) } if u != 1 { t.Fatalf("Wanted value 1 for option 3, got %d", u) diff --git a/dhcp4/packet.go b/dhcp4/packet.go index 2297bae..6d0462c 100644 --- a/dhcp4/packet.go +++ b/dhcp4/packet.go @@ -125,11 +125,11 @@ func (p *Packet) DebugString() string { var opts []int for n := range p.Options { - opts = append(opts, n) + opts = append(opts, int(n)) } sort.Ints(opts) for _, n := range opts { - fmt.Fprintf(&b, " %d: %#v\n", n, p.Options[n]) + fmt.Fprintf(&b, " %d: %#v\n", n, p.Options[Option(n)]) } return b.String() } @@ -146,8 +146,8 @@ func (p *Packet) Marshal() ([]byte, error) { return nil, errors.New("sname must be <= 64 bytes") } optsInFile, optsInSname := false, false - v, ok := p.Options.Byte(52) - if ok { + v, err := p.Options.Byte(OptOverload) + if err == nil { optsInFile, optsInFile = v&1 != 0, v&2 != 0 } if optsInFile && p.BootFilename != "" { @@ -199,7 +199,6 @@ func (p *Packet) Marshal() ([]byte, error) { opts[k] = v } opts[53] = []byte{byte(p.Type)} - var err error if optsInSname { opts, err = opts.marshalLimited(ret, 64, true) if err != nil { @@ -286,8 +285,8 @@ func Unmarshal(bs []byte) (*Packet, error) { // meaning from BOOTP, or can store extra DHCP options if the main // options section specifies the "Option overload" option. file, sname := false, false - v, ok := ret.Options.Byte(52) - if ok { + v, err := ret.Options.Byte(OptOverload) + if err == nil { file, sname = v&1 != 0, v&2 != 0 } if sname { @@ -310,17 +309,17 @@ func Unmarshal(bs []byte) (*Packet, error) { if !ok { return nil, fmt.Errorf("unterminated 'file' string") } - ret.BootServerName = s + ret.BootFilename = s } // DHCP packets must all have at least the "DHCP Message Type" // option. - typ, ok := ret.Options.Byte(53) - if !ok { - return nil, errors.New("packet has no DHCP Message Type") + typ, err := ret.Options.Byte(OptDHCPMessageType) + if err != nil { + return nil, fmt.Errorf("getting DHCP Message type: %s", err) } ret.Type = MessageType(typ) - delete(ret.Options, 53) + delete(ret.Options, OptDHCPMessageType) switch ret.Type { case MsgDiscover, MsgRequest, MsgDecline, MsgRelease, MsgInform: if bs[0] != 1 {