package dhcpv6 import ( "crypto/rand" "encoding/binary" "errors" "fmt" "log" "net" "time" "github.com/insomniacslk/dhcp/iana" ) const MessageHeaderSize = 4 type DHCPv6Message struct { messageType MessageType transactionID uint32 // only 24 bits are used though options []Option } func BytesToTransactionID(data []byte) (*uint32, error) { // return a uint32 from a sequence of bytes, representing a transaction ID. // Transaction IDs are three-bytes long. If the provided data is shorter than // 3 bytes, it return an error. If longer, will use the first three bytes // only. if len(data) < 3 { return nil, fmt.Errorf("Invalid transaction ID: less than 3 bytes") } buf := make([]byte, 4) copy(buf[1:4], data[:3]) tid := binary.BigEndian.Uint32(buf) return &tid, nil } var randomRead = rand.Read func GenerateTransactionID() (*uint32, error) { var tid *uint32 for { tidBytes := make([]byte, 4) n, err := randomRead(tidBytes) if err != nil { return nil, err } if n != 4 { return nil, fmt.Errorf("invalid random sequence: shorter than 4 bytes") } tid, err = BytesToTransactionID(tidBytes) if err != nil { return nil, err } if tid == nil { return nil, fmt.Errorf("got a nil Transaction ID") } // retry until != 0 // TODO add retry limit if *tid != 0 { break } } 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) } // NewSolicitWithCID creates a new SOLICIT message with CID. func NewSolicitWithCID(duid Duid, modifiers ...Modifier) (DHCPv6, error) { d, err := NewMessage() if err != nil { return nil, err } d.(*DHCPv6Message).SetMessage(MessageTypeSolicit) d.AddOption(&OptClientId{Cid: duid}) oro := new(OptRequestedOption) oro.SetRequestedOptions([]OptionCode{ OptionDNSRecursiveNameServer, OptionDomainSearchList, }) d.AddOption(oro) d.AddOption(&OptElapsedTime{}) // FIXME use real values for IA_NA iaNa := &OptIANA{} iaNa.IaId = [4]byte{0xfa, 0xce, 0xb0, 0x0c} iaNa.T1 = 0xe10 iaNa.T2 = 0x1518 d.AddOption(iaNa) // Apply modifiers for _, mod := range modifiers { d = mod(d) } return d, nil } // NewSolicitForInterface creates a new SOLICIT message with DUID-LLT, using the // given network interface's hardware address and current time func NewSolicitForInterface(ifname string, modifiers ...Modifier) (DHCPv6, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } duid := Duid{ Type: DUID_LLT, HwType: iana.HwTypeEthernet, Time: GetTime(), LinkLayerAddr: iface.HardwareAddr, } return NewSolicitWithCID(duid, modifiers...) } // NewAdvertiseFromSolicit creates a new ADVERTISE packet based on an SOLICIT packet. func NewAdvertiseFromSolicit(solicit DHCPv6, modifiers ...Modifier) (DHCPv6, error) { if solicit == nil { return nil, errors.New("SOLICIT cannot be nil") } if solicit.Type() != MessageTypeSolicit { return nil, errors.New("The passed SOLICIT must have SOLICIT type set") } sol, ok := solicit.(*DHCPv6Message) if !ok { return nil, errors.New("The passed SOLICIT must be of DHCPv6Message type") } // build ADVERTISE from SOLICIT adv := DHCPv6Message{} adv.SetMessage(MessageTypeAdvertise) adv.SetTransactionID(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 d := DHCPv6(&adv) for _, mod := range modifiers { d = mod(d) } return d, nil } // NewRequestFromAdvertise creates a new REQUEST packet based on an ADVERTISE // packet options. func NewRequestFromAdvertise(advertise DHCPv6, modifiers ...Modifier) (DHCPv6, error) { if advertise == nil { return nil, fmt.Errorf("ADVERTISE cannot be nil") } if advertise.Type() != MessageTypeAdvertise { return nil, fmt.Errorf("The passed ADVERTISE must have ADVERTISE type set") } adv, ok := advertise.(*DHCPv6Message) if !ok { return nil, fmt.Errorf("The passed ADVERTISE must be of DHCPv6Message type") } // build REQUEST from ADVERTISE req := DHCPv6Message{} req.SetMessage(MessageTypeRequest) req.SetTransactionID(adv.TransactionID()) // 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{}) // add IA_NA iaNa := adv.GetOneOption(OptionIANA) if iaNa == nil { return nil, fmt.Errorf("IA_NA cannot be nil in ADVERTISE when building REQUEST") } req.AddOption(iaNa) // add OptRequestedOption oro := OptRequestedOption{} oro.SetRequestedOptions([]OptionCode{ OptionDNSRecursiveNameServer, OptionDomainSearchList, }) req.AddOption(&oro) // 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 d := DHCPv6(&req) for _, mod := range modifiers { d = mod(d) } return d, nil } // NewReplyFromDHCPv6Message creates a new REPLY packet based on a // DHCPv6Message. The function is to be used when generating a reply to // REQUEST, CONFIRM, RENEW, REBIND, RELEASE and INFORMATION-REQUEST packets. func NewReplyFromDHCPv6Message(message DHCPv6, modifiers ...Modifier) (DHCPv6, error) { if message == nil { return nil, errors.New("DHCPv6Message cannot be nil") } switch message.Type() { case MessageTypeRequest, MessageTypeConfirm, MessageTypeRenew, MessageTypeRebind, MessageTypeRelease, MessageTypeInformationRequest: default: return nil, errors.New("Cannot create REPLY from the passed message type set") } msg, ok := message.(*DHCPv6Message) if !ok { return nil, errors.New("The passed MESSAGE must be of DHCPv6Message type") } // build REPLY from MESSAGE rep := DHCPv6Message{} rep.SetMessage(MessageTypeReply) rep.SetTransactionID(msg.TransactionID()) // add Client ID cid := message.GetOneOption(OptionClientID) if cid == nil { return nil, errors.New("Client ID cannot be nil when building REPLY") } rep.AddOption(cid) // apply modifiers d := DHCPv6(&rep) for _, mod := range modifiers { d = mod(d) } return d, nil } func (d *DHCPv6Message) Type() MessageType { return d.messageType } func (d *DHCPv6Message) SetMessage(messageType MessageType) { msgString := messageType.String() if msgString == "" { log.Printf("Warning: unknown DHCPv6 message type: %v", messageType) } if messageType == MessageTypeRelayForward || messageType == MessageTypeRelayReply { log.Printf("Warning: using a RELAY message type with a non-relay message: %v (%v)", msgString, messageType) } d.messageType = messageType } func (d *DHCPv6Message) MessageTypeToString() string { return d.messageType.String() } func (d *DHCPv6Message) TransactionID() uint32 { return d.transactionID } func (d *DHCPv6Message) SetTransactionID(tid uint32) { ttid := tid & 0x00ffffff if ttid != tid { log.Printf("Warning: truncating transaction ID that is longer than 24 bits: %v", tid) } d.transactionID = ttid } func (d *DHCPv6Message) SetOptions(options []Option) { d.options = options } func (d *DHCPv6Message) AddOption(option Option) { d.options = append(d.options, option) } func (d *DHCPv6Message) UpdateOption(option Option) { for idx, opt := range d.options { if opt.Code() == option.Code() { d.options[idx] = option // don't look further return } } // if not found, add it d.AddOption(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 (d *DHCPv6Message) IsNetboot() bool { if d.IsOptionRequested(OptionBootfileURL) { return true } if optbf := d.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 (d *DHCPv6Message) IsOptionRequested(requested OptionCode) bool { for _, optoro := range d.GetOption(OptionORO) { for _, o := range optoro.(*OptRequestedOption).RequestedOptions() { if o == requested { return true } } } return false } func (d *DHCPv6Message) String() string { return fmt.Sprintf("DHCPv6Message(messageType=%v transactionID=0x%06x, %d options)", d.Type().String(), d.TransactionID(), len(d.options), ) } func (d *DHCPv6Message) Summary() string { ret := fmt.Sprintf( "DHCPv6Message\n"+ " messageType=%v\n"+ " transactionid=0x%06x\n", d.Type().String(), d.TransactionID(), ) ret += " options=[" if len(d.options) > 0 { ret += "\n" } for _, opt := range d.options { ret += fmt.Sprintf(" %v\n", opt.String()) } ret += " ]\n" return ret } // Convert a DHCPv6Message structure into its binary representation, suitable for being // sent over the network func (d *DHCPv6Message) ToBytes() []byte { var ret []byte ret = append(ret, byte(d.messageType)) tidBytes := make([]byte, 4) binary.BigEndian.PutUint32(tidBytes, d.transactionID) ret = append(ret, tidBytes[1:4]...) // discard the first byte for _, opt := range d.options { ret = append(ret, opt.ToBytes()...) } return ret } func (d *DHCPv6Message) Length() int { mLen := 4 for _, opt := range d.options { mLen += opt.Length() + 4 // +4 for opt code and opt len } return mLen } func (d *DHCPv6Message) Options() []Option { return d.options } func (d *DHCPv6Message) GetOption(code OptionCode) []Option { return getOptions(d.options, code, false) } func (d *DHCPv6Message) GetOneOption(code OptionCode) Option { return getOption(d.options, code) } func (d *DHCPv6Message) IsRelay() bool { return false }