Skip to main content

vlan-probe 🛡️

VLAN Isolation & Network Permission Probe Tool — verify your firewall rules by probing target VLAN subnets, IPs, and ports from the host, and get alerted to any unauthorized inter-VLAN access.

PyPI - Version PyPI - Python Versions PyPI - License CI/CD Coverage


✨ Features

  • Multi-protocol probingtcp, udp (DNS-aware on port 53, NTP-aware on port 123), icmp (ping), and sctp.
  • Policy-first configuration — declare what should be blocked vs. reachable; every deviation is reported as a violation 🔴.
  • Concurrent execution — multi-threaded worker pool (-j / --concurrency) to probe dozens of targets simultaneously.
  • Flexible output — human-friendly table, structured json, or streaming ndjson.
  • Alerting built-in--strict exit codes for automation, plus scheduled MQTT reporting via systemd timers 📡.
  • Boring & reliable — typed Python 3.10+, zero long-running daemons, 100% test coverage.

📑 Table of contents


Requirements

  • Python 3.10 or newer
  • Linux (uses the ip and ping utilities)

Installation

Install with pipx (recommended)

vlan-probe is a command-line tool — pipx runs it in an isolated environment while keeping the vlan-probe command on your PATH.

pipx install vlan-probe

Upgrade to a newer release:

pipx upgrade vlan-probe

Don't have pipx yet? Install it first:

brew install pipx && pipx ensurepath      # macOS/Homebrew
apt install pipx && pipx ensurepath       # Debian/Ubuntu
pacman -S python-pipx && pipx ensurepath  # Arch

Install from source

pipx install .          # via pipx
uv tool install .       # via uv
pip install .           # into the active environment

Quick start

# 1. copy the example config and edit it to match your network
cp vlan_probe.toml.example /etc/vlan_probe.toml

# 2. probe everything and show a human-friendly table
vlan-probe -f table

# 3. or go headless: JSON to stdout, exit 1 on any violation
vlan-probe -f json -s

Configuration

Configuration lives in a single TOML file (or JSON file) — readable, diff-able, and easy to keep in git. By default it is read from /etc/vlan_probe.toml (or /etc/vlan_probe.json); use -c <path> for another location.

[[targets]]
name = "Internal - Device A SSH"
vlan = "Internal"
ip = "10.10.1.10"
port = 22
protocol = "tcp"
expected_blocked = true

[[targets]]
name = "External - Public DNS"
vlan = "External"
ip = "8.8.8.8"
port = 53
protocol = "udp"
expected_blocked = false

[[targets]]
name = "Internal - Gateway Ping"
vlan = "Internal"
ip = "10.10.1.1"
port = 0
protocol = "icmp"
expected_blocked = true

[[targets]]
name = "Internal - Diameter Signalling"
vlan = "Internal"
ip = "10.10.1.20"
port = 3868
protocol = "sctp"
expected_blocked = false

Each target declares the firewall policy it expects:

expected_blocked Policy Reported when…
true deny target is reachable → violation 🔴
false allow target is unreachable → failure 🔴

Supported protocols: tcp · udp (probed with DNS query on port 53, NTP request on port 123, or null byte; ICMP port-unreachable is detected) · icmp (via ping) · sctp.

Note on UDP probing: UDP is connectionless. For DNS (port 53) and NTP (port 123), vlan-probe sends valid protocol request packets and awaits responses. For other UDP ports, it sends a probe byte and waits for a response or an ICMP Port Unreachable message. Silent UDP endpoints that drop unsolicited packets will time out.

Environment variables

Every setting that isn't a probe target can come from an environment variable instead of a flag or the config file. CLI flags win over env vars; env vars win over the [mqtt] config section. This keeps secrets out of the config file and makes containerized/systemd deployments trivial.

CLI defaults:

Env var Controls Default
VLAN_PROBE_CONFIG config file path /etc/vlan_probe.toml
VLAN_PROBE_TIMEOUT probe timeout (seconds) 2.0
VLAN_PROBE_FORMAT output format ndjson
VLAN_PROBE_STRICT strict mode (exit 1) false
VLAN_PROBE_CONCURRENCY worker thread pool size 10

MQTT ([mqtt] section, overridable per-key):

