Skip to content

docs: improve unsafe routes guide - #386

Open
f-hollow wants to merge 4 commits into
DefinedNet:mainfrom
f-hollow:docs/improve_upsafe_routes_guide
Open

f-hollow wants to merge 4 commits into
DefinedNet:mainfrom
f-hollow:docs/improve_upsafe_routes_guide

Conversation

@f-hollow

@f-hollow f-hollow commented Oct 5, 2026 •

Copy link
Copy Markdown

This PR is my attempt to improve the unsafe routes guide mentioned in slackhq/nebula#1837 (comment) after my journey setting up a Nebula overlay network with unsafe routes.

In the near future, I plan to create a couple more PRs with some minor improvements in other documents related to this one.

Important

I took the liberty to update quite a few things.

Feel free to let me know if any proposed changes are not desirable or don't feel right for whatever reason.

Changes

Among the other changes in this PR, the following deserve a mention:

  • Introduced consistent terminology for hosts as opposed to shifting phrases, such as Linux router host vs. linux host or local network vs. subnet vs. extended network
  • Replace deprecated parameters
    • -ip > -networks
    • -subnets > -unsafe-networks
  • Removed existing images
    • Replaced the old network diagram with easy-to-update and lightweight ASCII art diagram bearing consistent terminology and leaving out details like IP addresses
    • Removed the screenshot of app's unsafe route settings with route and via as it doesn't seem to add enough valuable information to justify its update
  • Added guidelines on how to route 0.0.0.0/0
  • Added a few internal links for smoother navigation. They might introduce a slight maintenance overhead, but you can consider using a tool like broken link checker in CI to automatically validate all documentation links.

Proposals

Just because you use the same documentation for all Nebula versions, it might be good to mark documents with the version they were written for. It will help track how old the documents are and which of them need to be updated according to the breaking changes in newer Nebula versions.

For this, I propose adding metadata parameters in doc YAML frontmatter:

  • app_version -- Nebula version for which the guide was written and verified against
  • min_app_version -- lowest supported version: new nebula-cert options -networks and -unsafe-networks used

Questions

  • While following the guide, I had to repeatedly check if 192.168.100.0/24 was the Nebula or LAN subnet, because 192.168.86.0/24 is very similar. Using 10.0.0.0/24 for Nebula and 192.168.1.0/24 for LAN would immediately solve the problem. But I see that you use 192.168.100.0/24 for the Nebula network in config.yml. Do you want to keep it that way or are you willing to reconsider example IP ranges in the docs?
  • Does this PR resolve Document use of local_cidr firewall rules in the firewall section of the unsafe_routes guide?
  • I didn't have a chance to test the ufw configuration. Can you please verify it?

@f-hollow f-hollow changed the title docs: improve upsafe routes guide docs: improve unsafe routes guide Oct 5, 2026
@f-hollow
f-hollow force-pushed the docs/improve_upsafe_routes_guide branch from 9bc53ec to 92747b6 Compare October 5, 2026 18:51
@f-hollow
f-hollow force-pushed the docs/improve_upsafe_routes_guide branch from 92747b6 to 8f0cc95 Compare October 13, 2026 11:35
@f-hollow

Copy link
Copy Markdown
Author

@johnmaguire @bgolat PTAL

@jasikpark jasikpark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! The terminology cleanup, the move to -networks / -unsafe-networks, and the refreshed 1.11 output are all big improvements, and the 0.0.0.0/0 section is something people keep asking for. I think this also closes #107.

The main things to fix before merge are in the firewall/routing steps: the nftables policy drop, the iptables -A vs -I, and the ufw forward policy can each break or over-open forwarding on a host that already has other rules. There's also the scp destination, tun.dev on macOS, and the gateway side of the 0.0.0.0/0 setup. The rest are smaller nits.

One more that isn't on a diff line: network_diagram-unsafe_routes.png and mobile_nebula-unsafe_routes.png are no longer referenced, so they can be deleted in this PR.

— Caleb + Cache

Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
If connected, disconnect from the site. Now, connect again and try to access one of the hosts only accessible using
`unsafe_routes`. You should see a new line in your Mobile Nebula logs.

`level=warning msg="Adding UNSAFE Route" route=192.168.1.0/24 via=10.0.0.5`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this log line captured on a current Mobile Nebula build? Desktop 1.11 logs Added route now (as you show above), so this one may be out of date.

@f-hollow f-hollow Oct 20, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I just updated IP addresses in the existing log line.

