summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4/ztpv4/ztp.go
blob: 8ab3a8e75a938df1dd4bf6287c9eb690a75d49ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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      <vendor>-<model>-<serial
	//    Juniper-qfx10008           <vendor>-<model> (serial in hostname option)
	//    Juniper-qfx10002-361-DN817 <vendor>-<model>-<serial> (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")
}