Understanding FreeBSD’s Route Metric

Route metrics are a fundamental networking concept that determine which path traffic takes when multiple routes to the same destination exist. FreeBSD CURRENT now has full metric support in the routing stack.

Let me walk you through how it works with a practical example.

What is a Metric?

A metric is a numeric value assigned to a route that influences route selection.

When you have multiple routes to the same destination, the kernel prefers the route with the lowest metric.

Think of it as a “cost”, lower cost = preferred path.

Default Metric Behavior

When you create a route without specifying a metric, FreeBSD assigns a default metric of 1.

Wildcard Metric Behavior

We don’t allow you to create a route with metric of 0, That one is used for wildcard. You can delete all of the nexthops without specifiying route metric.

Its netlink behavior is also consistent. Either you don’t have RTA_PRIORITY or you have one with metric of zero.

If you’re creating a route, it means you’re not specifying its metric, which means default metric (1). If you’re deleting a route, it means you don’t care about its metric, you want to delete all of them.

# Create a route without specifying metric
route -6 add -net 3fff:a::/64 -gateway 3fff::1

# The route automatically gets metric 1
route -n6 get 3fff:a::/64
netstat -rn6W

This is useful because it means all “equal” routes start with the same priority, and you can selectively add routes with higher metrics as fallbacks.

Scenario: Building a Tiered Routing

Let’s say you’re a network operator managing traffic to network 3fff:a::/64. You want:

  1. Primary paths (metric 10): Two equal-cost gateways for load balancing
  2. Secondary path (metric 20): A backup gateway that’s only used if primaries fail
  3. Fallback path (metric 30): A better gateway that becomes available later

Step 1: Add Primary ECMP Routes

You start by creating an Equal-Cost Multi-Path (ECMP) setup with two gateways at metric 10:

For the sake of this example, I want this ECMP route to be unbalanced.

% route -6 add -net 3fff:a::/64 -gateway 3fff::2 -metric 10
add net 3fff:a::/64: gateway 3fff::2 fib 0
% route -6 add -net 3fff:a::/64 -gateway 3fff::3 -weight 2 -metric 10
% route -on6 get 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 2
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::3 iface vtnet1 metric 10 weight 2 mtu 1500

The -weight 2 parameter indicates these are NOT equally balanced within the ECMP group. Traffic to 3fff:a::/64 now load-balances between the two gateways.

Verify unbalanced ECMP

See the value of Slots column:

% netstat -On6W
Nexthop groups data

Internet6:
GrpIdx  NhIdx   Weight  Slots                       Gateway Netif  Refcnt
14      ------- ------- ------- --------------------------- -----       2
        12      1       1                           3fff::2 vtnet1
        13      2       2                           3fff::3 vtnet1

Current best route: 3fff::2 or 3fff::3 (metric 10) Backup: (none yet)

Step 2: Add a Backup Route

Next, you add a backup gateway with a higher metric (20), so it’s only used if the primary routes fail:

% route -6 add -net 3fff:a::/64 -gateway 3fff::1 -metric 20
add net 3fff:a::/64: gateway 3fff::1 fib 0
% # Verify the results
% route -on6 get 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 3
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::3 iface vtnet1 metric 10 weight 2 mtu 1500
via gw 3fff::1 iface vtnet1 metric 20 weight 1 mtu 1500
% netstat -On6W
Nexthop groups data

Internet6:
GrpIdx  NhIdx   Weight  Slots                       Gateway Netif  Refcnt
16      ------- ------- ------- --------------------------- -----       2
        12       1       0                          3fff::2 vtnet1
        13       2       2                          3fff::3 vtnet1
        15       1       0                          3fff::1 vtnet1

The system now has three routes, but only uses the metric 10 paths:

Current best route: 3fff::2 or 3fff::3 (metric 10) Backup: 3fff::1 (metric 20), waiting in the RIB

Step 3: Attempting Duplicates (What Fails)

If you try to add the same gateway with the same metric again, the system rejects it:

% route -6 add -net 3fff:a::/64 -gateway 3fff::1 -metric 20
route: message indicates error: File exists
add net 3fff:a::/64: gateway 3fff::1 fib 0: route already in table

This prevents accidental duplicates.

Step 4: Promoting a Better Gateway

Now your new, faster gateway 3fff::1 becomes available. You can immediately promote it to metric 5, better than the current metric 20 routes:

