A Go library for looking up MAC address manufacturers and OUI (Organizationally Unique Identifier) information from IEEE listings.
Recommended for production: Use local file sources (download listings with DownloadListingToDir()). Online mode is available but not recommended for production use as it makes direct requests to IEEE servers.
- MAC Address Lookup: Look up manufacturer information from MAC addresses or OUIs (3-byte prefixes)
- Company Suggestions: Fuzzy matching to find company names
- Reverse Lookup: Find all OUIs assigned to a company
- Multiple Data Sources: Support for local files and online IEEE listings
- Multiple Listings: Support for MA-L (OUI-24), MA-M (OUI-28), MA-S (OUI-36), and CID (Company ID)
- Fuzzy Matching: Heuristic matching with configurable scoring algorithms
go get github.com/network-plane/planouiFor production applications, create a Resolver once at startup and reuse it.
Why this is efficient:
- The OUI data is loaded and indexed once when you create the resolver (reads from local files or online)
- All subsequent lookups are fast in-memory operations (no file I/O or network requests after initial load)
- The
LookupMAC()function automatically performs hierarchical lookup in a single call
How hierarchical lookup works:
When you call LookupMAC() with a full MAC address, it automatically:
- Checks MA-S (5-byte OUI, 10 hex chars) - most specific
- Falls back to MA-M (4-byte OUI, 8 hex chars) if not found
- Falls back to MA-L (3-byte OUI, 6 hex chars) if not found
This happens automatically in a single function call - you don't need to check each listing manually. Just load all three listings when creating the resolver, and LookupMAC() handles the rest.
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/network-plane/planoui"
)
type App struct {
resolver *planeoui.Resolver
}
func NewApp(dataDir string) (*App, error) {
ctx := context.Background()
// Load all three listings for most accurate lookups
sources := map[planeoui.Listing]planeoui.DataSource{
planeoui.ListingMAL: planeoui.LocalListing(dataDir, planeoui.ListingMAL),
planeoui.ListingMAM: planeoui.LocalListing(dataDir, planeoui.ListingMAM),
planeoui.ListingMAS: planeoui.LocalListing(dataDir, planeoui.ListingMAS),
}
loadOpts := planeoui.LoadOptions{
Listing: planeoui.ListingMAL,
NormalizeCompanies: true,
BuildReverseIndex: true,
}
// Create resolver once - loads and indexes all data from all three listings
resolver, err := planeoui.NewMultiResolver(ctx, sources, loadOpts)
if err != nil {
return nil, fmt.Errorf("failed to create resolver: %w", err)
}
return &App{resolver: resolver}, nil
}
func (a *App) LookupMAC(mac string) {
reply, err := a.resolver.LookupMAC(mac)
if err != nil {
if err == planeoui.ErrNotFound {
fmt.Printf("%s: Not found\n", mac)
return
}
log.Printf("Error: %v", err)
return
}
fmt.Printf("%s -> %s\n", mac, reply.Manufacturer)
}
func main() {
dataDir := os.Getenv("OUI_DATA_DIR")
if dataDir == "" {
dataDir = "./data" // default
}
app, err := NewApp(dataDir)
if err != nil {
log.Fatal(err)
}
// Now you can do many lookups efficiently without reloading data
// Each LookupMAC() call automatically checks MA-S → MA-M → MA-L in a single function call
app.LookupMAC("00:15:5d")
app.LookupMAC("aa:bb:cc:dd:ee:ff")
app.LookupMAC("001122")
}LookupMACWithSource, SuggestCompaniesWithSource, OUIsForCompanyWithSource) create a new resolver each time they're called. This means:
- They reload and re-index all OUI data on every call
- This is inefficient for multiple lookups
- They make file I/O or network requests on every call
Use these only for truly one-off lookups. For any application that performs multiple lookups, use the Resolver pattern instead (create once, reuse many times).
Always prefer local files for production use:
ctx := context.Background()
// Use local files (recommended)
src := planeoui.LocalListing("./data", planeoui.ListingMAL)
loadOpts := planeoui.LoadOptions{
Listing: planeoui.ListingMAL,
NormalizeCompanies: true,
BuildReverseIndex: true,
}
// One-off lookup - creates resolver internally
reply, err := planeoui.LookupMACWithSource(ctx, "00:15:5d:e3:70:02", src, loadOpts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("OUI: %s\nManufacturer: %s\n", reply.OUI, reply.Manufacturer)
fmt.Printf("Listing: %s\nSource: %s\n", reply.Listing, reply.SourceType)A common pattern is to configure the data directory via environment variables. Always prefer local files for production:
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/network-plane/planoui"
)
func initResolver() (*planeoui.Resolver, error) {
ctx := context.Background()
// Get data directory from environment or use default
dataDir := os.Getenv("OUI_DATA_DIR")
if dataDir == "" {
dataDir = "./data"
}
// Use local files (recommended for production)
// Online mode should only be used for testing/development
sources := map[planeoui.Listing]planeoui.DataSource{
planeoui.ListingMAL: planeoui.LocalListing(dataDir, planeoui.ListingMAL),
planeoui.ListingMAM: planeoui.LocalListing(dataDir, planeoui.ListingMAM),
planeoui.ListingMAS: planeoui.LocalListing(dataDir, planeoui.ListingMAS),
}
loadOpts := planeoui.LoadOptions{
Listing: planeoui.ListingMAL,
NormalizeCompanies: true,
BuildReverseIndex: true,
}
return planeoui.NewMultiResolver(ctx, sources, loadOpts)
}
func main() {
resolver, err := initResolver()
if err != nil {
log.Fatal(err)
}
// Use resolver for all lookups
reply, err := resolver.LookupMAC("00:15:5d")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s (from %s)\n", reply.Manufacturer, reply.Listing)
}Local files are the recommended approach for production use. Download listings using DownloadListingToDir() first, then use LocalListing() to read them:
// Load from ./data/ma-l.oui
src := planeoui.LocalListing("./data", planeoui.ListingMAL)WARNING: OnlineListing() makes direct requests to IEEE servers and should only be used occasionally. The package will print warnings to stderr when online mode is used.
For production use, always download listings locally first using DownloadListingToDir().
import "net/http"
// Use default HTTP client (will show warnings)
src := planeoui.OnlineListing(planeoui.ListingMAL, nil)
// Or use a custom HTTP client with timeout
client := &http.Client{
Timeout: 30 * time.Second,
}
src := planeoui.OnlineListing(planeoui.ListingMAL, client)Note: When OnlineListing() is called, the package automatically prints warnings to stderr about production usage. These warnings appear once per process.
Download listings from IEEE to local files:
ctx := context.Background()
opts := planeoui.DownloadOptions{
Atomic: true,
FileMode: 0644,
}
path, meta, err := planeoui.DownloadListingToDir(ctx, planeoui.ListingMAL, "./data", opts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Downloaded to: %s\n", path)
fmt.Printf("Size: %d bytes\n", meta.ByteSize)
fmt.Printf("SHA256: %s\n", meta.SHA256)Look up manufacturer information from MAC addresses. The library accepts both full MAC addresses and just the OUI (first 3 bytes):
resolver, _ := planeoui.NewResolver(ctx, src, loadOpts)
// Full MAC address
reply, err := resolver.LookupMAC("00:15:5d:e3:70:02")
// Just OUI (3 bytes)
reply, err := resolver.LookupMAC("00:15:5d")
// Various formats supported
reply, err := resolver.LookupMAC("00-15-5d")
reply, err := resolver.LookupMAC("00155d")Find company names using fuzzy matching:
opts := planeoui.SuggestOptions{
MaxResults: 10,
MaxDistance: 2,
UseHybridScoring: true,
MinScore: 0.3,
}
reply, err := resolver.SuggestCompanies("apple", opts)
if err != nil {
log.Fatal(err)
}
for _, suggestion := range reply.Suggestions {
fmt.Printf("%s (score: %.3f)\n", suggestion.Name, suggestion.Score)
}Get all OUIs assigned to a company:
// Exact match (normalized)
opts := planeoui.MatchOptions{
Heuristic: false,
}
reply, err := resolver.OUIsForCompany("Microsoft Corporation", opts)
// Heuristic/fuzzy match
opts = planeoui.MatchOptions{
Heuristic: true,
MaxDistance: 2,
MinScore: 0.5,
}
reply, err := resolver.OUIsForCompany("microsoft", opts)
for _, match := range reply.Matches {
fmt.Printf("OUI: %s - %s\n", match.OUI, match.Manufacturer)
}For the most accurate MAC address lookups, use all three listings (MA-L, MA-M, MA-S). The lookup function performs hierarchical matching:
- MA-S (OUI-36): 5-byte prefix - most specific assignment
- MA-M (OUI-28): 4-byte prefix - medium specificity
- MA-L (OUI-24): 3-byte prefix - least specific, but most common
When you provide a full MAC address, the library automatically tries the most specific match first, falling back to less specific matches. This ensures you get the most accurate manufacturer information available.
ctx := context.Background()
// Load all three listings for comprehensive lookups
sources := map[planeoui.Listing]planeoui.DataSource{
planeoui.ListingMAL: planeoui.LocalListing("./data", planeoui.ListingMAL),
planeoui.ListingMAM: planeoui.LocalListing("./data", planeoui.ListingMAM),
planeoui.ListingMAS: planeoui.LocalListing("./data", planeoui.ListingMAS),
}
loadOpts := planeoui.LoadOptions{
Listing: planeoui.ListingMAL, // Used as default for company lookups
NormalizeCompanies: true,
BuildReverseIndex: true,
}
resolver, err := planeoui.NewMultiResolver(ctx, sources, loadOpts)
if err != nil {
log.Fatal(err)
}
// Now lookups will check MA-S, then MA-M, then MA-L automatically
reply, err := resolver.LookupMAC("00:15:5d:e3:70:02")
if err != nil {
log.Fatal(err)
}
// Returns the most specific match found (MA-S if available, otherwise MA-M, otherwise MA-L)
fmt.Printf("Manufacturer: %s\n", reply.Manufacturer)
fmt.Printf("Listing: %s (accuracy: %s)\n", reply.Listing, getAccuracyLevel(reply.Listing))
fmt.Printf("Source: %s from %s\n", reply.SourceType, reply.SourceID)
// Helper function to show accuracy level
func getAccuracyLevel(listing planeoui.Listing) string {
switch listing {
case planeoui.ListingMAS:
return "highest (5-byte OUI)"
case planeoui.ListingMAM:
return "high (4-byte OUI)"
case planeoui.ListingMAL:
return "standard (3-byte OUI)"
default:
return "unknown"
}
}Why use all three? Some devices have more specific OUI assignments in MA-S or MA-M that provide more accurate manufacturer information than the generic 3-byte OUI in MA-L. For example, a device might be assigned a 5-byte OUI in MA-S that identifies it as a specific product line, while the 3-byte OUI only identifies the parent company.
The library gracefully handles missing data files when using NewMultiResolver. If some listings are missing (e.g., you only have ma-l.oui downloaded but not ma-m.oui or ma-s.oui), the resolver will:
- Load successfully with whatever data is available
- Continue operation - lookups will work with the available listings
- Hierarchical lookup still works - it will check available listings in order (MA-S → MA-M → MA-L)
// This will succeed even if ma-m.oui and ma-s.oui are missing
sources := map[planeoui.Listing]planeoui.DataSource{
planeoui.ListingMAL: planeoui.LocalListing("./data", planeoui.ListingMAL),
planeoui.ListingMAM: planeoui.LocalListing("./data", planeoui.ListingMAM), // Missing file - skipped
planeoui.ListingMAS: planeoui.LocalListing("./data", planeoui.ListingMAS), // Missing file - skipped
}
resolver, err := planeoui.NewMultiResolver(ctx, sources, loadOpts)
// Succeeds with just MA-L data
// Lookups will work, but only with 3-byte OUI matchesNote: If you use NewResolver with a single source and that file is missing, it will return an error since you explicitly requested that listing. For NewMultiResolver, at least one source must load successfully, otherwise an error is returned.
For one-off lookups without creating a resolver. Note: These create a new resolver internally each time, so they're inefficient for multiple calls. Use the Resolver pattern for production applications.
// MAC lookup (creates resolver internally)
reply, err := planeoui.LookupMACWithSource(ctx, "00:15:5d", src, loadOpts)
// Company suggestions (creates resolver internally)
reply, err := planeoui.SuggestCompaniesWithSource(ctx, "apple", src, loadOpts, suggestOpts)
// OUIs for company (creates resolver internally)
reply, err := planeoui.OUIsForCompanyWithSource(ctx, "Microsoft", src, loadOpts, matchOpts)Performance Note: If you need to perform multiple lookups, create a Resolver once and reuse it. The convenience functions are only suitable for single lookups or scripts.
Get results as JSON:
jsonData, err := resolver.LookupMACJSON("00:15:5d")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonData))
// {"oui":"00155D","manufacturer":"Microsoft Corporation","listing":"ma-l","source_type":"local-file","source_id":"./data/ma-l.oui"}The ManufacturerReply includes metadata about the lookup:
listing: Which IEEE listing was used (ma-s,ma-m,ma-l, orcid)source_type: Where the data came from (local-fileorhttp)source_id: The file path or URL of the data source
This helps you understand:
- Accuracy level:
ma-s(5-byte OUI) is more specific thanma-l(3-byte OUI) - Data freshness: Whether you're using local cached data or online data
- Data source: Which file or URL provided the information
ListingMAL("ma-l"): MA-L / OUI-24 (most common, 3-byte prefixes)ListingMAM("ma-m"): MA-M / OUI-28 (4-byte prefixes)ListingMAS("ma-s"): MA-S / OUI-36 (5-byte prefixes)ListingCID("cid"): Company ID
Recommendation: For production applications, load all three listings (MA-L, MA-M, MA-S) using NewMultiResolver to get the most accurate lookup results. The lookup function automatically performs hierarchical matching, checking the most specific assignment first.
The package defines a sentinel error for not found cases:
reply, err := resolver.LookupMAC("00:00:00")
if err != nil {
if err == planeoui.ErrNotFound {
fmt.Println("OUI not found in database")
} else {
log.Fatal(err)
}
}The package includes a command-line tool for quick lookups and downloads.
go build ./cmd/planouiDownload listings from IEEE to a local directory:
# Download a single listing
./planoui download ma-l --data-dir ./data
# Download multiple listings
./planoui download ma-l ma-m ma-s --data-dir ./data
# Download all listings
./planoui download all --data-dir ./data
# JSON output
./planoui download all --data-dir ./data --json# Using local files (recommended for production)
./planoui lookup 00:15:5d --data-dir ./data
# Just OUI (3 bytes) works too
./planoui lookup 00:15:5d --data-dir ./data
# Full MAC address
./planoui lookup 00:15:5d:e3:70:02 --data-dir ./data
# JSON output
./planoui lookup 00:15:5d --data-dir ./data --json
# Online mode (NOT recommended - shows warnings)
./planoui lookup 00:15:5d --online# Basic suggestion (using local files)
./planoui suggest "apple" --data-dir ./data
# With options
./planoui suggest "apple" --data-dir ./data --max-results 5 --hybrid --min-score 0.5
# Online mode (NOT recommended - shows warnings)
./planoui suggest "microsoft" --online# Exact match
./planoui ouis "Microsoft Corporation" --data-dir ./data
# Heuristic/fuzzy match
./planoui ouis "microsoft" --data-dir ./data --heuristic
# With scoring options
./planoui ouis "apple" --data-dir ./data --heuristic --min-score 0.6--data-dir string: Directory containing .oui files (required, useplanoui downloadto get listings)--listing string: IEEE listing to use: ma-l, ma-m, ma-s, or cid (default "ma-l")--online: Use online mode (NOT recommended for production - makes direct requests to IEEE servers, shows warnings)--json: Output results as JSON
suggest:
-n, --max-results int: Maximum number of suggestions (default 10)-D, --max-distance int: Maximum edit distance for fuzzy matching (default 2)-s, --min-score float: Minimum similarity score 0.0-1.0 (default 0.0)--hybrid: Use hybrid scoring method
ouis:
-H, --heuristic: Use heuristic/fuzzy matching instead of exact match-D, --max-distance int: Maximum edit distance for fuzzy matching (default 2)-s, --min-score float: Minimum similarity score 0.0-1.0 (default 0.5)
[Add your license here]
[Add contribution guidelines here]