package dhcpv6 import ( "errors" "fmt" "net" "strings" "time" "github.com/insomniacslk/dhcp/iana" "github.com/insomniacslk/dhcp/rfc1035label" "github.com/u-root/uio/rand" "github.com/u-root/uio/uio" ) const MessageHeaderSize = 4 // MessageOptions are the options that may appear in a normal DHCPv6 message. // // RFC 3315 Appendix B lists the valid options that can be used. type MessageOptions struct { Options } // ArchTypes returns the architecture type option. func (mo MessageOptions) ArchTypes() iana.Archs { opt := mo.GetOne(OptionClientArchType) if opt == nil { return nil } return opt.(*optClientArchType).Archs } // ClientID returns the client identifier option. func (mo MessageOptions) ClientID() DUID { opt := mo.GetOne(OptionClientID) if opt == nil { return nil } return opt.(*optClientID).DUID } // ServerID returns the server identifier option. func (mo MessageOptions) ServerID() DUID { opt := mo.GetOne(OptionServerID) if opt == nil { return nil } return opt.(*optServerID).DUID } // IANA returns all Identity Association for Non-temporary Address options. func (mo MessageOptions) IANA() []*OptIANA { opts := mo.Get(OptionIANA) var ianas []*OptIANA for _, o := range opts { ianas = append(ianas, o.(*OptIANA)) } return ianas } // OneIANA returns the first IANA option. func (mo MessageOptions) OneIANA() *OptIANA { ianas := mo.IANA() if len(ianas) == 0 { return nil } return ianas[0] } // IATA returns all Identity Association for Temporary Address options. func (mo MessageOptions) IATA() []*OptIATA { opts := mo.Get(OptionIATA) var iatas []*OptIATA for _, o := range opts { iatas = append(iatas, o.(*OptIATA)) } return iatas } // OneIATA returns the first IATA option. func (mo MessageOptions) OneIATA() *OptIATA { iatas := mo.IATA() if len(iatas) == 0 { return nil } return iatas[0] } // IAPD returns all Identity Association for Prefix Delegation options. func (mo MessageOptions) IAPD() []*OptIAPD { opts := mo.Get(OptionIAPD) var ianas []*OptIAPD for _, o := range opts { ianas = append(ianas, o.(*OptIAPD)) } return ianas } // OneIAPD returns the first IAPD option. func (mo MessageOptions) OneIAPD() *OptIAPD { iapds := mo.IAPD() if len(iapds) == 0 { return nil } return iapds[0] } // FourRD returns all 4RD options. func (mo MessageOptions) FourRD() []*Opt4RD { opts := mo.Get(Option4RD) var frds []*Opt4RD for _, o := range opts { if m, ok := o.(*Opt4RD); ok { frds = append(frds, m) } } return frds } // Status returns the status code associated with this option. func (mo MessageOptions) Status() *OptStatusCode { opt := mo.Options.GetOne(OptionStatusCode) if opt == nil { return nil } sc, ok := opt.(*OptStatusCode) if !ok { return nil } return sc } // RequestedOptions returns the Options Requested Option. func (mo MessageOptions) RequestedOptions() OptionCodes { // Technically, RFC 8415 states that ORO may only appear once in the // area of a DHCP message. However, some proprietary clients have been // observed sending more than one OptionORO. // // So we merge them. opt := mo.Options.Get(OptionORO) if len(opt) == 0 { return nil } var oc OptionCodes for _, o := range opt { if oro, ok := o.(*optRequestedOption); ok { oc = append(oc, oro.OptionCodes...) } } return oc } // DNS returns the DNS Recursive Name Server option as defined by RFC 3646. func (mo MessageOptions) DNS() []net.IP { opt := mo.Options.GetOne(OptionDNSRecursiveNameServer) if opt == nil { return nil } if dns, ok := opt.(*optDNS); ok { return dns.NameServers } return nil } // DomainSearchList returns the Domain List option as defined by RFC 3646. func (mo MessageOptions) DomainSearchList() *rfc1035label.Labels { opt := mo.Options.GetOne(OptionDomainSearchList) if opt == nil { return nil } if dsl, ok := opt.(*optDomainSearchList); ok { return dsl.DomainSearchList } return nil } // BootFileURL returns the Boot File URL option as defined by RFC 5970. func (mo MessageOptions) BootFileURL() string { opt := mo.Options.GetOne(OptionBootfileURL) if opt == nil { return "" } if u, ok := opt.(*optBootFileURL); ok { return u.url } return "" } // BootFileParam returns the Boot File Param option as defined by RFC 5970. func (mo MessageOptions) BootFileParam() []string { opt := mo.Options.GetOne(OptionBootfileParam) if opt == nil { return nil } if u, ok := opt.(*optBootFileParam); ok { return u.params } return nil } // UserClasses returns a list of user classes. func (mo MessageOptions) UserClasses() [][]byte { opt := mo.Options.GetOne(OptionUserClass) if opt == nil { return nil } if t, ok := opt.(*OptUserClass); ok { return t.UserClasses } return nil } // VendorClasses returns the all vendor class options. func (mo MessageOptions) VendorClasses() []*OptVendorClass { opt := mo.Options.Get(OptionVendorClass) if opt == nil { return nil } var vo []*OptVendorClass for _, o := range opt { if t, ok := o.(*OptVendorClass); ok { vo = append(vo, t) } } return vo } // VendorClass returns the vendor class options matching the given enterprise // number. func (mo MessageOptions) VendorClass(enterpriseNumber uint32) [][]byte { vo := mo.VendorClasses() for _, v := range vo { if v.EnterpriseNumber == enterpriseNumber { return v.Data } } return nil } // VendorOpts returns the all vendor-specific options. // // RFC 8415 Section 21.17: // // Multiple instances of the Vendor-specific Information option may appear in // a DHCP message. func (mo MessageOptions) VendorOpts() []*OptVendorOpts { opt := mo.Options.Get(OptionVendorOpts) if opt == nil { return nil } var vo []*OptVendorOpts for _, o := range opt { if t, ok := o.(*OptVendorOpts); ok { vo = append(vo, t) } } return vo } // VendorOpt returns the vendor options matching the given enterprise number. // // RFC 8415 Section 21.17: // // Servers and clients MUST NOT send more than one instance of the // Vendor-specific Information option with the same Enterprise Number. func (mo MessageOptions) VendorOpt(enterpriseNumber uint32) Options { vo := mo.VendorOpts() for _, v := range vo { if v.EnterpriseNumber == enterpriseNumber { return v.VendorOpts } } return nil } // ElapsedTime returns the Elapsed Time option as defined by RFC 3315 Section 22.9. // // ElapsedTime returns a duration of 0 if the option is not present. func (mo MessageOptions) ElapsedTime() time.Duration { opt := mo.Options.GetOne(OptionElapsedTime) if opt == nil { return 0 } if t, ok := opt.(*optElapsedTime); ok { return t.ElapsedTime } return 0 } // InformationRefreshTime returns the Information Refresh Time option // as defined by RFC 815 Section 21.23. // // InformationRefreshTime returns the provided default if no option is present. func (mo MessageOptions) InformationRefreshTime(def time.Duration) time.Duration { opt := mo.Options.GetOne(OptionInformationRefreshTime) if opt == nil { return def } if t, ok := opt.(*optInformationRefreshTime); ok { return t.InformationRefreshtime } return def } // FQDN returns the FQDN option as defined by RFC 4704. func (mo MessageOptions) FQDN() *OptFQDN { opt := mo.Options.GetOne(OptionFQDN) if opt == nil { return nil } if fqdn, ok := opt.(*OptFQDN); ok { return fqdn } return nil } // DHCP4oDHCP6Server returns the DHCP 4o6 Server Address option as // defined by RFC 7341. func (mo MessageOptions) DHCP4oDHCP6Server() *OptDHCP4oDHCP6Server { opt := mo.Options.GetOne(OptionDHCP4oDHCP6Server) if opt == nil { return nil } if server, ok := opt.(*OptDHCP4oDHCP6Server); ok { return server } return nil } // NTPServers returns the NTP server addresses contained in the // NTP_SUBOPTION_SRV_ADDR of an OPTION_NTP_SERVER. // If multiple NTP server options exist, the function will return all the NTP // server addresses it finds, as defined by RFC 5908. func (mo MessageOptions) NTPServers() []net.IP { opts := mo.Options.Get(OptionNTPServer) if opts == nil { return nil } addrs := make([]net.IP, 0) for _, opt := range opts { ntp, ok := opt.(*OptNTPServer) if !ok { continue } for _, subopt := range ntp.Suboptions { so, ok := subopt.(*NTPSuboptionSrvAddr) if !ok { continue } addrs = append(addrs, net.IP(*so)) } } return addrs } // Message represents a DHCPv6 Message as defined by RFC 3315 Section 6. type Message struct { MessageType MessageType TransactionID TransactionID Options MessageOptions } var randomRead = rand.Read // GenerateTransactionID generates a random 3-byte transaction ID. func GenerateTransactionID() (TransactionID, error) { var tid TransactionID n, err := randomRead(tid[:]) if err != nil { return tid, err } if n != len(tid) { return tid, fmt.Errorf("invalid random sequence: shorter than 3 bytes") } return tid, nil } // GetTime returns a time integer suitable for DUID-LLT, i.e. the current time counted // in seconds since January 1st, 2000, midnight UTC, modulo 2^32 func GetTime() uint32 { now := time.Since(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) return uint32((now.Nanoseconds() / 1000000000) % 0xffffffff) } // NewSolicit creates a new SOLICIT message, using the given hardware address to // derive the IAID in the IA_NA option. func NewSolicit(hwaddr net.HardwareAddr, modifiers ...Modifier) (*Message, error) { duid := &DUIDLLT{ HWType: iana.HWTypeEthernet, Time: GetTime(), LinkLayerAddr: hwaddr, } m, err := NewMessage() if err != nil { return nil, err } m.MessageType = MessageTypeSolicit m.AddOption(OptClientID(duid)) m.AddOption(OptRequestedOption( OptionDNSRecursiveNameServer, OptionDomainSearchList, )) m.AddOption(OptElapsedTime(0)) if len(hwaddr) < 4 { return nil, errors.New("short hardware addrss: less than 4 bytes") } l := len(hwaddr) var iaid [4]byte copy(iaid[:], hwaddr[l-4:l]) modifiers = append([]Modifier{WithIAID(iaid)}, modifiers...) // Apply modifiers for _, mod := range modifiers { mod(m) } return m, nil } // NewAdvertiseFromSolicit creates a new ADVERTISE packet based on an SOLICIT packet. func NewAdvertiseFromSolicit(sol *Message, modifiers ...Modifier) (*Message, error) { if sol == nil { return nil, errors.New("SOLICIT cannot be nil") } if sol.Type() != MessageTypeSolicit { return nil, errors.New("The passed SOLICIT must have SOLICIT type set") } // build ADVERTISE from SOLICIT adv := &Message{ MessageType: MessageTypeAdvertise, TransactionID: sol.TransactionID, } // add Client ID cid := sol.GetOneOption(OptionClientID) if cid == nil { return nil, errors.New("Client ID cannot be nil in SOLICIT when building ADVERTISE") } adv.AddOption(cid) // apply modifiers for _, mod := range modifiers { mod(adv) } return adv, nil } // NewRequestFromAdvertise creates a new REQUEST packet based on an ADVERTISE // packet options. func NewRequestFromAdvertise(adv *Message, modifiers ...Modifier) (*Message, error) { if adv == nil { return nil, errors.New("ADVERTISE cannot be nil") } if adv.MessageType != MessageTypeAdvertise { return nil, fmt.Errorf("The passed ADVERTISE must have ADVERTISE type set") } // build REQUEST from ADVERTISE req, err := NewMessage() if err != nil { return nil, err } req.MessageType = MessageTypeRequest // add Client ID cid := adv.GetOneOption(OptionClientID) if cid == nil { return nil, fmt.Errorf("Client ID cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(cid) // add Server ID sid := adv.GetOneOption(OptionServerID) if sid == nil { return nil, fmt.Errorf("Server ID cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(sid) // add Elapsed Time req.AddOption(OptElapsedTime(0)) // add IA_NA iana := adv.Options.OneIANA() if iana == nil { return nil, fmt.Errorf("IA_NA cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(iana) // add IA_PD if iaPd := adv.GetOneOption(OptionIAPD); iaPd != nil { req.AddOption(iaPd) } req.AddOption(OptRequestedOption( OptionDNSRecursiveNameServer, OptionDomainSearchList, )) // add OPTION_VENDOR_CLASS, only if present in the original request // TODO implement OptionVendorClass vClass := adv.GetOneOption(OptionVendorClass) if vClass != nil { req.AddOption(vClass) } // apply modifiers for _, mod := range modifiers { mod(req) } return req, nil } // NewReplyFromMessage creates a new REPLY packet based on a // Message. The function is to be used when generating a reply to a SOLICIT with // rapid-commit, REQUEST, CONFIRM, RENEW, REBIND, RELEASE and INFORMATION-REQUEST // packets. func NewReplyFromMessage(msg *Message, modifiers ...Modifier) (*Message, error) { if msg == nil { return nil, errors.New("message cannot be nil") } switch msg.Type() { case MessageTypeSolicit: if msg.GetOneOption(OptionRapidCommit) == nil { return nil, errors.New("cannot create REPLY from a SOLICIT without rapid-commit option") } modifiers = append([]Modifier{WithRapidCommit}, modifiers...) case MessageTypeRequest, MessageTypeConfirm, MessageTypeRenew, MessageTypeRebind, MessageTypeRelease, MessageTypeInformationRequest: default: return nil, errors.New("cannot create REPLY from the passed message type set") } // build REPLY from MESSAGE rep := &Message{ MessageType: MessageTypeReply, TransactionID: msg.TransactionID, } // add Client ID cid := msg.GetOneOption(OptionClientID) if cid == nil { return nil, errors.New("Client ID cannot be nil when building REPLY") } rep.AddOption(cid) // apply modifiers for _, mod := range modifiers { mod(rep) } return rep, nil } // Type returns this message's message type. func (m Message) Type() MessageType { return m.MessageType } // GetInnerMessage returns the message itself. func (m *Message) GetInnerMessage() (*Message, error) { return m, nil } // AddOption adds an option to this message. func (m *Message) AddOption(option Option) { m.Options.Add(option) } // UpdateOption updates the existing options with the passed option, adding it // at the end if not present already func (m *Message) UpdateOption(option Option) { m.Options.Update(option) } // IsNetboot returns true if the machine is trying to netboot. It checks if // "boot file" is one of the requested options, which is useful for // SOLICIT/REQUEST packet types, it also checks if the "boot file" option is // included in the packet, which is useful for ADVERTISE/REPLY packet. func (m *Message) IsNetboot() bool { if m.IsOptionRequested(OptionBootfileURL) { return true } if optbf := m.GetOneOption(OptionBootfileURL); optbf != nil { return true } return false } // IsOptionRequested takes an OptionCode and returns true if that option is // within the requested options of the DHCPv6 message. func (m *Message) IsOptionRequested(requested OptionCode) bool { return m.Options.RequestedOptions().Contains(requested) } // String returns a short human-readable string for this message. func (m *Message) String() string { return fmt.Sprintf("Message(MessageType=%s, TransactionID=%#x, %d options)", m.MessageType, m.TransactionID, len(m.Options.Options)) } // Summary prints all options associated with this message. func (m *Message) Summary() string { return m.LongString(0) } // LongString prints all options associated with this message. func (m *Message) LongString(spaceIndent int) string { indent := strings.Repeat(" ", spaceIndent) var s strings.Builder s.WriteString("Message{\n") s.WriteString(indent) s.WriteString(fmt.Sprintf(" MessageType=%s\n", m.MessageType)) s.WriteString(indent) s.WriteString(fmt.Sprintf(" TransactionID=%s\n", m.TransactionID)) s.WriteString(indent) s.WriteString(" Options: ") s.WriteString(m.Options.Options.LongString(spaceIndent + 2)) s.WriteString("\n") s.WriteString(indent) s.WriteString("}") return s.String() } // ToBytes returns the serialized version of this message as defined by RFC // 3315, Section 5. func (m *Message) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) buf.Write8(uint8(m.MessageType)) buf.WriteBytes(m.TransactionID[:]) buf.WriteBytes(m.Options.ToBytes()) return buf.Data() } // GetOption returns the options associated with the code. func (m *Message) GetOption(code OptionCode) []Option { return m.Options.Get(code) } // GetOneOption returns the first associated option with the code from this // message. func (m *Message) GetOneOption(code OptionCode) Option { return m.Options.GetOne(code) } // IsRelay returns whether this is a relay message or not. func (m *Message) IsRelay() bool { return false }