Env var [mqtt] key
VLAN_PROBE_MQTT_HOST host
VLAN_PROBE_MQTT_PORT port
VLAN_PROBE_MQTT_USERNAME username
VLAN_PROBE_MQTT_PASSWORD password
VLAN_PROBE_MQTT_TLS tls
VLAN_PROBE_MQTT_CA_CERTS ca_certs
VLAN_PROBE_MQTT_INSECURE insecure
VLAN_PROBE_MQTT_TOPIC_PREFIX topic_prefix
VLAN_PROBE_MQTT_RETAIN retain
VLAN_PROBE_MQTT_QOS qos
VLAN_PROBE_MQTT_CONNECT_TIMEOUT connect_timeout

Booleans accept 1/true/yes/on. A VLAN_PROBE_MQTT_HOST alone is enough to enable MQTT — no [mqtt] section needed:

export VLAN_PROBE_MQTT_HOST=mqtt.example.com
export VLAN_PROBE_MQTT_USERNAME=vlan-probe
export VLAN_PROBE_MQTT_PASSWORD='s3cret'
export VLAN_PROBE_MQTT_TLS=true
vlan-probe --mqtt

Usage

-v, --version             Show program's version number and exit
-c, --config PATH         Path to config TOML/JSON file (default: /etc/vlan_probe.toml)
-f, --format FORMAT       Output format: ndjson, json, or table (default: ndjson)
-t, --timeout SECONDS     Socket connection timeout (default: 2.0)
-j, --concurrency INT     Number of concurrent worker threads (default: 10)
-s, --strict              Exit with code 1 if any violations occur
--mqtt                    Publish results to MQTT (requires [mqtt] section or VLAN_PROBE_MQTT_HOST)
--color [auto|always|never]  Colorize output (default: auto)
# table output
vlan-probe -c ./vlan_probe.toml -f table

# json output
vlan-probe -f json

# strict mode with a custom timeout and concurrency
vlan-probe -s -t 5.0 -j 20

# no color
vlan-probe -f table --color never

Exit codes

Code Meaning
0 all checks passed
1 violations detected (with --strict)
2 configuration error or MQTT delivery failure

Output formats

Table (IPs are fictional):

VLAN         TARGET                         ENDPOINT               STATUS   DETAILS
------------------------------------------------------------------------------------------
Internal     Internal - Device A SSH        10.10.1.10:22 (tcp)    PASS     OK
Internal     Internal - DNS Server          10.10.1.1:53 (udp)     PASS     OK
IoT          IoT - Device B                 10.10.2.50:80 (tcp)    PASS     OK
DMZ          DMZ - Web Server               10.10.4.50:80 (tcp)    FAIL     EXPECTED_CONNECTIVITY_FAILED: Failed to connect to DMZ - Web Server (10.10.4.50:80)
External     External - Public DNS          8.8.8.8:53 (udp)       PASS     OK

JSON — a run summary with totals and violations:

{
  "timestamp": "2026-08-15T07:56:17.397044+00:00",
  "total_probed": 12,
  "passed": 10,
  "failed": 2,
  "violations": [
    {
      "vlan": "DMZ",
      "target": "DMZ - Web Server",
      "ip": "10.10.4.50",
      "port": 80,
      "error": "EXPECTED_CONNECTIVITY_FAILED: Failed to connect to DMZ - Web Server (10.10.4.50:80)"
    }
  ]
}

NDJSON — one object per probed target, ideal for streaming/ingestion:

{"timestamp": "2026-08-15T07:55:15.797713+00:00", "target_name": "Internal - Device A SSH", "target_vlan": "Internal", "target_ip": "10.10.1.10", "port": 22, "protocol": "tcp", "reachable": false, "expected_blocked": true, "status": "PASS", "latency_ms": 1001.0, "error": null}

Strict mode (-s) writes failures to stderr and exits with code 1:

🚨 2 unauthorized connection(s) detected!
  - EXPECTED_CONNECTIVITY_FAILED: Failed to connect to DMZ - Web Server (10.10.4.50:80)
  - EXPECTED_CONNECTIVITY_FAILED: Failed to connect to DMZ - Web Server HTTPS (10.10.4.50:443)

Scheduled MQTT reporting ⏱️

Run the probe on a schedule and publish results to an MQTT broker so dashboards and automations can track VLAN isolation state over time. A systemd timer wakes a short-lived process that probes, publishes, and exits — no daemon, no state to babysit.

Add an [mqtt] section to the config:

