Green Lights, No Packets: Common Homelab Network Issues

Four networking failures behind most homelab mysteries — IP conflicts, VLAN mistakes, Tailscale subnet routes, and DNS — with diagnosis, fixes, and prevention.

On this page
  1. Issue 1: Two machines, one address
  2. Issue 2: The VLAN that looks configured
  3. Issue 3: The subnet route that half-works
  4. Issue 4: It was DNS
  5. The five-minute audit
  6. What’s next

Storage failures, Part 1’s subject, at least have the decency to leave evidence — a graph climbing toward 100% is a confession. Network failures gaslight you instead. The link light is on, the switch is happy, every service reports running, the dashboard is a wall of green — and your packets are going somewhere quiet to die. Everything is up; nothing can talk.

This is Part 2 of the Common Homelab Issues series: the four network failures I keep meeting — the address two machines both think they own, the VLAN that looks configured but isn’t, the Tailscale subnet route that half-works, and DNS being DNS. Same deal as Part 1: symptom, diagnosis, the actual fix, and the prevention that makes it boring next time.

First: make these values your own

The examples below use placeholder values — swap them for yours before copying anything:

  • 10.0.0.0/24, 10.0.0.1, 10.0.0.50 — an example home subnet, its gateway, and a contested address. Substitute your real ranges.
  • eth0 — an example interface name; find yours with ip -br link.
  • homelab-router, nas.homelab.lan — an example machine name and internal DNS name.
  • 9.9.9.9 — a public resolver (Quad9); any public resolver works for the comparisons below.

Rule of thumb: if a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


Issue 1: Two machines, one address

The symptom is intermittence, and intermittence is the tell. A device answers ping, then doesn’t, then does. SSH sessions freeze mid-keystroke. The camera feed drops every few minutes and “fixes itself.” One machine can reach the printer and the one next to it can’t. When a problem flickers, think IP conflict before you think hardware.

Here’s the mechanism in plain terms: when a machine wants to talk to 10.0.0.50, it shouts “who has 10.0.0.50?” on the local network — that’s ARP, the protocol that maps IP addresses to physical MAC addresses. If two devices both believe they’re 10.0.0.50, both answer, and whoever answers last wins a spot in the asker’s memory until the next shout. Your traffic alternates between two destinations, and only one of them is the machine you meant.

An ARP argument: two devices, one addressyour laptop“who has 10.0.0.50?”NAS“me — 10.0.0.50”smart camera“no, me — 10.0.0.50”whoever answered last gets your packets — half your traffic visits the wrong machine
1Diagnose: make the two claimants reveal themselves5 min

From a third machine on the same network:

Two different MACs answering = a live conflict

sudo arping -I eth0 -c 4 10.0.0.50
ip neigh show 10.0.0.50     # run again in a minute — did the MAC change?

arping asks the who-has question directly and prints every reply with the MAC address it came from — replies from two different MACs are the smoking gun (it needs root, since raw ARP requires the CAP_NET_RAW capability per its manual page). The second command reads your machine’s ARP memory; the same IP wearing a different hardware address each time you look is the same verdict, gathered passively.

2Fix: identify both devices and evict one10 min

A MAC address tells you more than it looks like it does: the first three bytes identify the manufacturer, and your router’s DHCP client list or your switch’s MAC table will usually pin both claimants to a name or a port. Nine times out of ten the culprit is a hand-assigned static address sitting inside the DHCP pool — a printer, a camera, or a hypervisor someone configured years-in-dog-years ago — and the router eventually leased the same address to something else. Move the static device to an address outside the pool, or better, delete the manual config entirely and pin it with a DHCP reservation at the router.

The prevention is a single source of truth for addresses. Let the router own the pool, give every machine that matters a reservation (so it always receives the same lease), and keep a written split — say, .100.199 for dynamic leases and everything below for reserved infrastructure. When you’re about to claim an address by hand, ask first whether it’s genuinely free:

Is this address actually free? (exit 0 = free)

sudo arping -D -c 2 -I eth0 10.0.0.42

That -D is duplicate-address-detection mode — the same probe DHCP clients use before accepting a lease — and it exits zero only if nobody answered. I’ll admit this one is fresh for me: just this week I stood up a new logging container, let it take a DHCP lease, and by evening that address was hardcoded into seven config files across the lab. The honest fix — a reservation, made before the address soaks into configs — is advice I’m handing you while my own reservation still sits on the to-do list. If you’d rather pin the address on the machine itself, do it properly with netplan or your distro’s equivalent — outside the pool.


Issue 2: The VLAN that looks configured

VLANs earn their spot here because the failure is silent and the config looks right. Quick recap of the three words that matter (the full beginner’s guide walks these gently): a tagged port carries frames labeled with a VLAN number, an untagged (access) port strips labels for devices that don’t speak VLAN, and the PVID decides which VLAN an unlabeled incoming frame gets stamped into. Every VLAN mystery I’ve met lives in one of those three settings.

The symptom comes in two opposite flavors. Flavor one: the new IoT VLAN’s devices get no address, or get one and reach nothing — frames are being dropped at a port whose membership doesn’t match. Flavor two is sneakier: everything works too well, and the camera you “isolated” can still see your NAS — the isolation you built isn’t isolating.

Where VLAN frames actually dieIoT cameraVLAN 30 trafficswitch port 5not a VLAN 30 memberrouterframe dropped here — silently“isolated” deviceport left in VLAN 1default VLAN 1everything is a memberyour NASstill reachableflavor one: the frame dies at a non-member port · flavor two: the port you forgot stays in VLAN 1 and leaks

The diagnosis is a walk along the path, in order:

  • The access port. Is the device’s port untagged in the intended VLAN, and is its PVID set to that VLAN? A PVID left at 1 quietly files everything the device sends into the default VLAN.
  • Every link between switches. Each trunk port along the path must carry the new VLAN tagged. One switch in the middle that never heard of VLAN 30 is a wall — and this is the single most common miss when a VLAN spans two switches.
  • The membership you didn’t set. When I wrote the VLANs guide, the adversarial review pass caught a real hole in my own walkthrough: on TP-Link’s Easy Smart switches, every port stays a member of VLAN 1 until you explicitly remove it. The new VLAN was configured and working — and the “isolated” ports still leaked through VLAN 1 the whole time. Isolation isn’t what you added; it’s what you removed.
  • DHCP for that VLAN. A new VLAN needs its own DHCP scope (usually a VLAN interface on the router). Devices stuck at self-assigned 169.254.x.x addresses on an otherwise-correct VLAN mean the frames are flowing and nobody’s answering — the scope is missing.

On the Proxmox side, if your containers ride a VLAN-aware bridge, the node can show you its own membership table — bridge vlan show lists which VLANs each port carries:

What the Linux bridge itself believes (Proxmox node)

bridge vlan show

Fix whichever rung of that ladder broke, and change one port at a time with a test device plugged in — VLAN lockouts of the switch’s own management interface are their own genre of bad afternoon. Then prove the isolation instead of assuming it: the VLAN isolation playbook pins a ping matrix across your VLANs and tells you which pairs can and can’t talk. One expectation to keep: device discovery (casting, AirPlay, “find my printer”) relies on mDNS, which doesn’t cross VLAN boundaries by design — the VLANs guide covers the workarounds, so don’t diagnose working isolation as a failure.


Issue 3: The subnet route that half-works

Tailscale is the piece of my lab I recommend most freely, and the subnet router — one machine that advertises your whole home subnet to your private mesh network (your tailnet) — is the feature that makes the lab reachable from anywhere. It’s also the feature with the longest chain of quiet prerequisites, and the symptom is always the same shape: you can reach the subnet router itself, but nothing behind it. Or the route works from your phone but not your Linux laptop. Or it worked for months and died after a reinstall.

