Adding routes with Netplan
When you need to send one destination through a different router on Ubuntu 26.04 LTS Server, the route goes into Netplan’s routes:. You can also add it with ip route add, but that one disappears on reboot.
Configuring just the default route is covered in Network Configuration (Netplan). This article covers per-destination routes, selection by metric, on-link, IPv6, and policy routing.
How routes is written
routes: goes under the interface, listing one route per entry.
| Key | Meaning |
|---|---|
to | The destination, as a prefix (10.130.1.0/24) or default |
via | The next hop. An address inside that interface’s subnet |
metric | The preference when several routes share a destination. Lower wins |
on-link | Set to true when via sits outside the subnet |
table | The routing table number the route goes into. Defaults to main |
Create a new file under /etc/netplan/. Leave the 50-cloud-init.yaml that cloud-init wrote alone. A higher leading number is read later.
Set the permissions to 600. Netplan files may hold credentials, so the convention is to keep them unreadable to anyone but root.
sudo tee /etc/netplan/70-static-routes.yaml <<'EOF'
network:
version: 2
ethernets:
ens3:
routes:
- to: 10.130.1.0/24
via: 192.168.100.1
- to: 10.130.2.0/24
via: 192.168.100.2
EOF
sudo chmod 600 /etc/netplan/70-static-routes.yaml
sudo netplan applyOnce applied, the routes appear in ip route marked proto static.
ip route
ip route get 10.130.1.1kazulog@sv1:~$ ip route
default via 10.19.0.1 dev ens2 proto static
10.19.0.0/16 dev ens2 proto kernel scope link src 10.19.12.11
10.130.1.0/24 via 192.168.100.1 dev ens3 proto static
10.130.2.0/24 via 192.168.100.2 dev ens3 proto static
192.168.100.0/24 dev ens3 proto kernel scope link src 192.168.100.10
kazulog@sv1:~$ ip route get 10.130.1.1
10.130.1.1 via 192.168.100.1 dev ens3 src 192.168.100.10 uid 1000
cache
kazulog@sv1:~$ ip route get 10.130.2.1
10.130.2.1 via 192.168.100.2 dev ens3 src 192.168.100.10 uid 1000
cache ip route get tells you which route the kernel actually picks. When there are many routes and the ip route listing is hard to read, this is the reliable answer.
netplan get shows what you have written. It merges every file, so you get the whole picture even if you forget which file holds what.
kazulog@sv1:~$ sudo netplan get ethernets.ens3.routes
- to: "10.130.1.0/24"
via: "192.168.100.1"
- to: "10.130.2.0/24"
via: "192.168.100.2"
- metric: 100
to: "10.130.9.0/24"
via: "192.168.100.1"
- metric: 200
to: "10.130.9.0/24"
via: "192.168.100.2"Several routes to the same destination (metric)
Write two routes to the same destination and the lower metric is used. The other stays in the table as a backup.
- to: 10.130.9.0/24
via: 192.168.100.1
metric: 100
- to: 10.130.9.0/24
via: 192.168.100.2
metric: 200kazulog@sv1:~$ ip route show 10.130.9.0/24
10.130.9.0/24 via 192.168.100.1 dev ens3 proto static metric 100
10.130.9.0/24 via 192.168.100.2 dev ens3 proto static metric 200
kazulog@sv1:~$ ip route get 10.130.9.1
10.130.9.1 via 192.168.100.1 dev ens3 src 192.168.100.10 uid 1000
cache Swap the metrics and the selection moves.
- to: 10.130.9.0/24
via: 192.168.100.1
metric: 100
- to: 10.130.9.0/24
via: 192.168.100.2
metric: 50kazulog@sv1:~$ ip route show 10.130.9.0/24
10.130.9.0/24 via 192.168.100.2 dev ens3 proto static metric 50
10.130.9.0/24 via 192.168.100.1 dev ens3 proto static metric 100
kazulog@sv1:~$ ip route get 10.130.9.2
10.130.9.2 via 192.168.100.2 dev ens3 src 192.168.100.10 uid 1000
cache to: default on another interface can outrank the management default depending on its metric, and then you lose your remote session.When via sits outside the subnet (on-link)
The address in via normally has to be inside the interface’s subnet. Write one outside it and netplan apply reports no error, yet the route is never installed.
kazulog@sv1:~$ sudo cat /etc/netplan/71-onlink.yaml
network:
version: 2
ethernets:
ens3:
routes:
- to: 10.140.0.0/16
via: 192.168.200.1
kazulog@sv1:~$ sudo netplan apply
kazulog@sv1:~$ ip route show 10.140.0.0/16
kazulog@sv1:~$ sudo grep -A3 -B1 "10.140.0.0/16" /run/systemd/network/10-netplan-ens3.network
[Route]
Destination=10.140.0.0/16
Gateway=192.168.200.1Netplan did generate the systemd-networkd configuration. It is the kernel that drops the route, because it has no way to reach that next hop.
Adding on-link: true tells the kernel that the via is on the same link, and the route goes in.
kazulog@sv1:~$ sudo cat /etc/netplan/71-onlink.yaml
network:
version: 2
ethernets:
ens3:
routes:
- to: 10.140.0.0/16
via: 192.168.200.1
on-link: true
kazulog@sv1:~$ ip route show 10.140.0.0/16
10.140.0.0/16 via 192.168.200.1 dev ens3 proto static onlink
kazulog@sv1:~$ sudo grep -A4 -B1 "10.140.0.0/16" /run/systemd/network/10-netplan-ens3.network
[Route]
Destination=10.140.0.0/16
Gateway=192.168.200.1
GatewayOnLink=trueThe ip route line ends with onlink. Use it when the router’s address lives on another segment, or when the interface carries a /32.
IPv6 static routes
The syntax is the same as IPv4: put IPv6 addresses in to and via. IPv4 and IPv6 entries can sit in the same routes: list.
- to: 2001:db8:130:1::/64
via: 2001:db8:100::1
- to: 2001:db8:130:2::/64
via: 2001:db8:100::2kazulog@sv1:~$ ip -6 route
2001:db8:100::/64 dev ens3 proto kernel metric 256 pref medium
2001:db8:130:1::/64 via 2001:db8:100::1 dev ens3 proto static metric 1024 pref medium
2001:db8:130:2::/64 via 2001:db8:100::2 dev ens3 proto static metric 1024 pref medium
fe80::/64 dev ens2 proto kernel metric 256 pref medium
fe80::/64 dev ens3 proto kernel metric 256 pref medium
kazulog@sv1:~$ ip -6 route get 2001:db8:130:1::1
2001:db8:130:1::1 from :: via 2001:db8:100::1 dev ens3 proto static src 2001:db8:100::10 metric 1024 pref mediumIPv6 routes get a metric of 1024 by default even when you do not write one. That differs from the IPv4 default of 0, so check the real values in ip -6 route before relying on metric.
Choosing a route by source address (policy routing)
To select routes by source address rather than destination, put the routes in a separate table and have routing-policy look that table up.
sudo tee /etc/netplan/72-policy.yaml <<'EOF'
network:
version: 2
ethernets:
ens3:
addresses:
- 192.168.100.110/24
routes:
- to: default
via: 192.168.100.1
metric: 50
table: 100
- to: default
via: 192.168.100.2
metric: 100
table: 100
routing-policy:
- from: 192.168.100.110
table: 100
EOF
sudo chmod 600 /etc/netplan/72-policy.yaml
sudo netplan applyRoutes carrying table: 100 stay out of main and land in table number 100. Only traffic matching the from in routing-policy looks there. You do not need to register a table name in /etc/iproute2/rt_tables.
ip rule
ip route show table 100
ip route get 10.130.9.1
ip route get 10.130.9.1 from 192.168.100.110kazulog@sv1:~$ ip rule
0: from all lookup local
32765: from 192.168.100.110 lookup 100 proto static
32766: from all lookup main
32767: from all lookup default
kazulog@sv1:~$ ip route show table 100
default via 192.168.100.1 dev ens3 proto static metric 50
default via 192.168.100.2 dev ens3 proto static metric 100
kazulog@sv1:~$ ip route get 10.130.9.1
10.130.9.1 via 192.168.100.2 dev ens3 src 192.168.100.10 uid 1000
cache
kazulog@sv1:~$ ip route get 10.130.9.1 from 192.168.100.110
10.130.9.1 from 192.168.100.110 via 192.168.100.1 dev ens3 table 100 uid 1000
cache The next hop changes for the same destination, but only when the source is 192.168.100.110. Rule 32765 sits below main at 32766, so it is consulted first.
Table 100 holds two default routes, and the lower metric wins there as well. This lets you try several default routes without touching main.
The change shows up in real traffic too. Pick the source address with -I.
kazulog@sv1:~$ ping -c3 -W2 -I 192.168.100.110 10.130.9.1
PING 10.130.9.1 (10.130.9.1) from 192.168.100.110 : 56(84) bytes of data.
64 bytes from 10.130.9.1: icmp_seq=1 ttl=255 time=1.18 ms
64 bytes from 10.130.9.1: icmp_seq=2 ttl=255 time=1.66 ms
64 bytes from 10.130.9.1: icmp_seq=3 ttl=255 time=1.57 ms
--- 10.130.9.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.176/1.468/1.660/0.210 msip route add is temporary
A route added with ip route add goes straight into the kernel table and Netplan knows nothing about it. It disappears on netplan apply or on reboot.
kazulog@sv1:~$ sudo ip route add 10.150.0.0/16 via 192.168.100.1
kazulog@sv1:~$ ip route show 10.150.0.0/16
10.150.0.0/16 via 192.168.100.1 dev ens3
kazulog@sv1:~$ sudo netplan apply
kazulog@sv1:~$ ip route show 10.150.0.0/16It is handy while you are narrowing a problem down. Routes meant to stay belong in Netplan.
Restoring the defaults
Delete the files you added and run netplan apply. The routes and the extra address go away with them.
sudo rm /etc/netplan/70-static-routes.yaml
sudo netplan applykazulog@sv1:~$ ls /etc/netplan/
50-cloud-init.yaml
kazulog@sv1:~$ ip -br a show ens3
ens3 UP 192.168.100.10/24 2001:db8:100::10/64 fe80::5054:ff:feb3:bb22/64
kazulog@sv1:~$ ip route
default via 10.19.0.1 dev ens2 proto static
10.19.0.0/16 dev ens2 proto kernel scope link src 10.19.12.11
192.168.100.0/24 dev ens3 proto kernel scope link src 192.168.100.10
kazulog@sv1:~$ ip rule
0: from all lookup local
32766: from all lookup main
32767: from all lookup defaultTest environment and session logs
The examples were captured on CML with an Ubuntu 26.04 LTS Server (sv1) and two Cisco routers (R1 and R2) on one segment.
sv1 (Ubuntu 26.04) R1 (IOS XE 17.03.08a) R2 (IOS XE 17.03.08a)
ens3 192.168.100.10/24 Gi2 192.168.100.1/24 Gi2 192.168.100.2/24
2001:db8:100::10/64 2001:db8:100::1/64 2001:db8:100::2/64
Lo1 10.130.1.1/24 Lo1 10.130.2.1/24
2001:db8:130:1::1/64 2001:db8:130:2::1/64
Lo9 10.130.9.1/32 Lo9 10.130.9.2/32
| | |
+---------------------------+--------- LAB-SW ---------+The topology gives the server two next hops. Within 10.130.9.0/24, R1 owns only .1 and R2 only .2, and each router blackholes the rest to Null0. Changing the metric therefore flips not only what ip route get prints but whether the ping succeeds.
| Step | Session log | Configuration | syslog |
|---|---|---|---|
| Initial state (no routes) | show | conf | log |
| Adding per-destination routes | show | conf | log |
| Two routes, metric 100 and 200 | show | conf | log |
| Swapping the metrics | show | conf | log |
| Without on-link (no route installed) | show | — | — |
| With on-link | show | conf | log |
| IPv6 routes | show | conf | log |
| Policy routing | show | conf | log |
| ip route add and netplan apply | show | conf | log |
| Restoring the defaults (final state) | show | conf | log |
Records from R1 and R2, which served as the next hops. The running-config is the configuration under test.
| Router | show output | syslog | running-config |
|---|---|---|---|
| R1 | show | log | run |
| R2 | show | log | run |
Reference
Related articles
- Changing the Hostname on Ubuntu 26.04 LTS Server (hostnamectl)
- Updating Packages on Ubuntu 26.04 LTS Server (apt update / upgrade)
- Setting the Timezone and Time Synchronisation on Ubuntu 26.04 LTS Server
- Creating Users and Granting sudo Privileges on Ubuntu 26.04 LTS Server
- Configuring the SSH Server on Ubuntu 26.04 LTS Server
- Managing Services with systemctl and Reading Logs with journalctl on Ubuntu 26.04 LTS Server
- Configuring Automatic Updates on Ubuntu 26.04 LTS Server (unattended-upgrades)
- Automating the Initial Setup of Ubuntu 26.04 LTS Server with cloud-init
- Configuring Kernel Parameters on Ubuntu 26.04 LTS Server (sysctl)
- Configuring the Network on Ubuntu 26.04 LTS Server (Netplan)
- Configuring Name Resolution on Ubuntu 26.04 LTS Server (systemd-resolved)
- Changing the NTP Source on Ubuntu 26.04 LTS Server (chrony)
- Static Routes on Ubuntu 26.04 LTS Server (Netplan)
- How to Install the Latest neovim from the Official Site on Ubuntu 26.04 LTS Server