generate-oui-table.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. """
  3. Regenerates RackPeek.Domain/Discovery/MacVendorTable.g.cs from the IEEE OUI registry.
  4. curl -sL -o /tmp/oui.csv https://standards-oui.ieee.org/oui/oui.csv
  5. ./generate-oui-table.py /tmp/oui.csv
  6. The registry holds ~40,000 assignments; shipping all of them would bloat the
  7. single-file binaries for little gain, so this keeps a curated subset: the vendors
  8. whose hardware actually turns up on a homelab network. Add a pattern to VENDORS
  9. below and re-run to extend it.
  10. Records are packed as fixed-width "PPPPPPv" (6 hex prefix chars + one vendor index
  11. char) in a sorted string so the lookup is a binary search with no allocation and no
  12. dictionary to build at startup.
  13. """
  14. import csv
  15. import re
  16. import sys
  17. # Display name -> pattern matched against the registry's Organization Name.
  18. # Anchored where a loose substring would drag in unrelated companies.
  19. VENDORS = {
  20. "Apple": r"^Apple, Inc\.?$|^Apple Inc",
  21. "Intel": r"^Intel Corporate$|^Intel Corporation$",
  22. "Ubiquiti": r"Ubiquiti",
  23. "Raspberry Pi": r"Raspberry Pi",
  24. "Espressif": r"Espressif|^Shanghai High-Flying|Ai-Thinker",
  25. "Proxmox": r"Proxmox",
  26. "VMware": r"VMware",
  27. "QEMU/KVM": r"^QEMU",
  28. "VirtualBox": r"PCS Systemtechnik|VirtualBox",
  29. "Microsoft": r"^Microsoft Corporation$",
  30. "Parallels": r"^Parallels",
  31. "Synology": r"Synology",
  32. "QNAP": r"QNAP",
  33. "Netgear": r"NETGEAR|Netgear",
  34. "TP-Link": r"TP-LINK|TP-Link|Tp-Link",
  35. "D-Link": r"D-Link|D\-LINK",
  36. "MikroTik": r"MikroTik|Mikrotik|Routerboard",
  37. "Cisco": r"^Cisco Systems|^Cisco$",
  38. "Aruba": r"Aruba",
  39. "Zyxel": r"ZyXEL|Zyxel",
  40. "Realtek": r"Realtek",
  41. "Broadcom": r"^Broadcom",
  42. "NVIDIA": r"NVIDIA|Nvidia",
  43. "AMD": r"^Advanced Micro Devices",
  44. "Dell": r"^Dell Inc|^Dell Computer|^Dell EMC",
  45. "HPE/HP": r"Hewlett Packard|^HP Inc",
  46. "Supermicro": r"Super Micro|Supermicro",
  47. "ASUS": r"^ASUSTek|^ASUS",
  48. "ASRock": r"ASRock",
  49. "Gigabyte": r"GIGA-BYTE|Gigabyte",
  50. "MSI": r"Micro-Star",
  51. "Lenovo": r"^Lenovo",
  52. "Samsung": r"^Samsung Electro|^Samsung Electronics",
  53. "LG": r"^LG Electronics",
  54. "Sony": r"^Sony ",
  55. "Google": r"^Google, Inc|^Google LLC",
  56. "Amazon": r"^Amazon Technologies",
  57. "Sonos": r"^Sonos",
  58. "Signify (Hue)": r"Signify|Philips Lighting",
  59. "Philips": r"^Philips",
  60. "Xiaomi": r"Xiaomi|XIAOMI",
  61. "IKEA": r"IKEA|Inter IKEA",
  62. "Aqara": r"Lumi United|Aqara",
  63. "Reolink": r"Reolink",
  64. "Hikvision": r"Hikvision|HIKVISION",
  65. "Dahua": r"Dahua",
  66. "Axis": r"^Axis Communication",
  67. "AVM (Fritz!Box)": r"^AVM ",
  68. "Juniper": r"^Juniper",
  69. "Fortinet": r"^Fortinet",
  70. "Netgate": r"Netgate",
  71. "Seagate": r"^Seagate",
  72. "Western Digital": r"Western Digital",
  73. "Buffalo": r"^BUFFALO|^Buffalo",
  74. "Asustor": r"ASUSTOR|Asustor",
  75. "TerraMaster": r"TerraMaster|Terra-?Master",
  76. "Sophos": r"^Sophos",
  77. "Arris/CommScope": r"^ARRIS|CommScope",
  78. "Technicolor": r"Technicolor",
  79. "Sagemcom": r"Sagemcom",
  80. "Roku": r"^Roku",
  81. "Nintendo": r"^Nintendo",
  82. "Texas Instruments": r"^Texas Instruments",
  83. "Tuya": r"^Tuya|Hangzhou Tuya",
  84. "Shelly": r"Allterco|Shelly",
  85. "Zotac": r"ZOTAC",
  86. "Beelink": r"Beelink|AZW",
  87. "Minisforum": r"Minisforum|MINIX",
  88. "Ring": r"^Ring LLC|Ring Inc",
  89. "Arlo": r"^Arlo",
  90. "Wyze": r"^Wyze",
  91. "Sonoff": r"ITEAD|SONOFF",
  92. "Mellanox": r"Mellanox",
  93. "Chelsio": r"Chelsio",
  94. "Aquantia": r"Aquantia",
  95. "Edimax": r"Edimax|EDIMAX",
  96. "Tenda": r"^Tenda|Shenzhen Tenda",
  97. "Huawei": r"^HUAWEI|^Huawei",
  98. "Sercomm": r"^Sercomm",
  99. "Silicon Labs": r"Silicon Laborator",
  100. "Nordic Semiconductor": r"Nordic Semiconductor",
  101. "Pine64": r"^Pine ?64|PINE64",
  102. "Hardkernel (ODROID)": r"Hardkernel",
  103. "Radxa": r"^Radxa|Rockchip",
  104. "FriendlyELEC": r"FriendlyARM|FriendlyELEC",
  105. "Khadas": r"Khadas|Shenzhen Wesion",
  106. }
  107. # Prefixes that identify software but are not IEEE assignments, because hypervisors
  108. # mint addresses out of the locally-administered range rather than buying an OUI.
  109. # Without these a KVM guest would be reported as merely "randomised".
  110. EXTRAS = {
  111. "525400": "QEMU/KVM",
  112. "00163E": "Xen",
  113. }
  114. # Vendor index is stored as one printable char, so the table cannot exceed this.
  115. _FIRST_INDEX_CHAR = 33 # '!'
  116. _MAX_VENDORS = 126 - _FIRST_INDEX_CHAR
  117. def main(csv_path: str) -> int:
  118. names = sorted(set(VENDORS) | set(EXTRAS.values()))
  119. if len(names) > _MAX_VENDORS:
  120. sys.exit(f"{len(names)} vendors exceeds the {_MAX_VENDORS} a single index char can address.")
  121. index_of = {name: i for i, name in enumerate(names)}
  122. compiled = [(re.compile(pat), name) for name, pat in VENDORS.items()]
  123. seen: dict[str, str] = {}
  124. with open(csv_path, encoding="utf-8", errors="replace") as handle:
  125. for row in csv.DictReader(handle):
  126. prefix = (row.get("Assignment") or "").strip().upper()
  127. org = (row.get("Organization Name") or "").strip()
  128. if len(prefix) != 6 or prefix in seen:
  129. continue
  130. for pattern, name in compiled:
  131. if pattern.search(org):
  132. seen[prefix] = name
  133. break
  134. seen.update(EXTRAS)
  135. records = "".join(
  136. prefix + chr(_FIRST_INDEX_CHAR + index_of[name])
  137. for prefix, name in sorted(seen.items())
  138. )
  139. # 140 is a whole number of records, so each source line holds exactly 20 of them.
  140. chunks = [records[i:i + 140] for i in range(0, len(records), 140)]
  141. # The index alphabet starts at '!' and runs past '"' and '\', both of which have to
  142. # be escaped in the C# literal. Escaping changes only the source text; the string
  143. # the runtime builds still holds the original characters, so offsets stay correct.
  144. def escape(chunk: str) -> str:
  145. return chunk.replace("\\", "\\\\").replace('"', '\\"')
  146. literal = "\n".join(f' "{escape(chunk)}" +' for chunk in chunks)
  147. literal = literal.rstrip(" +") + ";"
  148. vendor_literal = ",\n".join(f' "{name}"' for name in names)
  149. out = f'''// <auto-generated>
  150. // Generated by generate-oui-table.py from the IEEE OUI registry
  151. // (https://standards-oui.ieee.org/oui/oui.csv). Do not edit by hand — add a
  152. // vendor pattern to the generator and re-run it instead.
  153. //
  154. // {len(seen)} assignments across {len(names)} vendors, packed as fixed-width
  155. // "PPPPPPv" records (6 hex prefix chars + one vendor index char), sorted so
  156. // {'MacVendorLookup'} can binary-search them without building a dictionary.
  157. // </auto-generated>
  158. namespace RackPeek.Domain.Discovery;
  159. internal static class MacVendorTable {{
  160. internal const int RecordLength = 7;
  161. internal const char FirstIndexChar = '{chr(_FIRST_INDEX_CHAR)}';
  162. internal static readonly string[] Vendors = [
  163. {vendor_literal}
  164. ];
  165. internal static readonly string Records =
  166. {literal}
  167. }}
  168. '''
  169. target = "RackPeek.Domain/Discovery/MacVendorTable.g.cs"
  170. with open(target, "w", encoding="utf-8") as handle:
  171. handle.write(out)
  172. print(f"Wrote {target}: {len(seen)} assignments, {len(names)} vendors, {len(records)} chars.")
  173. return 0
  174. if __name__ == "__main__":
  175. if len(sys.argv) != 2:
  176. sys.exit(__doc__)
  177. raise SystemExit(main(sys.argv[1]))