The chain, in the order it fails (Tailscale’s subnet docs are the canonical reference for each link):

1Advertise the route (on the subnet router)2 min
Tell the tailnet this machine can reach 10.0.0.0/24

sudo tailscale set --advertise-routes=10.0.0.0/24

(Older guides use tailscale up --advertise-routes=... — same effect, and set is the current documented form.)

2Enable IP forwarding (same machine)2 min

A machine won’t pass traffic between its interfaces unless the kernel is told to. Tailscale’s docs give exactly this:

Kernel forwarding — without it the route is a dead end

echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
3Approve the route — the step everyone forgets1 min

Advertising is a request, not a grant. Until an admin approves the route in the admin console, every device sees nothing behind the router. In the console’s Machines page, the subnet router carries a Subnets badge — and an unapproved route sits there marked as awaiting approval, which looks done at a glance and isn’t:

admin console — MachinesMACHINESTATUSlaptopconnectedphoneconnectedhomelab-routerSubnets10.0.0.0/24 — awaiting approvalApprove…advertised ≠ approved — until this click, every device sees nothing behind the router(mockup with example names, not a real tailnet)
4Accept routes on Linux clients1 min

Android, iOS, macOS, tvOS, and Windows clients pick up approved subnet routes automatically. Linux clients don’t — each one must opt in once:

On each Linux client that should use the route

sudo tailscale set --accept-routes

If the route “works everywhere except my Linux laptop,” it’s this. Check what any client currently sees with tailscale status.

Past that chain lives the layer nobody’s quickstart mentions: the destination gets a vote, and so does everything in between. When a container of mine couldn’t reach my NAS, the packet had to survive three separate checkpoints — the route itself, the tailnet’s access rules, and the NAS’s own firewall — and from inside the container, all three failures looked identical: silence. Diagnose them as separate gates, in order, or you’ll fix one and conclude the whole approach is broken.

One packet, three separate permissionsyour devicegate 1route exists,approvedgate 2access rulesallow itgate 3host firewallallows itNASany one gate closed = silence, and all three silences feel identical from the sendercheck them in order: tailscale status → access rules → the destination’s own firewall

Two hard-won notes to round it out. First, if you add a route to a Proxmox container by hand, know that Proxmox regenerates the container’s network config from its own settings — my hand-added route quietly vanished on the next restart, and the durable fix is putting the route in the container’s configuration (or a small systemd unit inside the guest), the same class of lesson as container networking generally. Second, from Tailscale’s own gotcha list: with overlapping advertised routes, there’s no automatic fallback from a more-specific route to a less-specific one when its router goes offline — keep advertisements to one clean subnet unless you genuinely need more.


Issue 4: It was DNS

There’s an old ops haiku: it’s not DNS. There’s no way it’s DNS. It was DNS. It earns its place because DNS failures wear other problems’ clothes — “the internet is down” (it isn’t; name lookups are), “the NAS is offline” (it’s fine; its name is gone), “Docker is broken” (the container just can’t resolve anything).

The five-second triage that splits the world in half: ping a number. If ping 9.9.9.9 works while ping nas.homelab.lan fails, the network is fine and naming is broken. From there, the trick is asking the same question several ways and comparing answers:

The fork test — same question, different answerers

getent hosts nas.homelab.lan     # what applications actually see
dig nas.homelab.lan              # straight to your configured DNS server
dig @9.9.9.9 nas.homelab.lan     # the same question to a public resolver
resolvectl status | head         # (systemd machines) who IS my resolver?

Each line isolates a different layer. getent walks the same lookup path your applications use — including /etc/hosts — while dig goes straight to DNS; when those two disagree, you’ve found a stale /etc/hosts entry lying to every program on that machine. When your configured resolver fails but the public one answers, the problem is your resolver, not the name. And an internal name returning NXDOMAIN from a public resolver is correct — the outside world has never heard of your NAS, which is exactly how you want it. On systemd distributions, resolvectl status settles the surprisingly-common prior question of which server your machine is even asking.

