Skip to main content
  1. Linux Articles/
  2. Ubuntu 26.04 LTS Server/

Static Routes on Ubuntu 26.04 LTS Server (Netplan)

Table of Contents

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.

KeyMeaning
toThe destination, as a prefix (10.130.1.0/24) or default
viaThe next hop. An address inside that interface’s subnet
metricThe preference when several routes share a destination. Lower wins
on-linkSet to true when via sits outside the subnet
tableThe 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.

Command to create the configuration file
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 apply

Once applied, the routes appear in ip route marked proto static.

Commands to check the routes
ip route
ip route get 10.130.1.1
Per-destination routes - example
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 
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.

netplan get - example
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.

Setting the preference with metric
        - 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: 200
The metric 100 route wins - example
kazulog@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.

Preferring the 192.168.100.2 side instead
        - 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: 50
The metric 50 route wins - example
kazulog@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 
Check that a new default route does not take over the management path. A 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.

Without on-link - example
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.1

Netplan 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.

With on-link - example
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=true

The 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.

IPv6 routes
        - to: 2001:db8:130:1::/64
          via: 2001:db8:100::1
        - to: 2001:db8:130:2::/64
          via: 2001:db8:100::2
IPv6 routes - example
kazulog@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 medium

IPv6 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.

Configuring policy routing
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 apply

Routes 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.

Commands to check policy routing
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.110
Policy routing - example
kazulog@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.

ping with a chosen source - example
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 ms

ip 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.

ip route add - example
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/16

It 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.

Commands to restore the defaults
sudo rm /etc/netplan/70-static-routes.yaml
sudo netplan apply
Back to the initial state - example
kazulog@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 default

Test 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.

Test topology
  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.

StepSession logConfigurationsyslog
Initial state (no routes)showconflog
Adding per-destination routesshowconflog
Two routes, metric 100 and 200showconflog
Swapping the metricsshowconflog
Without on-link (no route installed)show
With on-linkshowconflog
IPv6 routesshowconflog
Policy routingshowconflog
ip route add and netplan applyshowconflog
Restoring the defaults (final state)showconflog

Records from R1 and R2, which served as the next hops. The running-config is the configuration under test.

Routershow outputsyslogrunning-config
R1showlogrun
R2showlogrun

Reference

Netplan reference - routes

Related articles

Ubuntu official pages