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.
✨ Features
- Multi-protocol probing —
tcp,udp(DNS-aware on port 53, NTP-aware on port 123),icmp(ping), andsctp. - 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, structuredjson, or streamingndjson. - Alerting built-in —
--strictexit codes for automation, plus scheduled MQTT reporting viasystemdtimers 📡. - 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
ipandpingutilities)
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-probesends 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.
--mqttwithout an[mqtt]section orVLAN_PROBE_MQTT_HOSTis 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 runsystemctl list-timers vlan-probe— next scheduled run- Broker credentials: the
[mqtt]config section (chmod 600) or theVLAN_PROBE_MQTT_*environment variables in a systemdEnvironmentFile
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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vlan_probe-0.5.0.tar.gz.
File metadata
- Download URL: vlan_probe-0.5.0.tar.gz
- Upload date:
- Size: 75.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f800f442932213514f8fd2ddb7e5bede96419d1f841975003bea46269da0e063
|
|
| MD5 |
1fde8b4e2037b801993a2d3c91b87686
|
|
| BLAKE2b-256 |
c376ff2c44024f42209fec38300aeb049f18660f2280a4690890a589ede2e5d8
|
Provenance
The following attestation bundles were made for vlan_probe-0.5.0.tar.gz:
Publisher:
ci.yml on hellqvio86/vlan-probe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vlan_probe-0.5.0.tar.gz -
Subject digest:
f800f442932213514f8fd2ddb7e5bede96419d1f841975003bea46269da0e063 - Sigstore transparency entry: 2573652348
- Sigstore integration time:
-
Permalink:
hellqvio86/vlan-probe@22a14192d8a88f85130f87071b796a782c9d18a7 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/hellqvio86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@22a14192d8a88f85130f87071b796a782c9d18a7 -
Trigger Event:
release
-
Statement type:
File details
Details for the file vlan_probe-0.5.0-py3-none-any.whl.
File metadata
- Download URL: vlan_probe-0.5.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0e1cbd415c192f6d7d8e78bc7f5ee1129aa70bd13d343b12ab7a6da558e6f7c
|
|
| MD5 |
6cf4544caef6ffb8dc7c4de161ddfce7
|
|
| BLAKE2b-256 |
6c836793f48d8d288efafc4041c0ab9530ef054d5f7aaa8defb72acb9bdb322e
|
Provenance
The following attestation bundles were made for vlan_probe-0.5.0-py3-none-any.whl:
Publisher:
ci.yml on hellqvio86/vlan-probe
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vlan_probe-0.5.0-py3-none-any.whl -
Subject digest:
c0e1cbd415c192f6d7d8e78bc7f5ee1129aa70bd13d343b12ab7a6da558e6f7c - Sigstore transparency entry: 2573652408
- Sigstore integration time:
-
Permalink:
hellqvio86/vlan-probe@22a14192d8a88f85130f87071b796a782c9d18a7 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/hellqvio86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@22a14192d8a88f85130f87071b796a782c9d18a7 -
Trigger Event:
release
-
Statement type: