Linuxnetworking

Verify VLAN Isolation from a Linux Host

Stand inside any VLAN from a Linux laptop and run a pass/fail ping check that proves your network segmentation actually holds.

DistrosUbuntu 24.04, Debian 12
Shellbash
Updated
Script
bash
#!/usr/bin/env bash
# verify-vlan-isolation.sh — stand in a VLAN and prove the walls hold.
#
# Pings are read-only. The only thing this script ever creates is one
# optional virtual VLAN interface on your own machine (--vlan N), and it
# deletes it again on exit.
#
# Usage:
#   ./verify-vlan-isolation.sh            # test from this host's current network
#   ./verify-vlan-isolation.sh --vlan 20  # first join VLAN 20 (needs a trunk port + sudo)
set -u

# ==== MAKE THESE VALUES YOUR OWN ==========================================
PARENT_IF="eth0"                 # your wired interface (list yours: ip link)
INTERNET_TARGET="8.8.8.8"        # anything reliably reachable on the internet

# What SHOULD answer from where you're standing. NOTE: with the strict IoT
# firewall from the companion post (input REJECT), the IoT gateway will NOT
# answer pings from inside its own VLAN — leave this empty when standing
# there, or list hosts you know answer (e.g. a sibling device in the VLAN):
EXPECT_OK=()

# What SHOULD be unreachable from here (e.g. a trusted PC, the lab NAS):
EXPECT_BLOCKED=("192.168.1.50" "10.0.0.10")
# ==========================================================================

VLAN_ID=""
if [ "${1:-}" = "--vlan" ] && [ -n "${2:-}" ]; then
  VLAN_ID="$2"
fi

PASS=0
FAIL=0

VIF=""
cleanup() {
  if [ -n "$VIF" ]; then
    echo "Cleaning up: removing $VIF"
    command -v dhclient >/dev/null 2>&1 && sudo dhclient -r "$VIF" 2>/dev/null
    sudo ip link del "$VIF" 2>/dev/null
  fi
}
trap cleanup EXIT

if [ -n "$VLAN_ID" ]; then
  VIF="${PARENT_IF}.${VLAN_ID}"
  echo "Joining VLAN $VLAN_ID via $PARENT_IF (creates temporary interface $VIF)..."
  sudo ip link add link "$PARENT_IF" name "$VIF" type vlan id "$VLAN_ID" || exit 1
  sudo ip link set "$VIF" up
  if command -v dhclient >/dev/null 2>&1; then
    sudo timeout 20 dhclient -1 "$VIF"
  elif command -v dhcpcd >/dev/null 2>&1; then
    sudo timeout 20 dhcpcd -1 --noipv6 "$VIF"
  else
    echo "No DHCP client found — assign an address manually, e.g.:"
    echo "  sudo ip addr add 10.0.20.200/24 dev $VIF"
  fi
  ADDR=$(ip -4 -o addr show "$VIF" | awk '{print $4}')
  if [ -n "$ADDR" ]; then
    echo "PASS  got address $ADDR on VLAN $VLAN_ID (trunk + DHCP are wired correctly)"
  else
    echo "FAIL  no address on $VIF — is this port carrying VLAN $VLAN_ID tagged?"
    FAIL=$((FAIL+1))
  fi
fi

# When a VLAN interface exists, pin every probe to it so the test can't
# quietly route over your regular connection and prove nothing.
BIND=""
if [ -n "$VIF" ]; then BIND="-I $VIF"; fi

probe() {
  # probe <host> <should_answer: yes|no>
  if ping $BIND -c 2 -W 2 "$1" >/dev/null 2>&1; then RESULT="answered"; else RESULT="silent"; fi
  if { [ "$2" = "yes" ] && [ "$RESULT" = "answered" ]; } || \
     { [ "$2" = "no" ]  && [ "$RESULT" = "silent" ];   }; then
    echo "PASS  $1 $RESULT (expected)"
    PASS=$((PASS+1))
  else
    echo "FAIL  $1 $RESULT (expected the opposite)"
    FAIL=$((FAIL+1))
  fi
}

echo "--- reachability that SHOULD work ---"
probe "$INTERNET_TARGET" yes
for host in "${EXPECT_OK[@]}"; do probe "$host" yes; done

echo "--- isolation that SHOULD hold ---"
for host in "${EXPECT_BLOCKED[@]}"; do probe "$host" no; done

echo
echo "Summary: $PASS passed, $FAIL failed."
if [ "$FAIL" -eq 0 ]; then
  echo "The walls hold."
else
  echo "Segmentation is NOT doing what you think — check VLAN membership, PVIDs, and firewall forwardings."
  exit 1
fi

What this does

First: make these values your own — every IP, interface name, and VLAN ID in this script is an example placeholder (see Notes below for the full list). Edit the marked block at the top before running anything.

Network segmentation you haven’t tested is a hope, not a control. This script stands on one side of your VLAN walls and knocks on doors: the targets in EXPECT_OK (plus one internet address) should answer, and the targets in EXPECT_BLOCKED should time out. Each probe prints PASS or FAIL against what you expected, so a firewall hole shows up as a loud FAIL instead of a quietly successful ping you never noticed. It exits non-zero if any expectation fails — including a failed VLAN join — so a scheduled cron job can warn you the day a wall silently falls.

With --vlan <id>, the script first joins that VLAN by creating a temporary 802.1Q virtual interface on top of your wired network port (the NIC), using the ip link ... type vlan id syntax from ip-link(8), and requesting a DHCP lease. That lets a laptop plugged into a tagged trunk port impersonate a device on any VLAN — no need to SSH into your smart TV. Getting a lease at all is itself a test: it proves the trunk carries that VLAN and something on it is handing out addresses. While the interface exists, every probe is pinned to it, so results can’t leak over your regular connection — and the interface is deleted automatically on exit.

Prerequisites

  • A Linux machine with a wired connection to the switch you’re testing (Wi-Fi won’t carry your VLAN tags).
  • iproute2 (the ip command — present on effectively every modern distro) and either dhclient or dhcpcd for the --vlan mode.
  • sudo rights, only needed for --vlan mode (creating and deleting the virtual interface).
  • For --vlan mode: the switch port you’re plugged into must carry the target VLAN tagged — on the port map from the VLANs for beginners post, that’s the trunk port.

Notes

  • Placeholders to replace, all in the marked block at the top: eth0 (your interface name, from ip link), 8.8.8.8 (any internet host you’re happy to ping), the hosts you add to EXPECT_OK (devices that should answer from where you stand), 192.168.1.50 (a trusted-network device), 10.0.0.10 (a lab device, e.g. your NAS), 10.0.20.200/24 (the manual-address example the script prints when no DHCP client is found), and the VLAN ID you pass with --vlan. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.
  • A FAIL on the isolation section means a frame crossed a wall that should have stopped it. Work backwards in this order: the port’s untagged VLAN membership, its PVID, then the router’s firewall forwardings — that sequence catches the mistake in almost every case.
  • Some devices block ping (ICMP) themselves, which can make a reachable host look silent and score a false PASS on the blocked list. Prefer targets you know answer pings from their own network (test that once from the trusted side first).
  • ICMP is a good first probe but not the whole story: a strict test also checks TCP (e.g. can IoT reach your NAS’s SSH or Samba ports?). Netcat’s nc -z -w 2 10.0.0.10 445 is the one-liner to add if you want to go deeper.
  • Running with --vlan while your regular network connection stays up gives your machine a foot in two networks. That’s exactly what makes the test possible — just remember it also gives your laptop a foothold in both zones, a potential pivot point, which is why the interface is removed on exit.