% route -6 add -net 3fff:a::/64 -gateway 3fff::1 -metric 5
add net 3fff:a::/64: gateway 3fff::1 fib 0
% route -on6 get 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 4
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::3 iface vtnet1 metric 10 weight 2 mtu 1500
via gw 3fff::1 iface vtnet1 metric 5 weight 1 mtu 1500
via gw 3fff::1 iface vtnet1 metric 20 weight 1 mtu 1500
% netstat -On6W
Nexthop groups data

Internet6:
GrpIdx  NhIdx   Weight  Slots                      Gateway Netif  Refcnt
17      ------- ------- ------- -------------------------- -----       2
        12       1       0                         3fff::2 vtnet1
        13       2       0                         3fff::3 vtnet1
        14       1       1                         3fff::1 vtnet1
        15       1       0                         3fff::1 vtnet1

The routing table now has four routes, but the best one changes:

Current best route: 3fff::1 (metric 5, NEW PRIMARY) Alternate: 3fff::2, 3fff::3 (metric 10) Backup: 3fff::1 (metric 20, This one is shadowed now)

Advanced: Selective Route Deletion

The metric system also enables fine-grained route deletion. You can delete routes by:

  • Gateway + Metric: Remove a specific route
  • Gateway only: Remove all routes to that gateway
  • Metric only: Remove all routes with that metric

Example: Clean Up by Metric

After the gateway 3fff::1 at metric 5 has proven stable, you can remove the old metric 20 routes without affecting your ECMP setup:

% route -n6 delete -net 3fff:a::/64 -metric 20
delete net 3fff:a::/64 fib 0
% route -on6 get 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 3
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::3 iface vtnet1 metric 10 weight 2 mtu 1500
via gw 3fff::1 iface vtnet1 metric 5 weight 1 mtu 1500

Now you have:

  • 3fff::1 (metric 5) - best
  • 3fff::2, 3fff::3 (metric 10) - secondaries

Or, remove a specific gateway from ECMP without touching other metrics:

% route -n6 delete -net 3fff:a::/64 -gateway 3fff::3 -metric 10
delete net 3fff:a::/64: gateway 3fff::3 fib 0
% route -on6 get 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 2
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::1 iface vtnet1 metric 5 weight 1 mtu 1500
% netstat -On6W
Nexthop groups data

Internet6:
GrpIdx  NhIdx   Weight  Slots                      Gateway Netif  Refcnt
15      ------- ------- ------- -------------------------- -----       2
        12      1       0                          3fff::2 vtnet1
        14      1       1                          3fff::1 vtnet1

Viewing Metrics in netstat

You can now see metric values in netstat output:

netstat -rn6 -W  # Wide output includes metric
netstat -rn6 --libxo json  # Structured output with metric field

metric filtering using netstat

You can find the metric attribute in libxo output.

% netstat -rn6 --libxo json | jq '.statistics."route-information"."route-table"."rt-family".[]."rt-entry".[] | select(.destination | contains("3fff:a::/64"))'
{
    "destination": "3fff:a::/64",
      "gateway": "3fff::2",
      "flags": "UGS",
      "flags_pretty": [
        "up",
      "gateway",
      "static"
        ],
      "weight": 1,
      "metric": 10,
      "nhg-kidx": 15,
      "interface-name": "vtnet1"
}
{
    "destination": "3fff:a::/64",
      "gateway": "3fff::1",
      "flags": "UGS",
      "flags_pretty": [
        "up",
      "gateway",
      "static"
        ],
      "weight": 1,
      "metric": 5,
      "nhg-kidx": 15,
      "interface-name": "vtnet1"
}

Viewing all nexthops of a route

You can now see all the nexthops of a route in route output:

% route -on 3fff:a::/64
route to: 3fff:a::
destination: 3fff:a::
mask: ffff:ffff:ffff:ffff::
fib: 0
flags: <UP,GATEWAY,DONE,STATIC>
nhops: 3
via gw 3fff::2 iface vtnet1 metric 10 weight 1 mtu 1500 table inet6.0
via gw 3fff::3 iface vtnet1 metric 10 weight 2 mtu 1500
via gw 3fff::1 iface vtnet1 metric 5 weight 1 mtu 1500

Bird

By default bird uses metric value of 32 during route installation, you can overwrite it by setting metric keyword in kernel section:

protocol kernel kernel6 {
	metric 1;
	ipv6 {
		import all;
		export all;
	};
	learn all;
	merge paths;
}