Packages

Fast compile-time MAC address vendor lookup and utilities

Current section

Files

Jump to
gsmlg_mac README.md
Raw

README.md

# 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`:
```elixir
def deps do
[
{:gsmlg_mac, "~> 0.1.0"}
]
end
```
### Database Setup
The library requires Wireshark's manufacturer database file. Download it:
```bash
# 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:
```bash
mix deps.clean gsmlg_mac --build
mix compile
```
## Quick Start
```elixir
# 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:
```elixir
# 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:
```elixir
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:
```elixir
# 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:
```elixir
# 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:
```elixir
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:
```elixir
# 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:
```elixir
# 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:
```elixir
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:**
```elixir
: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 (`:ok` or `: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:
1. **Build Time:** The `manuf.txt` database is parsed and compiled into Elixir module attributes
2. **Compile Time:** The lookup table becomes pattern-matching code in the BEAM bytecode
3. **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
```bash
# 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:
```bash
#!/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
```elixir
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
```elixir
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
```elixir
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
```elixir
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)
```elixir
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:
```bash
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:**
1. OUI not in database (new vendor, private OUI)
2. Outdated database
3. 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 compile` cache 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:
```elixir
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:
1. Fork the repository
2. Create a feature branch
3. Make your changes with tests
4. Submit a pull request
## License
MIT License - see LICENSE file for details.
## Resources
- [IEEE OUI Database](https://standards.ieee.org/develop/regauth/oui/)
- [Wireshark Manufacturer Database](https://gitlab.com/wireshark/wireshark/-/blob/master/manuf)
- [MAC Address Format Standards](https://standards.ieee.org/products-programs/regauth/)
## Credits
Vendor database sourced from the Wireshark project, which aggregates IEEE OUI assignments and additional manufacturer information.