GSMLG.MAC
Fast, compile-time MAC address vendor lookup library for Elixir
GSMLG.MAC provides efficient MAC (Media Access Control) address utilities including vendor/manufacturer lookup based on the IEEE OUI (Organizationally Unique Identifier) database. The library uses Wireshark's manufacturer database and compiles it into efficient pattern-matching code at compile time for zero-cost lookups.
Features
- Lightning-fast vendor lookups - Compile-time database compilation for zero runtime overhead
- Comprehensive MAC utilities - Validate, normalize, format, and parse MAC addresses
- Multiple format support - Handles colons, hyphens, dots, and no separators
- Telemetry integration - Built-in observability with GSMLG.Telemetry
- Type-safe API - Full typespec coverage
- Zero dependencies (except optional telemetry)
Installation
Add gsmlg_mac to your list of dependencies in mix.exs:
def deps do
[
{:gsmlg_mac, "~> 0.1.0"}
]
end
Database Setup
The library requires Wireshark's manufacturer database file. Download it:
# From your project root
mkdir -p apps/gsmlg_mac/priv
curl -sSLf https://gitlab.com/wireshark/wireshark/-/raw/master/manuf \
-o apps/gsmlg_mac/priv/manuf.txt
Note: The database is compiled at compile-time. After updating manuf.txt, you must recompile:
mix deps.clean gsmlg_mac --build
mix compile
Quick Start
# Lookup vendor by MAC address
iex> GSMLG.MAC.lookup_vendor("00:00:0A:BB:28:FC")
{:ok, "OmronTat", "Omron Tateisi Electronics Co."}
# Validate MAC address format
iex> GSMLG.MAC.validate("00:1A:2B:3C:4D:5E")
true
# Normalize MAC to standard format
iex> GSMLG.MAC.normalize("00-1a-2b-3c-4d-5e")
{:ok, "00:1A:2B:3C:4D:5E"}
# Format MAC with different separators
iex> GSMLG.MAC.format("001a2b3c4d5e", :hyphens)
{:ok, "00-1A-2B-3C-4D-5E"}
# Extract OUI (first 24 bits)
iex> GSMLG.MAC.parse_oui("00:1A:2B:3C:4D:5E")
{:ok, "00:1A:2B"}
# Generate random MAC address
iex> GSMLG.MAC.random()
"A3:4F:12:8B:C9:7E"
Usage
Vendor Lookup
Look up the manufacturer/vendor of a MAC address using the IEEE OUI database:
# Standard colon format
GSMLG.MAC.lookup_vendor("00:00:0A:BB:28:FC")
#=> {:ok, "OmronTat", "Omron Tateisi Electronics Co."}
# Hyphen format
GSMLG.MAC.lookup_vendor("00-50-56-C0-00-08")
#=> {:ok, "VMware", "VMware, Inc."}
# Dot format (Cisco)
GSMLG.MAC.lookup_vendor("0050.56C0.0008")
#=> {:ok, "VMware", "VMware, Inc."}
# No separators
GSMLG.MAC.lookup_vendor("005056C00008")
#=> {:ok, "VMware", "VMware, Inc."}
# Unknown vendor
GSMLG.MAC.lookup_vendor("FF:FF:FF:FF:FF:FF")
#=> :error
# Invalid format
GSMLG.MAC.lookup_vendor("invalid")
#=> :error
Returns:
{:ok, short_name, full_name}- Vendor found:error- Vendor not found or invalid MAC format
The lookup chooses the longest matching prefix, including /24, /28, and /36 entries sharing the same OUI. When a more specific prefix does not match, it falls back to a broader entry if one exists. The vendor database contains over 28,000+ entries from the IEEE registration authority and Wireshark project.
Operator-provided Wireshark manufacturer database contents can be compiled and queried without replacing the bundled database:
table = GSMLG.MAC.Compiler.build_lookup_table(File.read!("/etc/manuf.txt"))
GSMLG.MAC.Compiler.count_entries(table)
GSMLG.MAC.Vendor.lookup("00:11:22:33:40:00", table)
Tables with a single prefix width per OUI retain their existing format. Mixed widths use {:mixed, [{width, prefix_map}, ...]} buckets ordered longest first. Repeated identical prefixes keep the last record, as before.
MAC Address Validation
Check if a string is a valid MAC address format:
# Valid formats
GSMLG.MAC.validate("00:1A:2B:3C:4D:5E") #=> true
GSMLG.MAC.validate("00-1A-2B-3C-4D-5E") #=> true
GSMLG.MAC.validate("001A.2B3C.4D5E") #=> true
GSMLG.MAC.validate("001A2B3C4D5E") #=> true
# Invalid formats
GSMLG.MAC.validate("00:1A:2B:3C:4D") #=> false (too short)
GSMLG.MAC.validate("00:1A:2B:GG:4D:5E") #=> false (invalid hex)
GSMLG.MAC.validate("not-a-mac") #=> false
GSMLG.MAC.validate("") #=> false
Accepts any standard MAC address format with 48 bits (6 bytes).
MAC Address Normalization
Convert any MAC format to the standard uppercase colon-separated format:
# From hyphens
GSMLG.MAC.normalize("00-1a-2b-3c-4d-5e")
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# From dots (Cisco format)
GSMLG.MAC.normalize("001a.2b3c.4d5e")
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# From no separators
GSMLG.MAC.normalize("001a2b3c4d5e")
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# Already normalized (converts to uppercase)
GSMLG.MAC.normalize("00:1a:2b:3c:4d:5e")
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# Mixed formats (cleans up)
GSMLG.MAC.normalize("00:1A-2B.3C4D:5E")
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# Invalid MAC
GSMLG.MAC.normalize("invalid")
#=> {:error, :invalid_mac}
Normalization is useful for storing MACs in a database with consistent formatting.
MAC Address Formatting
Format a MAC address with different separator styles:
mac = "001A2B3C4D5E"
# Colon-separated (standard)
GSMLG.MAC.format(mac, :colons)
#=> {:ok, "00:1A:2B:3C:4D:5E"}
# Hyphen-separated
GSMLG.MAC.format(mac, :hyphens)
#=> {:ok, "00-1A-2B-3C-4D-5E"}
# Dot-separated, grouped by 2 (Cisco format)
GSMLG.MAC.format(mac, :dots)
#=> {:ok, "001A.2B3C.4D5E"}
# No separators
GSMLG.MAC.format(mac, :none)
#=> {:ok, "001A2B3C4D5E"}
# Cisco format (dots, grouped by 4 hex digits)
GSMLG.MAC.format(mac, :cisco)
#=> {:ok, "001A.2B3C.4D5E"}
# Invalid format style
GSMLG.MAC.format(mac, :unknown)
#=> {:error, :invalid_format}
# Invalid MAC
GSMLG.MAC.format("invalid", :colons)
#=> {:error, :invalid_mac}
Format options:
:colons-00:1A:2B:3C:4D:5E(default, most common):hyphens-00-1A-2B-3C-4D-5E(Windows style):dots-001A.2B3C.4D5E(Cisco style):cisco- Same as:dots:none-001A2B3C4D5E(compact)
OUI Parsing
Extract the OUI (Organizationally Unique Identifier) - the first 24 bits that identify the vendor:
# Standard format
GSMLG.MAC.parse_oui("00:1A:2B:3C:4D:5E")
#=> {:ok, "00:1A:2B"}
# Any format works
GSMLG.MAC.parse_oui("00-1A-2B-3C-4D-5E")
#=> {:ok, "00:1A:2B"}
GSMLG.MAC.parse_oui("001a2b3c4d5e")
#=> {:ok, "00:1A:2B"}
# Invalid MAC
GSMLG.MAC.parse_oui("invalid")
#=> {:error, :invalid_mac}
The OUI is always returned in uppercase colon-separated format.
Random MAC Generation
Generate random MAC addresses for testing:
# Fully random MAC
GSMLG.MAC.random()
#=> "A3:4F:12:8B:C9:7E"
# Random MAC with specific OUI
GSMLG.MAC.random("00:1A:2B")
#=> "00:1A:2B:7C:3E:91"
# Random MAC with OUI from vendor
GSMLG.MAC.random("00-50-56") # VMware OUI
#=> "00:50:56:A2:B8:3F"
# Invalid OUI
GSMLG.MAC.random("invalid")
#=> {:error, :invalid_oui}
Useful for generating test data or temporary MAC addresses.
Telemetry
GSMLG.MAC emits telemetry events when integrated with GSMLG.Telemetry. To enable telemetry, add the dependency:
def deps do
[
{:gsmlg_mac, "~> 0.1.0"},
{:gsmlg_telemetry, "~> 0.1.0"}
]
end
Events
[:gsmlg, :mac, :lookup]
Emitted when performing vendor lookups.
Metadata:
:mac- The MAC address being looked up:oui- Extracted OUI (first 24 bits):found- Boolean indicating if vendor was found:vendor_short- Short vendor name (if found):vendor_full- Full vendor name (if found)
Measurements:
:duration- Lookup duration in native time units
Example handler:
:telemetry.attach(
"mac-lookup-handler",
[:gsmlg, :mac, :lookup],
fn event, measurements, metadata, _config ->
IO.puts("MAC lookup: #{metadata.mac} -> #{metadata.found}")
end,
nil
)
[:gsmlg, :mac, :operation]
Emitted for utility operations (validate, normalize, format, etc.).
Metadata:
:operation- Operation name (:validate,:normalize,:format,:parse_oui,:random):mac- Input MAC address:result- Operation result (:okor:error)
Measurements:
:duration- Operation duration in native time units
Supported MAC Formats
GSMLG.MAC automatically handles multiple MAC address formats:
| Format | Example | Description |
|---|---|---|
| Colon-separated | 00:1A:2B:3C:4D:5E |
Most common format (IEEE standard) |
| Hyphen-separated | 00-1A-2B-3C-4D-5E |
Windows style |
| Dot-separated | 001A.2B3C.4D5E |
Cisco style (4 hex digits per group) |
| No separators | 001A2B3C4D5E |
Compact format |
| Mixed case | 00:1a:2B:3c:4D:5e |
Accepts any case, normalizes to uppercase |
All formats are automatically recognized by all functions. The library normalizes internally.
Architecture & Performance
Compile-Time Database Compilation
GSMLG.MAC uses a unique compile-time approach for maximum performance:
- Build Time: The
manuf.txtdatabase is parsed and compiled into Elixir module attributes - Compile Time: The lookup table becomes pattern-matching code in the BEAM bytecode
- Runtime: Lookups are simple map lookups - effectively zero overhead
Performance characteristics:
- Vendor lookup: O(1) - constant time map lookup
- Memory: ~2-3MB for compiled lookup table (28,000+ entries)
- Startup: Zero - no runtime database loading
- Lookup latency: ~0.1-0.5 microseconds (essentially free)
This is significantly faster than:
- Runtime parsing: ~100-1000x faster
- External API calls: ~10,000-100,000x faster
- File I/O on each lookup: ~1,000-10,000x faster
Trade-offs
Advantages:
- Blazing fast lookups (microseconds)
- No runtime dependencies
- No startup cost
- No file I/O or parsing overhead
- Type-safe with compile-time guarantees
Disadvantages:
- Requires recompilation to update database
- Increases compiled application size (~2-3MB)
- Database updates need redeployment
- Not suitable if database changes frequently
Best for:
- Applications where vendor database is relatively stable
- Performance-critical lookups
- Embedded systems with predictable MAC vendors
- CLI tools and scripts
- API services with high lookup volume
Not ideal for:
- Applications requiring frequent database updates
- When database must change without redeployment
- Extremely memory-constrained environments
Updating the Database
The MAC vendor database changes as new OUIs are assigned. To update:
Manual Update
# Download latest database
curl -sSLf https://gitlab.com/wireshark/wireshark/-/raw/master/manuf \
-o apps/gsmlg_mac/priv/manuf.txt
# Recompile the application
mix deps.clean gsmlg_mac --build
mix compile
# Verify entry count (should be 28,000+)
iex> GSMLG.MAC.Vendor.entries()
28542
Automated Update (CI/CD)
Add to your deployment pipeline:
#!/bin/bash
# update_mac_db.sh
echo "Updating MAC vendor database..."
curl -sSLf https://gitlab.com/wireshark/wireshark/-/raw/master/manuf \
-o apps/gsmlg_mac/priv/manuf.txt
echo "Recompiling with new database..."
mix deps.clean gsmlg_mac --build
mix compile
echo "Database updated successfully!"
Recommendation: Update quarterly or when you need specific new vendors.
Common Use Cases
1. Network Device Inventory
defmodule NetworkInventory do
def scan_devices(mac_addresses) do
Enum.map(mac_addresses, fn mac ->
case GSMLG.MAC.lookup_vendor(mac) do
{:ok, short, full} ->
%{mac: mac, vendor: short, manufacturer: full}
:error ->
%{mac: mac, vendor: "Unknown", manufacturer: "Unknown"}
end
end)
end
end
NetworkInventory.scan_devices([
"00:50:56:C0:00:08",
"00:1A:2B:3C:4D:5E",
"A4:5E:60:D8:9C:12"
])
2. MAC Address Validation in Ecto Schema
defmodule Device do
use Ecto.Schema
import Ecto.Changeset
schema "devices" do
field :mac_address, :string
field :vendor, :string
end
def changeset(device, attrs) do
device
|> cast(attrs, [:mac_address])
|> validate_required([:mac_address])
|> validate_mac_address()
|> populate_vendor()
end
defp validate_mac_address(changeset) do
validate_change(changeset, :mac_address, fn :mac_address, mac ->
if GSMLG.MAC.validate(mac) do
[]
else
[mac_address: "is not a valid MAC address"]
end
end)
end
defp populate_vendor(changeset) do
case get_change(changeset, :mac_address) do
nil -> changeset
mac ->
case GSMLG.MAC.lookup_vendor(mac) do
{:ok, _short, full} -> put_change(changeset, :vendor, full)
:error -> changeset
end
end
end
end
3. MAC Address Normalization for Storage
defmodule MACStore do
def store_device(mac, metadata) do
with {:ok, normalized_mac} <- GSMLG.MAC.normalize(mac),
{:ok, oui} <- GSMLG.MAC.parse_oui(normalized_mac) do
%{
mac: normalized_mac,
oui: oui,
vendor: lookup_vendor_safe(normalized_mac),
metadata: metadata,
inserted_at: DateTime.utc_now()
}
|> save_to_database()
else
{:error, _} -> {:error, :invalid_mac}
end
end
defp lookup_vendor_safe(mac) do
case GSMLG.MAC.lookup_vendor(mac) do
{:ok, _short, full} -> full
:error -> nil
end
end
end
4. Network Security Monitoring
defmodule SecurityMonitor do
@known_vendors ["Cisco", "VMware", "Dell", "HP"]
def check_rogue_device(mac) do
case GSMLG.MAC.lookup_vendor(mac) do
{:ok, vendor, _full} ->
if vendor in @known_vendors do
{:ok, :authorized}
else
{:warning, :unknown_vendor, vendor}
end
:error ->
{:alert, :unrecognized_mac}
end
end
end
5. API Endpoint (Phoenix)
defmodule MyAppWeb.MACController do
use MyAppWeb, :controller
def lookup(conn, %{"mac" => mac}) do
case GSMLG.MAC.lookup_vendor(mac) do
{:ok, short, full} ->
json(conn, %{
mac: mac,
vendor_short: short,
vendor_full: full,
success: true
})
:error ->
conn
|> put_status(:not_found)
|> json(%{mac: mac, error: "Vendor not found", success: false})
end
end
def validate(conn, %{"mac" => mac}) do
json(conn, %{
mac: mac,
valid: GSMLG.MAC.validate(mac)
})
end
end
Troubleshooting
Database file not found
Error: (File.Error) could not read file "...priv/manuf.txt": no such file or directory
Solution: Download the database file:
mkdir -p apps/gsmlg_mac/priv
curl -sSLf https://gitlab.com/wireshark/wireshark/-/raw/master/manuf \
-o apps/gsmlg_mac/priv/manuf.txt
mix deps.clean gsmlg_mac --build
mix compile
Vendor not found for valid MAC
Issue: lookup_vendor/1 returns :error for a legitimate MAC address.
Causes:
- OUI not in database (new vendor, private OUI)
- Outdated database
- MAC is randomly generated or spoofed
Solutions:
- Update database (see "Updating the Database" section)
- Check if MAC is from a newer vendor
- For private/local MACs (02:xx:xx:xx:xx:xx), vendor lookup won't work
Compilation is slow
Issue: Application takes longer to compile after adding gsmlg_mac.
Explanation: The library compiles ~28,000 vendor entries into pattern-matching code at compile time. This is a one-time cost.
Solutions:
- This is expected behavior for compile-time optimization
- Compilation happens once, runtime is extremely fast
- Consider using
mix compilecache in CI/CD - Typical compilation overhead: 2-5 seconds
MAC format not recognized
Issue: Valid MAC not being parsed correctly.
Solution: GSMLG.MAC accepts standard formats. Try normalizing first:
mac = "unusual:format:here"
case GSMLG.MAC.normalize(mac) do
{:ok, normalized} -> GSMLG.MAC.lookup_vendor(normalized)
{:error, _} -> # Handle invalid format
end
Comparison with Alternatives
| Approach | Speed | Memory | Updates | Dependencies |
|---|---|---|---|---|
| GSMLG.MAC | ★★★★★ | Medium | Recompile | None |
| Runtime parsing | ★★☆☆☆ | Low | Easy | None |
| ETS lookup | ★★★★☆ | Medium | Runtime | None |
| External API | ★☆☆☆☆ | Low | Always current | HTTP client |
| SQLite DB | ★★★☆☆ | Low | Easy | SQLite |
Why choose GSMLG.MAC:
- Need absolute fastest lookups
- Vendor database rarely changes
- Want zero runtime dependencies
- Prefer type safety and compile-time guarantees
Consider alternatives if:
- Database must update without redeployment
- Memory is extremely constrained
- Application rarely performs lookups
Contributing
To contribute or report issues:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
License
MIT License - see LICENSE file for details.
Resources
Credits
Vendor database sourced from the Wireshark project, which aggregates IEEE OUI assignments and additional manufacturer information.