I set up a new Nebula network and a full-tunnel config on an Android phone.
The connection log is given below.

Full-tunnel routing works fine, but I don't see any mention of added routes or unsafe routes... (the same on iOS)

@jasikpark Is it normal?

time=2026-10-20T01:41:34.867Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:outgoing endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-20T01:41:34.868Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-20T01:41:34.868Z level=INFO msg="Firewall started" firewallHashes=SHA:498215dec4e5687a2353f51c10838c113bd1af35ef72b8e8c9f536986ada5417,FNV:2782948616
time=2026-10-20T01:41:34.868Z level=INFO msg=listening addr=0.0.0.0:0
time=2026-10-20T01:41:34.868Z level=INFO msg="Main HostMap created" preferredRanges=[]
time=2026-10-20T01:41:34.868Z level=INFO msg="punchy disabled"
time=2026-10-20T01:41:34.868Z level=INFO msg="Loaded send_recv_error config" sendRecvError=always
time=2026-10-20T01:41:34.868Z level=INFO msg="Loaded accept_recv_error config" acceptRecvError=always
time=2026-10-20T01:41:34.869Z level=INFO msg="Nebula interface is active" interface=android networks=[10.0.10.14/24] build=1.11.1-0.20260922204501-7cfa47d569e8 udpAddr=[::]:38764 boringcrypto=false fips140Version=latest fips140Enabled=false fips140Enforced=false
time=2026-10-20T01:41:34.870Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.10] localIndex=134872465 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=134872465 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-20T01:41:34.871Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.1] localIndex=421341123 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=421341123 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-20T01:41:34.975Z level=INFO msg="Handshake message received" vpnAddrs=[10.0.10.10] from=<lighthouse-public-ip>:4242 certName=sgvps certVersion=2 fingerprint=5d8c3af750a49e2ee8e831ccdf8bfbc39b57902f4f32e6eb136349c2c922e6df issuer=2c1b5f8c0fc503970d14636df01f89eac17a8088ebadb87a131fe7ab5d926993 initiatorIndex=134872465 responderIndex=1171826692 handshake="map[stage:2 style:ix_psk0]" durationNs=105401823 sentCachedPackets=4
time=2026-10-20T01:41:35.085Z level=INFO msg="Handshake message received" vpnAddrs=[10.0.10.1] from=<gateway-public-ip>:4242 certName=jpvps certVersion=2 fingerprint=b6f31aaa78a3ce81326665f395e7a5f2698d4ae53b88874fa724f2dada2c01c8 issuer=2c1b5f8c0fc503970d14636df01f89eac17a8088ebadb87a131fe7ab5d926993 initiatorIndex=421341123 responderIndex=1425314157 handshake="map[stage:2 style:ix_psk0]" durationNs=214626354 sentCachedPackets=2