[mqtt]
host = "mqtt.example.com"
port = 8883
username = "vlan-probe"               # optional
password = "s3cret"                   # optional
tls = true                            # optional, default false
ca_certs = "/etc/ssl/certs/ca-certificates.crt"  # optional
insecure = false                      # optional
topic_prefix = "vlan-probe"           # optional, default "vlan-probe"
retain = true                         # optional, default true
qos = 1                               # optional, default 1
connect_timeout = 5.0                 # optional, default 5.0

…and run with --mqtt:

vlan-probe --mqtt

Topics published (all retained at QoS 1):

  • vlan-probe/<hostname>/summary — run summary (totals + violations)
  • vlan-probe/<hostname>/targets/<vlan>/<target> — one message per target

A failed MQTT delivery is fatal (exit code 2); probe violations in strict mode still exit 1. --mqtt without an [mqtt] section or VLAN_PROBE_MQTT_HOST is a config error.

systemd timer

Ship the example units and enable the timer (adjust the interval in vlan-probe.timer, default every 5 minutes):

# system-wide (root)
sudo cp deploy/systemd/vlan-probe.service deploy/systemd/vlan-probe.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now vlan-probe.timer

# user (no root)
mkdir -p ~/.config/systemd/user
cp deploy/systemd/user/vlan-probe.service deploy/systemd/user/vlan-probe.timer ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now vlan-probe.timer

Ops tips:

  • journalctl -u vlan-probe — probe + delivery logs per run
  • systemctl list-timers vlan-probe — next scheduled run
  • Broker credentials: the [mqtt] config section (chmod 600) or the VLAN_PROBE_MQTT_* environment variables in a systemd EnvironmentFile

Development 🧑‍💻

The repository ships a Makefile wrapping uv — that's the supported way to work in this project.

Command What it does
make venv create the virtualenv, install dev deps, and set hooks
make hooks configure git pre-commit hook to enforce lint & tests
make lint ruff lint and format check
make format auto-format the code with ruff
make test mypy type-check + pytest with 100% coverage gate
make run run the CLI, e.g. make run ARGS="-f table"
make build build sdist + wheel
make publish build and publish to PyPI
make install install the package into a uv tool environment
make clean remove the venv and build/cache artifacts
make venv    # one-time setup (also configures git hooks)
make lint    # before pushing
make test    # before pushing

Git hooks 🪝

A pre-commit hook is provided in .githooks/pre-commit to automatically run ruff, mypy, and pytest before every commit.

To install / enable the hook:

make hooks
# or directly with git:
git config core.hooksPath .githooks

Test coverage 🛡️

Every code path is covered by a meaningful test — real loopback sockets and real config files, mocking only system boundaries. pytest fails if coverage drops below 100% (--cov-fail-under=100). Run the suite with a coverage report:

make test                      # mypy + pytest with coverage
make run ARGS="-f table"       # try it out

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vlan_probe-0.6.0.tar.gz (76.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vlan_probe-0.6.0-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

Details for the file vlan_probe-0.6.0.tar.gz.

File metadata

  • Download URL: vlan_probe-0.6.0.tar.gz
  • Upload date:
  • Size: 76.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vlan_probe-0.6.0.tar.gz
Algorithm Hash digest
SHA256 4e19ac0a77b90683162b7f2c8db5f59d864cc3864cfecf4e71ef30ff11f4e6a9
MD5 5d8a2a5960b1c332af56ddfedd8ab268
BLAKE2b-256 41ae78554c4d4b8a615bbd7d61d6815b7a618e88ba57f6747bd4667d700ac8e8

See more details on using hashes here.

Provenance

The following attestation bundles were made for vlan_probe-0.6.0.tar.gz:

Publisher: ci.yml on hellqvio86/vlan-probe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vlan_probe-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: vlan_probe-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 18.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vlan_probe-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 06a6fda77386db63f4c83bbb894c929f5f0cdd542a69b0b81d7dc16a84a12946
MD5 4ada7a01815aa2c537fe93e484c81031
BLAKE2b-256 64f2c78715a3fe6353c2382a7868780664ab1bc49b6e731aef646680947b8a3c

See more details on using hashes here.

Provenance

The following attestation bundles were made for vlan_probe-0.6.0-py3-none-any.whl:

Publisher: ci.yml on hellqvio86/vlan-probe

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page