One name, two very different answersnas.homelab.lan?same question, twiceyour local resolver→ 10.0.0.60 ✓public resolver→ NXDOMAIN (correct!)internal names should only exist internally — the failure is when the LEFT side stops answeringif both sides fail: resolver down · if left fails, right works: local records broken · if they disagree on a public name: check /etc/hosts

Two traps deserve their own paragraphs, because both produce “works on my laptop, fails over there”:

Containers resolve differently than their host. Docker hands containers on the default bridge a copy of the host’s resolv.conf taken at start — change your host’s resolver later and the container keeps resolving with the old one until restarted. Containers on user-defined networks instead use Docker’s embedded DNS server at 127.0.0.11, which forwards upstream on the host’s behalf. So “DNS works on the host but not in the container” usually means a stale copy — restart the container — and any hardcoded 127.0.0.x value inside a container deserves the same suspicion as its cousin, the localhost binding trap.

Your router may be silently censoring answers. Many home routers run dnsmasq with rebind protection on: upstream answers that point into private address ranges are rejected and logged — a real defense against an attack where outside websites probe your LAN. The collateral damage: if a public DNS name (say, a domain you own) resolves to a private address like 10.0.0.60, it works everywhere except on your own network. If the fork test shows a name answering publicly but dying at your router, that’s the signature; the fixes are an explicit exemption for your domain (dnsmasq’s rebind-domain-ok) or serving that name from your local resolver directly.

Prevention here is consolidation: one resolver that owns your internal names — Pi-hole is the homelab classic, and mine runs in an LXC — handed out by DHCP so every device agrees, with the resolver itself on a reserved address (Issue 1’s lesson, wearing DNS clothes: a resolver that moves is an outage that follows you around). When a fix doesn’t seem to take, remember caches sit at every layer; flushing the client-side ones removes a whole category of ghosts.


The five-minute audit

Like Part 1, everything above condenses into one read-only script: the homelab network health check playbook walks link state, gateway reachability, a duplicate-address probe for your own IP, resolver identity, and the local-vs-public fork test — printing OK/WARN per check and exiting non-zero if anything’s wrong, so cron or an Uptime Kuma push monitor can run it and complain on your behalf. I ran it against my own machines while writing this post.

The prevention checklist, in full:

  • The router owns addressing: reservations for everything that matters, statics only outside the pool, and the split written down.
  • A reservation before an address soaks into config files — not after.
  • VLAN changes go one port at a time, with a test device, and isolation is proven with a ping matrix — remembering that membership you didn’t remove (VLAN 1) is still membership.
  • Subnet routes get the full chain: advertise → forward → approve → accept-routes on Linux clients.
  • Firewalls are diagnosed as separate gates — the overlay’s rules and the destination’s own, in that order.
  • One resolver owns internal names, every device gets it from DHCP, and the resolver lives on a reserved address.
  • Routes and network config live in the machine’s configuration, never in a one-off command that evaporates on restart.

What’s next

That’s Part 2 of the Common Homelab Issues series. Part 1 covered storage — the failures that at least leave evidence — and Part 3 takes on the service-level failures: containers that won’t start, the OOM killer, lost quorum, and the SSH lockout — the day the failure takes your rescue tools with it.

One last nudge before you go: every address, interface, and hostname above was an example. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.


Interactive: trace it instead of guessing

Two of the site’s field tools were built for exactly this post’s problems: the Overlay Network Diagnostic walks a can’t-reach-it path (Tailscale routes included) question by question, and the Homelab Troubleshooter takes a plain-English symptom and returns ranked causes with the commands to check them. Both need a model behind them: run them free on claude.ai, point them at your own provider, or at a model on your own hardware — whichever you pick, what you type goes to that provider and never to me.

Related posts:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.