# LibreNMS Device Detection Algorithm **Analysis Date**: 2026-02-11 **LibreNMS Version**: Analyzed from ~/dev/librenms **Key Files**: `LibreNMS/Modules/Core.php`, `app/ConfigRepository.php` ## Overview LibreNMS uses a sophisticated two-pass detection system with YAML-based OS definitions. The algorithm balances speed (fast string matching) with accuracy (optional SNMP queries) while handling edge cases through generic fallbacks. ## Detection Orchestration ### Entry Point **File**: `LibreNMS/Modules/Core.php::detectOS()` (lines 154-201) ```php public static function detectOS(Device $device, bool $fetch = true): string ``` **Called from**: - `discover()` method during device discovery (line 84) - `ValidateDeviceAndCreate` action during device creation - Optional `$fetch=true` parameter re-fetches SNMP data fresh ### SNMP Data Collection Three core SNMP OIDs are fetched at discovery: ```php $snmpdata = SnmpQuery::numeric()->get([ 'SNMPv2-MIB::sysObjectID.0', // System object identifier 'SNMPv2-MIB::sysDescr.0', // System description 'SNMPv2-MIB::sysName.0' // System name (hostname) ])->values(); ``` These values are stored in the Device model and used for all detection matching. ## Two-Pass Detection Flow ### Pass 1: Fast Detection (Immediate) **Goal**: Match devices using only fast string operations **Checks**: - `sysObjectID` (string prefix match) - `sysDescr` (substring contains) - `sysDescr_regex` (regex match) - `sysObjectID_regex` (regex match) **Excludes**: - Any OS with `snmpget` or `snmpwalk` conditions (deferred to Pass 2) - Generic fallback OSes: `airos`, `freebsd`, `linux` **Returns**: Immediately on first match ```php // PASS 1: Check fast matching foreach ($os_defs as $os => $def) { if (isset($def['discovery']) && !in_array($os, $generic_os)) { if (self::discoveryIsSlow($def)) { $deferred_os[] = $os; // Save for pass 2 continue; } foreach ($def['discovery'] as $item) { if (self::checkDiscovery($device, $item, $def['mib_dir'] ?? null)) { return $os; // Match found! } } } } ``` ### Pass 2: Slow Detection (If Pass 1 Fails) **Goal**: Match devices requiring SNMP queries or use generic fallbacks **Checks**: - All methods from Pass 1 - `snmpget` (active SNMP GET query) - `snmpwalk` (active SNMP WALK query) - Generic OSes: `airos`, `freebsd`, `linux` **Returns**: Immediately on first match ```php // PASS 2: Check slow matching + generic fallbacks $deferred_os = array_merge($deferred_os, $generic_os); foreach ($deferred_os as $os) { foreach ($os_defs[$os]['discovery'] as $item) { if (self::checkDiscovery($device, $item, $os_defs[$os]['mib_dir'] ?? null)) { return $os; } } } ``` ### Default Fallback If no OS matches in either pass: `return 'generic'` ## Matching Methods ### 1. sysObjectID (Prefix Match) ```php if ($key == 'sysObjectID') { if (Str::startsWith($device['sysObjectID'] ?? '', $value) == $check) { return false; } } ``` **Behavior**: Checks if sysObjectID **begins with** the specified value **Example** (ciscosb.yaml): ```yaml discovery: - sysObjectID: - .1.3.6.1.4.1.9.6.1. - .1.3.6.1.4.1.3955.6. ``` Matches any device whose sysObjectID starts with either prefix. ### 2. sysDescr (Substring Match) ```php if ($key == 'sysDescr') { if (Str::contains($device['sysDescr'] ?? '', $value) == $check) { return false; } } ``` **Behavior**: Checks if sysDescr **contains** the specified substring (case-sensitive) **Example** (ciscosb.yaml): ```yaml discovery: - sysDescr: - 'Catalyst 1200 Series' - 'Catalyst 1300 Series' ``` ### 3. sysDescr_regex (Regex Match) ```php if ($key == 'sysDescr_regex') { if (preg_match_any($device['sysDescr'] ?? '', $value) == $check) { return false; } } ``` **Behavior**: Checks if sysDescr matches **any** of the provided regexes **Example** (truenas.yaml): ```yaml discovery: - sysDescr_regex: - '/freenas/i' # Case-insensitive - '/^(TrueNAS)(?!.*-SCALE).*$/i' # Anchored with negative lookahead ``` ### 4. sysObjectID_regex (Regex Match) Similar to sysDescr_regex but applied to sysObjectID. **Example** (ciscosb.yaml): ```yaml discovery: - sysObjectID_regex: - '/^.1.3.6.1.4.1.9.1.(1058|1059|1060|1061|1062|1063|1064|1176|1177)/' ``` ### 5. snmpget (Active SNMP Query - SLOW) ```php if ($key == 'snmpget') { $get_value = SnmpQuery::device($device) ->options($value['options'] ?? null) ->mibDir($value['mib_dir'] ?? $mibdir) ->get(isset($value['mib']) ? "{$value['mib']}::{$value['oid']}" : $value['oid']) ->value(); if (Compare::values($get_value, $value['value'], $value['op'] ?? 'contains') == $check) { return false; } } ``` **Behavior**: Performs live SNMP GET during detection **Configuration**: ```yaml snmpget: oid: SOME-MIB::someOid.1.2.3 mib: SOME-MIB # Optional: MIB name prefix value: expected_value # Value to match op: contains # Comparison operator (default: 'contains') options: ... # Optional: SNMP options mib_dir: ... # Optional: override MIB directory ``` **Comparison Operators**: - `=` (equals, loose) - `!=` (not equals, loose) - `==` (identical, strict) - `!==` (not identical, strict) - `>=`, `<=`, `>`, `<` (numeric) - `contains` (substring match, default) - `starts`, `ends` (prefix/suffix) - `regex` (regex match) **Example** (airos.yaml): ```yaml discovery: - sysObjectID: - .1.3.6.1.4.1.10002.1 - .1.3.6.1.4.1.41112.1.4 sysDescr: Linux snmpget: oid: UI-AF60-MIB::af60Role.1 op: '=' value: false ``` ### 6. snmpwalk (Walk OID Tree - SLOW) ```php if ($key == 'snmpwalk') { $walk_value = SnmpQuery::device($device) ->options($value['options'] ?? null) ->mibDir($value['mib_dir'] ?? $mibdir) ->walk(isset($value['mib']) ? "{$value['mib']}::{$value['oid']}" : $value['oid']) ->raw; if (Compare::values($walk_value, $value['value'], $value['op'] ?? 'contains') == $check) { return false; } } ``` **Behavior**: Performs live SNMP WALK, checks if any returned value matches **Configuration**: Similar to snmpget but traverses OID subtree ## Precedence Rules ### Detection Order (Pass 1) All OS definitions are checked in **configuration iteration order** (depends on filesystem order or explicit config). The first matching OS is returned. ### Generic Fallback OSes (Pass 2) Three "generic" OSes are deferred to Pass 2: - `airos` (Ubiquiti AirOS - common generic wireless) - `freebsd` (FreeBSD - common generic server) - `linux` (Linux - common generic server) These are checked **after all specific vendors** fail, preventing them from matching too broadly. ### Example Precedence (Simplified) 1. All specific vendor OSes (cisco, juniper, dell, etc.) - FAST methods only 2. All specific vendor OSes with SLOW methods (snmpget/snmpwalk) 3. Generic OSes: airos, freebsd, linux - FAST & SLOW methods 4. Default: 'generic' OS ## Exception Patterns (_except Suffix) Conditions can be negated by appending `_except` to the key: ```php if ($check = Str::endsWith($key, '_except')) { $key = substr((string) $key, 0, -7); // Remove '_except' suffix } // ... perform the check, then invert the result ... if (Str::startsWith($device['sysObjectID'] ?? '', $value) == $check) { return false; // If $check=true (except), invert the logic } ``` **Logic**: When `_except` is present, normal match result is **inverted** **Example** (ciscosb.yaml): ```yaml discovery: - sysObjectID: - .1.3.6.1.4.1.9.6.1. - .1.3.6.1.4.1.3955.6. sysObjectID_except: - .1.3.6.1.4.1.9.6.1.23.1.1.1 # ciscospa (exclude) - .1.3.6.1.4.1.9.6.1.31. # ciscowap (exclude) ``` **Meaning**: "Match if sysObjectID **starts with** these prefixes **BUT NOT** these specific ones" ## Condition Composition ### Within a Discovery Item (AND Logic) All conditions in a discovery item are **AND-ed together**: ```yaml discovery: - sysObjectID: - .1.3.6.1.4.1.10002.1 sysDescr: Linux snmpget: oid: UI-AF60-MIB::af60Role.1 op: '=' value: false ``` **All must be true**: 1. sysObjectID starts with `.1.3.6.1.4.1.10002.1` 2. sysDescr contains "Linux" 3. SNMP GET of UI-AF60-MIB::af60Role.1 equals false ### Multiple Discovery Items (OR Logic) An OS can have multiple discovery items. **Any one item matching** causes the OS to be detected: ```yaml discovery: - sysObjectID: # First item - .1.3.6.1.4.1.40482 sysDescr_regex: - '/Pure.*Storage/i' - sysDescr_regex: # Second item (alternative) - '/FlashArray/i' ``` **Logic**: "Match if (first item passes) OR (second item passes)" ## YAML Configuration Structure Complete structure of an OS definition YAML file: ```yaml os: airos # OS identifier (must match filename) text: 'Ubiquiti AirOS' # Display name type: wireless # Device type icon: ubiquiti # Icon name mib_dir: ubnt # Custom MIB directory override snmp_bulk: false # Disable SNMP bulk gets over: - { graph: device_bits, text: 'Device Traffic' } # Graphs to override - { graph: device_wireless_clients, text: 'Connected Clients' } poller_modules: # Per-device polling modules ntp: false ospf: false discovery_modules: # Per-device discovery modules bgp-peers: false stp: false discovery: # Detection rules (multiple items, OR logic) - sysObjectID: # First detection method - .1.3.6.1.4.1.10002.1 - .1.3.6.1.4.1.41112.1.4 sysDescr: Linux snmpget: oid: UI-AF60-MIB::af60Role.1 op: '=' value: false - sysDescr_regex: # Alternative detection method - '/AirOS/' ``` ## Real-World Examples ### Example 1: Cisco Small Business (Fast + Exceptions) ```yaml os: ciscosb discovery: - sysObjectID: - .1.3.6.1.4.1.9.6.1. - .1.3.6.1.4.1.3955.6. sysObjectID_except: # Exclude other Cisco products - .1.3.6.1.4.1.9.6.1.23.1.1.1 - .1.3.6.1.4.1.9.6.1.31. ``` **Matches**: Cisco OID but not VoIP (23.1.1.1) or WAP (31) ### Example 2: Linux (Generic, Multiple Methods) ```yaml os: linux discovery: - sysObjectID: .1.3.6.1.4.1.8072.3.2.10 sysDescr_regex_except: - '/^USG/' # Exclude Ubiquiti USG - sysDescr_regex: '/^Linux/' # Fallback: simple regex match ``` **Matches**: Linux SNMP OID (except USG), or any sysDescr starting with "Linux" ### Example 3: AirOS (Slow Detection) ```yaml os: airos discovery: - sysObjectID: - .1.3.6.1.4.1.10002.1 - .1.3.6.1.4.1.41112.1.4 sysDescr: Linux snmpget: # SLOW method oid: UI-AF60-MIB::af60Role.1 op: '=' value: false ``` **Matches**: Ubiquiti OID + Linux sysDescr + specific SNMP value (deferred to pass 2) ## Performance Characteristics ### Pass 1 Speed - Pure string operations (startsWith, contains, regex) - No network I/O - Typical: <1ms per device - Handles 95%+ of devices ### Pass 2 Speed - Includes SNMP queries (network latency) - Typical: 50-500ms per device - Used for ambiguous devices - Falls back to generic OSes ### Configuration Loading - YAML files parsed at startup - 786 OS definitions loaded into memory - One-time cost: ~1-2 seconds ## Debugging Detection ```php Log::debug("| $device->sysDescr | $device->sysObjectID | \n"); ``` This is logged during `detectOS()` to help troubleshoot detection issues. ## Key Design Insights 1. **Two-pass strategy** balances speed (fast string matching) with accuracy (conditional queries) 2. **Generic fallbacks** prevent overly broad matches while catching edge cases 3. **Exception patterns** reduce YAML duplication for vendor-specific exclusions 4. **Composition via OR/AND** allows flexible matching without complex nesting 5. **MIB directory overrides** support vendor-specific MIB locations 6. **Operator flexibility** in snmpget/snmpwalk enables rich conditional logic ## Total Detection Profiles **Current Count**: 786 OS detection YAML files in `resources/definitions/os_detection/`