Comment thread docs/guides/unsafe_routes/index.mdx
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment on lines +532 to +534
Mobile Nebula [Android](https://play.google.com/store/apps/details?id=net.defined.mobile_nebula) and
[iOS](https://apps.apple.com/us/app/mobile-nebula/id1509587936) apps route `0.0.0.0/0` correctly as long as you follow
the guidelines in [Use unsafe routes with Mobile Nebula](#use-unsafe-routes-with-mobile-nebula).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you verified this on both Android and iOS? It may well be true, since the mobile apps manage their own tunnel routes, but it'd be good to confirm before we state it.

@f-hollow f-hollow Oct 20, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very good call. I didn't question this, because I assumed if 0.0.0.0/0 routing is implemented correctly on Android, it would also be on iOS.

Tested it on an iPhone with Mobile Nebula v0.11: Full-tunnel routing didn't work :(

Should I mention that full-tunnel routing on iOS doesn't work for now?

Here are the logs BEFORE adding the unsafe route 0.0.0.0/0:

time=2026-10-19T21:18:08.054Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:outgoing endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-19T21:18:08.054Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-19T21:18:08.055Z level=INFO msg="Firewall started" firewallHashes=SHA:498215dec4e5687a2353f51c10838c113bd1af35ef72b8e8c9f536986ada5417,FNV:2782948616
time=2026-10-19T21:18:08.055Z level=INFO msg=listening addr=0.0.0.0:0
time=2026-10-19T21:18:08.055Z level=INFO msg="Main HostMap created" preferredRanges=[]
time=2026-10-19T21:18:08.056Z level=INFO msg="punchy disabled"
time=2026-10-19T21:18:08.056Z level=INFO msg="Loaded send_recv_error config" sendRecvError=always
time=2026-10-19T21:18:08.056Z level=INFO msg="Loaded accept_recv_error config" acceptRecvError=always
time=2026-10-19T21:18:08.056Z level=INFO msg="Nebula interface is active" interface=iOS networks=[10.0.10.25/24] build=1.11.1-0.20260922204501-7cfa47d569e8 udpAddr=[::]:52909 boringcrypto=false fips140Version=latest fips140Enabled=false fips140Enforced=false
time=2026-10-19T21:18:08.060Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.1] localIndex=3182111111 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=3182111111 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:18:08.267Z level=INFO msg="Handshake message received" vpnAddrs=[10.0.10.1] from=<lighthouse-public-ip>:4242 certName=jpvps certVersion=2 fingerprint=b6f31aaa78a3ce81326665f395e7a5f2698d4ae53b88874fa724f2dada2c01c8 issuer=2c1b5f8c0fc503970d14636df01f89eac17a8088ebadb87a131fe7ab5d926993 initiatorIndex=3182111111 responderIndex=131141699 handshake="map[stage:2 style:ix_psk0]" durationNs=208554750 sentCachedPackets=1
time=2026-10-19T21:21:02.618Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.10] localIndex=173963653 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=173963653 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:21:02.695Z level=INFO msg="Handshake message received" vpnAddrs=[10.0.10.10] from=<gateway-public-ip>:4242 certName=sgvps certVersion=2 fingerprint=5d8c3af750a49e2ee8e831ccdf8bfbc39b57902f4f32e6eb136349c2c922e6df issuer=2c1b5f8c0fc503970d14636df01f89ea

Here are the logs AFTER adding the unsafe route 0.0.0.0/0:

(Note that the unsafe route 0.0.0.0/0 is not indicated in the logs, the same way as on Android)

time=2026-10-19T21:33:41.420Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:outgoing endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-19T21:33:41.421Z level=INFO msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
time=2026-10-19T21:33:41.421Z level=INFO msg="Firewall started" firewallHashes=SHA:498215dec4e5687a2353f51c10838c113bd1af35ef72b8e8c9f536986ada5417,FNV:2782948616
time=2026-10-19T21:33:41.421Z level=INFO msg=listening addr=0.0.0.0:0
time=2026-10-19T21:33:41.421Z level=INFO msg="Main HostMap created" preferredRanges=[]
time=2026-10-19T21:33:41.422Z level=INFO msg="punchy disabled"
time=2026-10-19T21:33:41.422Z level=INFO msg="Loaded send_recv_error config" sendRecvError=always
time=2026-10-19T21:33:41.422Z level=INFO msg="Loaded accept_recv_error config" acceptRecvError=always
time=2026-10-19T21:33:41.422Z level=INFO msg="Nebula interface is active" interface=iOS networks=[10.0.10.25/24] build=1.11.1-0.20260922204501-7cfa47d569e8 udpAddr=[::]:63173 boringcrypto=false fips140Version=latest fips140Enabled=false fips140Enforced=false
time=2026-10-19T21:33:41.426Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.1] localIndex=2135332346 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=2135332346 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:41.427Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.10] localIndex=2072646949 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=2072646949 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:48.123Z level=INFO msg="Handshake timed out" vpnAddrs=[10.0.10.1] localIndex=2135332346 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=2135332346 durationNs=6697955208 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:48.124Z level=INFO msg="Handshake timed out" vpnAddrs=[10.0.10.10] localIndex=2072646949 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=2072646949 durationNs=6697459583 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:51.546Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.10] localIndex=2276813806 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=2276813806 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:51.547Z level=INFO msg="Handshake message sent" vpnAddrs=[10.0.10.1] localIndex=3326655433 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=3326655433 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:58.223Z level=INFO msg="Handshake timed out" vpnAddrs=[10.0.10.10] localIndex=2276813806 remoteIndex=0 udpAddrs=[<gateway-public-ip>:4242] initiatorIndex=2276813806 durationNs=6678485042 handshake="map[stage:1 style:ix_psk0]"
time=2026-10-19T21:33:58.223Z level=INFO msg="Handshake timed out" vpnAddrs=[10.0.10.1] localIndex=3326655433 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=3326655433 durationNs=6678948125 handshake="map[stage:1 style:ix_psk0]"

Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment thread docs/guides/unsafe_routes/index.mdx Outdated
Comment on lines +581 to +582
Then, you must ensure that underlay Nebula traffic bypasses the Nebula tunnel. For this, add explicit static routes in
your routing table for the public IPs of the lighthouse and any direct Nebula peers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static routes for the lighthouse work, but direct peers' public IPs come from the lighthouse at runtime and can change, so readers can't really pin those ahead of time. Maybe say that only the lighthouse(s) can realistically be pinned, and call out that this is the limitation on macOS/Windows?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point. I missed a lot of important details here.

