Skip to content

Package to search into OUI lists and return manufactures (or the reverse), it is handling the downloading, etc for the data

License

Notifications You must be signed in to change notification settings

network-plane/planeoui

Repository files navigation

planeoui

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.

Features

  • 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

Installation

go get github.com/network-plane/planoui

Quick Start

Recommended Pattern: Create Resolver Once

For 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:

  1. Checks MA-S (5-byte OUI, 10 hex chars) - most specific
  2. Falls back to MA-M (4-byte OUI, 8 hex chars) if not found
  3. 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")
}

One-Off Lookups (Convenience Functions)

⚠️ Important: The convenience functions (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)

Configuration via Environment Variables

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)
}

Data Sources

Local Files (Recommended for Production)

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)

Online Listings (Not Recommended for Production)

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.

Downloading Listings

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)

Lookup Operations

MAC Address Lookup

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")

Company Name Suggestions

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)
}

Find OUIs for a Company

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)
}

Advanced Usage

Multi-Resolver for Accurate Lookups

For the most accurate MAC address lookups, use all three listings (MA-L, MA-M, MA-S). The lookup function performs hierarchical matching:

  1. MA-S (OUI-36): 5-byte prefix - most specific assignment
  2. MA-M (OUI-28): 4-byte prefix - medium specificity
  3. 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.

Handling Missing Data Files

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 matches

Note: 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.

Convenience Functions

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.

JSON Output

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, or cid)
  • source_type: Where the data came from (local-file or http)
  • 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 than ma-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

Available Listings

  • 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.

Error Handling

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)
    }
}

CLI Tool

The package includes a command-line tool for quick lookups and downloads.

Installation

go build ./cmd/planoui

Usage

Download OUI Listings

Download 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

Lookup MAC Address

# 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

Company Suggestions

# 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

Find OUIs for Company

# 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

Global Flags

  • --data-dir string: Directory containing .oui files (required, use planoui download to 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

Command-Specific Flags

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)

License

[Add your license here]

Contributing

[Add contribution guidelines here]

About

Package to search into OUI lists and return manufactures (or the reverse), it is handling the downloading, etc for the data

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages