package ztpv4 import ( "bytes" "errors" "fmt" "strconv" "strings" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/iana" ) // VendorData is optional data a particular vendor may or may not include // in the Vendor Class options. type VendorData struct { VendorName, Model, Serial string } var errVendorOptionMalformed = errors.New("malformed vendor option") var errClientIDOptionMissing = errors.New("client identifier option is missing") func parseClassIdentifier(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd := &VendorData{} switch vc := packet.ClassIdentifier(); { // Arista;DCS-7050S-64;01.23;JPE12221671 case strings.HasPrefix(vc, "Arista;"): p := strings.Split(vc, ";") if len(p) < 4 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[3] return vd, nil // ZPESystems:NSC:002251623 case strings.HasPrefix(vc, "ZPESystems:"): p := strings.Split(vc, ":") if len(p) < 3 { return nil, errVendorOptionMalformed } vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[2] return vd, nil // Juniper option 60 parsing is a bit more nuanced. The following are all // "valid" identifying strings for Juniper: // Juniper-ptx1000-DD576 --- (serial in hostname option) // Juniper-qfx10002-361-DN817 -- (model has a dash in it!) case strings.HasPrefix(vc, "Juniper-"): p := strings.Split(vc, "-") if len(p) < 3 { vd.Model = p[1] vd.Serial = packet.HostName() if len(vd.Serial) == 0 { return nil, errors.New("host name option is missing") } } else { vd.Model = strings.Join(p[1:len(p)-1], "-") vd.Serial = p[len(p)-1] } vd.VendorName = p[0] return vd, nil // Juniper:tttt-ttt:DN817 case strings.HasPrefix(vc, "Juniper:"): p := strings.Split(vc, ":") if len(p) == 3 { vd.VendorName = p[0] vd.Model = p[1] vd.Serial = p[2] return vd, nil } return nil, fmt.Errorf("%w got '%s'", errVendorOptionMalformed, vc) // For Ciena the class identifier (opt 60) is written in the following format: // {vendor iana code}-{product}-{type} // For Ciena the iana code is 1271 // The product type is a number that maps to a Ciena product // The type is used to identified different subtype of the product. // An example can be ‘1271-23422Z11-123’. case strings.HasPrefix(vc, strconv.Itoa(int(iana.EnterpriseIDCienaCorporation))): v := strings.Split(vc, "-") if len(v) != 3 { return nil, fmt.Errorf("%w got '%s'", errVendorOptionMalformed, vc) } vd.VendorName = iana.EnterpriseIDCienaCorporation.String() vd.Model = v[1] + "-" + v[2] vd.Serial = dhcpv4.GetString(dhcpv4.OptionClientIdentifier, packet.Options) if len(vd.Serial) == 0 { return nil, errClientIDOptionMissing } return vd, nil // Cisco Firepower FPR4100/9300 models use Opt 60 for model info // and Opt 61 contains the serial number case vc == "FPR4100" || vc == "FPR9300": vd.VendorName = iana.EnterpriseIDCiscoSystems.String() vd.Model = vc vd.Serial = dhcpv4.GetString(dhcpv4.OptionClientIdentifier, packet.Options) if len(vd.Serial) == 0 { return nil, errClientIDOptionMissing } return vd, nil } return nil, nil } func parseVIVC(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd := &VendorData{} for _, id := range packet.VIVC() { if id.EntID == iana.EnterpriseIDCiscoSystems { vd.VendorName = iana.EnterpriseIDCiscoSystems.String() //SN:0;PID:R-IOSXRV9000-CC for _, f := range bytes.Split(id.Data, []byte(";")) { p := bytes.Split(f, []byte(":")) if len(p) != 2 { return nil, errVendorOptionMalformed } switch string(p[0]) { case "SN": vd.Serial = string(p[1]) case "PID": vd.Model = string(p[1]) } } return vd, nil } } return nil, nil } // ParseVendorData will try to parse dhcp4 options looking for more // specific vendor data (like model, serial number, etc). func ParseVendorData(packet *dhcpv4.DHCPv4) (*VendorData, error) { vd, err := parseClassIdentifier(packet) if err != nil { return nil, err } // If VendorData is set, return early if vd != nil { return vd, nil } vd, err = parseVIVC(packet) if err != nil { return nil, err } if vd != nil { return vd, nil } return nil, errors.New("no known ZTP vendor found") }