Rewrote this part and ended up rewriting all other content related to full-tunnel routing.

Please check it again if you have a chance.

Comment on lines +552 to +558
so_mark: 0x6EB
```
After that, create a separate table for internal overlay network traffic by running:
```sh
ip route replace default via 10.0.0.5 dev nebula1 table 4242

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more on the Linux recipe, from your #1837 thread: John's Option B pairs so_mark with tun.use_system_route_table: true, so Nebula doesn't install default dev nebula1 in the main table. You got the same effect with install: false. As written, a reader who also adds the Step 4 unsafe_routes stanza for 0.0.0.0/0 gets that conflicting main-table default again. Could this show the full config? Either

listen:
  so_mark: 0x6EB
tun:
  use_system_route_table: true

with no unsafe_routes entry (the via on the table-4242 route is then required), or an unsafe_routes entry with install: false. Also, table 4242 holds the default route for all non-Nebula traffic, not internal overlay traffic. Maybe "a separate routing table for the full-tunnel default route"?

— Caleb + Cache

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewrite reads well, but the gap from this comment is still there. With only so_mark and the table-4242 route, the client has no unsafe_routes entry and no use_system_route_table, so Nebula drops the internet-bound packets it receives on nebula1. Could the snippet include one of the two configs above? Also, L557 still describes the table as holding "internal overlay network traffic". It actually holds the default route for everything except Nebula's own underlay traffic.

@f-hollow f-hollow Oct 20, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested use_system_route_table again and left a comment in #387. I will act on this comment after we test the two configurations and decide which one to document.

@jasikpark jasikpark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We ran the firewall steps from this PR (at 8f0cc95) live on fresh VMs with Nebula 1.11.2. The routing and NAT commands work as written for ufw, nftables, and iptables. Three of the persistence/setup notes don't hold up on a fresh host, and the ufw sed is confirmed unnecessary. Line comments below.

Test setup and results

Each run used a fresh Lima VM as subnet-gateway, with real systemd and real ufw/nft/iptables. nebula-roaming and the printer ran in network namespaces inside the VM, so roaming → printer traffic crossed the gateway's real FORWARD path. The printer had no default route, so replies only came back if the gateway's MASQUERADE worked. The probe was TCP to port 631 (see the note on ping below). The guide's eth0 was lan0 in the VMs.

Scenario Distro Result
ufw as written Ubuntu 24.04 works; leaves FORWARD policy ACCEPT
ufw without the DEFAULT_FORWARD_POLICY sed Ubuntu 24.04 works with policy DROP; survives a reboot
ufw with the sed, no ufw route allow Ubuntu 24.04 works, so the scoped rule has no effect
ufw with neither Ubuntu 24.04 TCP blocked; ping still passes
nftables as written Ubuntu 24.04, Debian 13 works; loading the file twice is fine
nftables include line after a reboot Ubuntu 24.04, Debian 13 rules not loaded until nftables.service is enabled
nftables policy drop vs. another table's accept Ubuntu 24.04 drop blocks the other table's flow; accept doesn't
ip_forward in /etc/sysctl.conf after a reboot Debian 13 not applied
ip_forward in /etc/sysctl.d/99-nebula.conf after a reboot Ubuntu 24.04, Debian 13 applied
iptables as written Ubuntu 24.04, FORWARD ACCEPT and DROP works
iptables -A vs. -I FORWARD 1/2 with an existing catch-all REJECT Ubuntu 24.04 -A blocked; -I works

Every command suggested in this review's line comments was run in these VMs. We didn't test iptables persistence, because the guide doesn't give specific commands for it.

— Caleb + Cache


This change will be lost upon reboot.

To make it persist across reboots, add `net.ipv4.ip_forward = 1` to `/etc/sysctl.conf`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Debian 13, /etc/sysctl.conf isn't shipped and isn't read at boot, so ip_forward came back as 0 after a reboot. A drop-in under /etc/sysctl.d/ survived a reboot on both Debian 13 and Ubuntu 24.04:

Suggested change
To make it persist across reboots, add `net.ipv4.ip_forward = 1` to `/etc/sysctl.conf`.
To make it persist across reboots, add it to a file under `/etc/sysctl.d/`:
```sh
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-nebula.conf
```

