master
go 199 lines 4.41 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "bufio"
7 "encoding/csv"
8 "flag"
9 "fmt"
10 "io"
11 "net/http"
12 "os"
13 "regexp"
14 "sort"
15 "strings"
16 "time"
17 )
18
19 type ieeeSource struct {
20 name string
21 url string
22 }
23
24 var ieeeSources = []ieeeSource{
25 {name: "ieee_oui", url: "https://standards-oui.ieee.org/oui/oui.csv"},
26 {name: "ieee_cid", url: "https://standards-oui.ieee.org/cid/cid.csv"},
27 {name: "ieee_oui36", url: "https://standards-oui.ieee.org/oui36/oui36.csv"},
28 {name: "ieee_iab", url: "https://standards-oui.ieee.org/iab/iab.csv"},
29 }
30
31 var nonHex = regexp.MustCompile(`[^0-9A-Fa-f]`)
32 var httpClient = &http.Client{Timeout: 30 * time.Second}
33
34 func main() {
35 var outputPath string
36 flag.StringVar(&outputPath, "out", "", "output TSV path (required)")
37 flag.Parse()
38
39 if strings.TrimSpace(outputPath) == "" {
40 fmt.Fprintln(os.Stderr, "missing required -out argument")
41 os.Exit(2)
42 }
43
44 index, err := buildVendorIndex()
45 if err != nil {
46 fmt.Fprintf(os.Stderr, "failed to build OUI index: %v\n", err)
47 os.Exit(1)
48 }
49 if err := writeDataset(outputPath, index); err != nil {
50 fmt.Fprintf(os.Stderr, "failed to write dataset: %v\n", err)
51 os.Exit(1)
52 }
53 fmt.Printf("wrote %d entries to %s\n", len(index), outputPath)
54 }
55
56 func buildVendorIndex() (map[string]string, error) {
57 prefixToVendor := make(map[string]string, 120000)
58
59 for _, src := range ieeeSources {
60 records, err := fetchCSV(src.url)
61 if err != nil {
62 return nil, fmt.Errorf("%s: %w", src.name, err)
63 }
64 for i, record := range records {
65 if i == 0 { // header
66 continue
67 }
68 if len(record) < 3 {
69 continue
70 }
71 registry := strings.TrimSpace(record[0])
72 assignment := normalizeAssignment(record[1])
73 vendor := strings.TrimSpace(record[2])
74 if assignment == "" || vendor == "" {
75 continue
76 }
77 prefixLen := prefixLengthForRegistry(registry, assignment)
78 if prefixLen == 0 || len(assignment) < prefixLen {
79 continue
80 }
81 prefix := assignment[:prefixLen]
82
83 if existing, ok := prefixToVendor[prefix]; ok {
84 // Prefer the more descriptive name on conflicts.
85 if len(existing) >= len(vendor) {
86 continue
87 }
88 }
89 prefixToVendor[prefix] = vendor
90 }
91 }
92 return prefixToVendor, nil
93 }
94
95 func fetchCSV(url string) ([][]string, error) {
96 req, err := http.NewRequest(http.MethodGet, url, nil)
97 if err != nil {
98 return nil, err
99 }
100 req.Header.Set("User-Agent", "netdata-topology-oui-dataset-updater/1.0")
101
102 resp, err := httpClient.Do(req)
103 if err != nil {
104 return nil, err
105 }
106 defer resp.Body.Close()
107
108 if resp.StatusCode != http.StatusOK {
109 return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
110 }
111
112 reader := csv.NewReader(resp.Body)
113 reader.FieldsPerRecord = -1
114 records := make([][]string, 0, 65536)
115 for {
116 record, err := reader.Read()
117 if err == io.EOF {
118 break
119 }
120 if err != nil {
121 return nil, err
122 }
123 records = append(records, record)
124 }
125 return records, nil
126 }
127
128 func normalizeAssignment(raw string) string {
129 raw = strings.TrimSpace(raw)
130 raw = nonHex.ReplaceAllString(raw, "")
131 raw = strings.ToUpper(raw)
132 if len(raw) > 12 {
133 raw = raw[:12]
134 }
135 return raw
136 }
137
138 func prefixLengthForRegistry(registry, assignment string) int {
139 switch strings.ToUpper(strings.TrimSpace(registry)) {
140 case "MA-S", "IAB":
141 if len(assignment) >= 9 {
142 return 9
143 }
144 case "MA-M":
145 if len(assignment) >= 7 {
146 return 7
147 }
148 case "MA-L", "CID":
149 if len(assignment) >= 6 {
150 return 6
151 }
152 }
153 // Fallback for non-standard/unknown registry values.
154 if len(assignment) >= 9 {
155 return 9
156 }
157 if len(assignment) >= 7 {
158 return 7
159 }
160 if len(assignment) >= 6 {
161 return 6
162 }
163 return 0
164 }
165
166 func writeDataset(path string, index map[string]string) error {
167 f, err := os.Create(path)
168 if err != nil {
169 return err
170 }
171 defer f.Close()
172
173 w := bufio.NewWriterSize(f, 1<<20)
174 defer w.Flush()
175
176 prefixes := make([]string, 0, len(index))
177 for prefix := range index {
178 prefixes = append(prefixes, prefix)
179 }
180 sort.Slice(prefixes, func(i, j int) bool {
181 if len(prefixes[i]) != len(prefixes[j]) {
182 return len(prefixes[i]) > len(prefixes[j])
183 }
184 return prefixes[i] < prefixes[j]
185 })
186
187 fmt.Fprintf(w, "# Netdata Topology OUI Vendor Dataset\n")
188 fmt.Fprintf(w, "# Generated: %s\n", time.Now().UTC().Format(time.RFC3339))
189 fmt.Fprintf(w, "# Sources:\n")
190 for _, src := range ieeeSources {
191 fmt.Fprintf(w, "# - %s %s\n", src.name, src.url)
192 }
193 fmt.Fprintf(w, "# Format: <HEX_PREFIX>\\t<VENDOR>\n")
194
195 for _, prefix := range prefixes {
196 fmt.Fprintf(w, "%s\t%s\n", prefix, index[prefix])
197 }
198 return nil
199 }