Network Scanning

Simple PHP IPAM includes a built-in network scanner that probes IP addresses via ICMP ping or TCP connect. Scanning requires no daemons, no root privileges (for TCP mode), and no PHP extensions beyond the standard proc_open / fsockopen functions.


Tables

Two new tables are added by the 2.3.0-scanning migration:

TablePurpose
scan_schedulesPer-subnet scan configuration (method, interval, active flag, last run time)
scan_resultsOne row per IP per scan run — records up/down status and latency

Two new columns are added to addresses:

ColumnTypePurpose
last_seen_atTEXT (datetime)Timestamp of last successful ICMP/TCP response
is_staleINTEGER (0/1)Auto-set by ipam_mark_stale_addresses()

Scan methods

MethodHow it worksPrivilege required
icmpSystem ping binary via proc_open(), 1 packet, 1-second timeoutRoot / cap_net_raw on Linux; none on macOS
tcpfsockopen($ip, $port, timeout=1s)None
bothICMP first; falls back to TCP on ICMP permission errorDepends on ICMP availability

If ICMP probing fails with a permission error (exit code 2 on Linux), the scanner automatically falls back to TCP-only for that subnet.

What gets scanned

The scanner iterates over every address recorded in the addresses table for a subnet. Two classes of IP are never probed, even if they exist in the table:

  • Network address (first IP in the subnet). Reports of "up" on the network address are almost always false positives.
  • IPv4 broadcast address (last IP in the subnet, /30 and larger). Many hosts on the same link respond to broadcast ICMP, producing misleading up/down results.

Both are counted in the skipped stat on the scan summary. /31 (RFC 3021 point-to-point) and /32 (single host) have no reserved addresses — all IPs are probed. IPv6 has no broadcast concept, so only the network address is reserved.


Scan schedules

Schedules are managed via the subnet list UI (write role required) or the REST API.

UI: On subnets.php, each subnet row has a "Scan Schedule" <details> section. Expand it to configure:

  • Methodicmp, tcp, or both
  • TCP port — port to probe when method is tcp or both (default 443)
  • Interval — minutes between scan runs (minimum 1)
  • Active — enable or disable the schedule

API:

GET    /api.php?resource=scan_schedules               # list all schedules
GET    /api.php?resource=scan_schedules&subnet_id=N   # get one schedule (null if none)
POST   /api.php?resource=scan_schedules               # create or update (upsert)
DELETE /api.php?resource=scan_schedules&subnet_id=N   # remove schedule

CLI runner (cron)

cron.php is the unified housekeeping and scanning runner. A single cron entry covers all periodic tasks:

  • Temp file cleanup
  • Audit log pruning
  • Address history pruning
  • Subnet utilisation alerts
  • Database backup
  • Network scanning — all active scheduled subnets that are due
# Run every 15 minutes — housekeeping tasks throttle themselves internally;
# scanning honours the per-subnet interval set in the Scan Schedule UI.
*/15 * * * * php /path/to/Simple-PHP-IPAM/cron.php >> /var/log/ipam-cron.log 2>&1

The script outputs one JSON object per task (JSONL format). Each subnet scan emits an individual result object, followed by a scan_summary totals object. Exit code 0 on success, 1 if any task raised an exception.

Example output:

{"task":"tmp_cleanup","files_removed":0,"plans_removed":0,"ts":"2024-01-15T10:00:00+00:00"}
{"task":"prune_audit_log","skipped":true,"reason":"retention_days=0","ts":"..."}
{"task":"prune_address_history","skipped":true,"reason":"retention_days=0","ts":"..."}
{"task":"utilisation_alerts","skipped":true,"reason":"alert_email not configured","ts":"..."}
{"task":"db_backup","skipped":true,"reason":"retired_v3_26_0","ts":"..."}
{"task":"scan","subnet_id":1,"cidr":"10.0.0.0\/24","method":"icmp","tcp_port":null,"scanned":254,"up":12,"down":242,"stale_marked":3,"elapsed_sec":8.4,"ts":"..."}
{"task":"scan_summary","scanned_subnets":1,"total_hosts":254,"total_up":12,"total_down":242,"total_stale_marked":3,"ts":"..."}

Per-subnet / manual scanning — scan_run.php

scan_run.php is still available for one-off or per-subnet scans from the CLI. Running it from a web request returns HTTP 403.

# Scan all active scheduled subnets past their interval
php /path/to/Simple-PHP-IPAM/scan_run.php --all

# Scan a specific subnet immediately
php scan_run.php --subnet-id=42

# Dry run — show what would be scanned
php scan_run.php --all --dry-run

# Override method and stale threshold
php scan_run.php --all --method=tcp --port=22 --stale-threshold=5

The script outputs a JSON summary per subnet and an overall totals object. Exit code 0 on success, 1 on error.


Auto-stale detection

After each subnet scan, ipam_mark_stale_addresses() is called automatically. It:

  1. Counts the last N scan results per address in scan_results
  2. Sets addresses.is_stale = 1 for any address that has N consecutive down results (default threshold: 3)
  3. Clears is_stale = 0 for any address that now has an up result as its most recent scan

Stale addresses show a red "Stale" badge on the addresses.php page and are included in scan history. The is_stale flag is never set manually — it is always derived from scan result history.

To change the miss threshold globally, pass --stale-threshold=N to scan_run.php, or call ipam_mark_stale_addresses($db, $subnetId, $threshold) directly.


ARP table import

import_arp.php lets you bulk-update MAC addresses from a copy-pasted ARP table.

Supported formats:

# Space-separated (most common)
192.168.1.1 aa:bb:cc:dd:ee:ff

# Tab-separated
10.0.0.5    00:11:22:33:44:55

# CSV
10.0.0.10,aa-bb-cc-dd-ee-ff

# Linux arp -a style
router (192.168.1.1) at aa:bb:cc:dd:ee:ff [ether] on eth0

Lines starting with # are ignored. Lines with an invalid IP or invalid MAC are skipped silently.

Workflow:

  1. Navigate to Admin → ARP Import
  2. Select the target subnet
  3. Paste your ARP table into the textarea
  4. Click Preview — shows parsed entries with an "In subnet?" column
  5. Click Apply — updates addresses.mac for matching IPs, skips out-of-subnet entries
  6. A success flash shows how many MACs were updated

Each apply is audit logged as address.arp_import.


REST API — scan resources

See docs/api.md for full request/response examples.

MethodResourceAuthDescription
GETscan_results&subnet_id=NreadLast scan run results for a subnet
GETscan_history&subnet_id=N[&limit=50]readPaginated scan run history
POSTscan_run&subnet_id=NwriteTrigger immediate synchronous scan (max /28)
GETscan_schedulesreadList all schedules
GETscan_schedules&subnet_id=NreadGet schedule for one subnet (null if none)
POSTscan_scheduleswriteCreate or update a schedule (JSON body)
DELETEscan_schedules&subnet_id=NwriteRemove a schedule

Synchronous scan limit: scan_run is capped at /28 (16 IPs) to prevent HTTP request timeouts. For larger subnets, use the CLI runner.


Security notes

  • All IP addresses are validated through normalize_ip() (which calls inet_pton()) before being passed to proc_open() or fsockopen(). Raw $_GET/$_POST values never reach system calls.
  • scan_run.php rejects web requests at the SAPI check (php_sapi_name() !== 'cli').
  • The ipam-proc-open-safe Semgrep rule enforces this pattern across the codebase.