@f-hollow f-hollow Oct 15, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I am talking about in my general comment below.

I, personally, use a drop-in config on Debian for net.ipv4.ip_forward these days. I used to set it in /etc/sysctl.conf on Ubuntu years ago and it seemed to work fine. So I decided to simplify the instructions based on my prior experience without testing, because there are many more important things to test related to Nebula.

This is general knowledge that is already covered in so many places.

Comment on lines +304 to +313
Confirm the new rule is active:

```sh
$ sudo ufw status
Status: active

To Action From
-- ------ ----
192.168.1.0/24 on eth0 ALLOW FWD 10.0.0.0/24 on nebula1
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This output assumes ufw is already enabled. On a fresh Ubuntu host it's inactive: ufw reload prints Firewall not enabled (skipping reload) and ufw status shows Status: inactive. Could we add a line saying this section assumes ufw is already active? Enabling it also needs SSH and Nebula's UDP port allowed first, or it can lock you out.

#### nftables

Create a new nftables configuration (e.g. `/etc/nftables-routing.conf`) with the following configuration:
Create a new nftables configuration (e.g., `/etc/nftables/nebula.conf`). Configure the packet forwarding chain and then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/etc/nftables/ doesn't exist on fresh Ubuntu 24.04 or Debian 13, so creating the file fails until the directory exists. Could we add sudo mkdir -p /etc/nftables first? (The Debian 13 cloud image also doesn't include nft, so sudo apt install nftables may be needed there.)

Comment on lines +396 to +401
To make them persist across reboots, include this file in your main `/etc/nftables.conf` by adding the following line to
it:

```sh
include "/etc/nftables/nebula.conf"
```

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On both Ubuntu 24.04 and Debian 13, nftables.service is disabled by default, so the include line alone didn't load the rules after a reboot. It worked once the service was enabled:

Suggested change
To make them persist across reboots, include this file in your main `/etc/nftables.conf` by adding the following line to
it:
```sh
include "/etc/nftables/nebula.conf"
```
To make them persist across reboots, include this file in your main `/etc/nftables.conf` by adding the following line to
it, and enable the `nftables` service so the file is loaded at boot:
```sh
include "/etc/nftables/nebula.conf"
```
```sh
sudo systemctl enable nftables
```

As you can see in `msg="Added route"`, the unsafe route is being used.

Ping working? Great! Go ahead and add that printer, map file shares, and VNC to any host on that LAN.
Now you should be able to successfully ping any host on the subnet from your roaming host and access subnet devices:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a ufw host, ping isn't a reliable test of the routing rules: ufw's default before.rules forwards ICMP echo even with no ufw route allow rule. Maybe suggest also testing a TCP port (e.g. the printer's 631) to confirm forwarding works?

@f-hollow

Copy link
Copy Markdown
Author

@jasikpark Thank you for the detailed review and for testing the firewall configs!

I have already implemented half of the comments and will push the changes once I implement and test a few others.

Regarding the firewall setup instructions, there is one issue from the technical writing perspective.

When core documentation includes instructions for setting up third-party tools or the environment, maintainers become responsible for tracking external changes and keeping those instructions current. A common documentation best practice is to focus the core docs on the product itself and provide high-level guidance or prerequisites for third-party tools and the environment, whatever might not be obvious from standard practices.

If there is capacity to help less experienced users, a good alternative is to provide additional resources, for example a blog post, with more detailed explanations. In that case, changes in specific distros or third-party tools are much less likely to impact the accuracy of the core documentation. An outdated blog post will be perceived by users as something natural and can be updated periodically when there is capacity for it.

What do you think of keeping the firewall section high-level and, optionally, writing a blog post with more detailed instructions?

@jasikpark

jasikpark commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks @f-hollow, that's a fair argument, and I agree with the general principle. Normally I'd say the same thing: put the detailed walkthrough in a blog post, let it age like a blog post, and fold the lasting parts into the docs.

We talked it over internally, though, and we'd like to keep the iptables/nftables/ufw sections in this page. In Diátaxis terms, this page sits somewhere between a tutorial and a how-to guide. It's less a reference for how unsafe_routes works and more a worked example of setting it up end to end, and the firewall step is where people tend to get stuck. If we cut it down to high-level guidance, we'd lose most of what makes the guide useful.

To handle the maintenance concern, could you add an admonition (:::note or :::warning) at the top of the firewall section? It should say that these rules are an example: they assume a fairly clean host, they may interact with rules you already have, and the exact commands can vary by distro and version. Your link to each tool's upstream docs would fit well in that note too.

— Caleb + Cache

@f-hollow

f-hollow commented Oct 16, 2026 •

Copy link
Copy Markdown
Author

@jasikpark I have resolved the straightforward comments (a4bdc8f), removed the outdated pictures (2ed9431) and replied to the comments that might need your follow-up, including the rewritten full-tunnel routing section (4fb426c).

I will try to implement the rest of the comments by the end of the week.


Regarding the firewall rules, I agree with your decision to keep the detailed steps and add an admonition. The detailed walkthrough makes this guide (or rather tutorial as you pointed out) much more user-friendly.

I started using Tinc a few years ago with almost zero networking experience, and its documentation wasn't very beginner-friendly. Though I am sure it's very easy to maintain, and now that I have more experience, I like that it skips the basics. That's why I asked the question to see what you prefer.

I will fill in the firewall details according to your comments and see if any other things can be further improved.

@jasikpark jasikpark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround! The scp, local_cidr, ufw, nftables, and link fixes all look good, and the full-tunnel rewrite reads much more clearly.

Suggestion: split the full-tunnel section into a stacked follow-up PR. Steps 1–4 are a tutorial for reaching a subnet, and that part is close: once the firewall items from the last round land, it's ready to merge. Routing 0.0.0.0/0 is a different setup on the client side (routing loops, so_mark, the /1 routes), and it's where most of the open questions are. Moving it to its own PR, stacked on this one, would let this one merge without waiting on those. The full-tunnel comments below still apply wherever the section ends up. If you'd rather keep it here, that's fine too.

In this round:

  • The iptables -I change dropped the -i flags, so those two commands now fail (line comment).
  • The prerequisites now list macOS and Windows for the subnet gateway, but the steps are Linux-only (line comment).
  • For the full-tunnel section: the gateway checklist needs a couple of additions (line comment), and the Linux recipe still has the routing gap from the earlier thread (replied there).

No rush on the remaining firewall items and the open questions on L516 and L534. I know those are still in progress.

— Caleb + Cache

Comment on lines +403 to +404
sudo iptables -I FORWARD 1 nebula1 -o eth0 -s 10.0.0.0/24 -d 192.168.1.0/24 -j ACCEPT
sudo iptables -I FORWARD 2 eth0 -o nebula1 -s 192.168.1.0/24 -d 10.0.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The -i got dropped when switching from -A. Without it, iptables rejects nebula1 with Bad argument `nebula1'.

Suggested change
sudo iptables -I FORWARD 1 nebula1 -o eth0 -s 10.0.0.0/24 -d 192.168.1.0/24 -j ACCEPT
sudo iptables -I FORWARD 2 eth0 -o nebula1 -s 192.168.1.0/24 -d 10.0.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo iptables -I FORWARD 1 -i nebula1 -o eth0 -s 10.0.0.0/24 -d 192.168.1.0/24 -j ACCEPT
sudo iptables -I FORWARD 2 -i eth0 -o nebula1 -s 192.168.1.0/24 -d 10.0.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Comment on lines +61 to +62
- The **subnet gateway** running Linux, macOS, or Windows that will forward traffic from the overlay network to the
subnet using `unsafe_routes`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my earlier comment was ambiguous. Other OSes can act as the gateway, but this guide only shows Linux commands in Step 3, so the prerequisite should match the note above:

Suggested change
- The **subnet gateway** running Linux, macOS, or Windows that will forward traffic from the overlay network to the
subnet using `unsafe_routes`
- The **subnet gateway** running Linux that will forward traffic from the overlay network to the subnet using
`unsafe_routes`

Comment on lines +526 to +528
- Inbound Nebula firewall rules cover `local_cidr` for `0.0.0.0/0`
- System firewall rules for forwarding and NAT/masquerading do not contain destination restrictions (such as
`-d 192.168.1.0/24`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two additions here:

  • local_cidr: 0.0.0.0/0 also matches the gateway's own addresses, so overlay hosts can reach any port on the gateway itself that the rule allows. Worth a sentence, or a separate narrower rule for the gateway's services.
  • The guide uses three tools, so the "no destination restrictions" example could name each one: to 192.168.1.0/24 in ufw route allow, ip daddr in nft, -d in iptables. The forwarding and masquerade rules should still be limited to the uplink interface (out on eth0, oifname eth0, -o eth0) once the destination is removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Document use of local_cidr firewall rules in the firewall section of the unsafe_routes guide

2 participants