Every Mac keeps a routing table: the kernel’s list of where to send each outgoing packet. The system builds it from your network settings, so most of the time you never have to touch it. When you do need to look inside — to check which route an address uses, add one to reach a subnet, or push some traffic through a VPN — macOS hands the job to a few BSD command-line tools.
Three of them cover almost everything. netstat shows the table, route changes the live one, and networksetup makes a change survive a reboot. This guide walks through each in turn, starting with the task most people come here for: showing the routing table.
Showing the routing table
The one command to remember is netstat -rn. It prints the IPv4 and IPv6 tables together. -r selects the routing display, and -n prints numeric addresses instead of resolving hostnames. That is both faster and clearer:
Routing tables
Internet:
Destination Gateway Flags Netif Expire
default 192.168.1.1 UGScg en0
192.168.1/24 link#12 UCS en0 !
192.168.1.1 aa:bb:cc:dd:ee:ff UHLWIir en0 1165
127 127.0.0.1 UCS lo0
127.0.0.1 127.0.0.1 UH lo0
224.0.0/4 link#12 UmCS en0 !
The gateway address and the MAC address above are placeholders. Everything else is a real run — the columns, the flag combinations, and the layout are untouched. To show a single address family, add -f:
netstat -rn -f inet # IPv4 only
netstat -rn -f inet6 # IPv6 only
How many rows you see depends on the machine. A plain Mac shows only a handful: the default route, loopback, and the local network. Install VPN clients or container tools and the count climbs, because each one adds its own interfaces and routes.
Reading the columns and flags
Each row is one route. It carries a destination, the gateway or interface that reaches it, a set of status flags, and the interface name. The flags hold most of the meaning. man netstat documents 23 of them, far more than the handful most guides mention. That is enough to decode a packed string like UGScg:
| Flag | Meaning |
|---|---|
| U | Route is up (RTF_UP) |
| G | Requires forwarding through a gateway (RTF_GATEWAY) |
| H | Route is to a single host, not a network (RTF_HOST) |
| S | Manually added, static route (RTF_STATIC) |
| C | Generates a new route on use (RTF_CLONING) |
| c | Protocol-cloned from a parent route (RTF_PRCLONING) |
| L | Valid link-layer (MAC) address (RTF_LLINFO) |
| I | Tied to a specific interface scope (RTF_IFSCOPE) |
| W | Generated by cloning (RTF_WASCLONED) |
| R | Rejects on match, host unreachable (RTF_REJECT) |
| g | Route to the global internet, a policy hint (RTF_GLOBAL) |
| m | Represents a multicast address (RTF_MULTICAST) |
| r | Host is a default router (RTF_ROUTER) |
| B | Silently discard packets, a blackhole (RTF_BLACKHOLE) |
Read left to right, UGScg on the default route means five things at once. It is up, uses a gateway, is static, is protocol-cloned, and is tagged for the global internet. The route -n get command below spells the same flags out in full words when a letter is unclear.
The route for one destination
Scanning the whole table to find one entry is slow. Instead, route -n get asks the kernel which route applies to a single address. It only reads, so nothing changes:
$ route -n get default
route to: default
destination: default
mask: default
gateway: 192.168.1.1
interface: en0
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire
0 0 0 0 0 0 1500 0
The gateway here is a placeholder; the rest is a real run. Every metric on the last line reads zero, because nothing has forced TCP to record round-trip data for this route yet. The flags line is the same properties as the letters above, written out in full. That makes it a handy way to check a combination you do not recognise.
Adding a static route
Adding a route to a whole network looks like this:
sudo route -n add -net 192.168.50.0 -netmask 255.255.255.0 192.168.1.1
-net marks the destination as a network. For a single address, use -host instead. These commands are not run against this machine, since that would edit a live routing table. The syntax is what man route documents.
One modifier trips people up: -interface. On Wi-Fi or Ethernet it does not take the port name. Per route(8), when the destination needs no gateway, -interface wants the Mac’s own address on that network. The bare interface name is only accepted “if the interface is point to point.” So on a broadcast link it is the local IP, not en0:
sudo route -n add -net 192.168.1.0/24 -interface 192.168.1.50
Here 192.168.1.50 is the Mac’s own address on that network. An earlier version of this page used the bare en0 in this spot, which the man page does not support on Wi-Fi or Ethernet. The VPN section below is where a bare interface name is genuinely correct.
Removing and changing routes
route also documents delete, the blunter flush, and change for editing one property of an existing route without removing it:
sudo route -n delete -net 192.168.50.0 -netmask 255.255.255.0 192.168.1.1
sudo route -n change -net 192.168.50.0 -netmask 255.255.255.0 192.168.1.254
Be careful with flush. It clears every route in the table, the default route included. It is for resetting the whole table, not for removing one entry. To drop a single static route, use delete.
When add succeeds but nothing answers
route add performs one check, and only one. Per route(8), its single documented failure for add is Network is unreachable: the gateway “was not on a directly-connected network.” That is all it verifies. It checks whether the gateway sits on a connected network, not whether any host answers there.
| Gateway address | route add outcome |
|---|---|
| Not on any directly-connected network | Network is unreachable — refused |
| On a connected network, nothing answers there | Route is installed anyway |
The second case is not a bug. It is the same behaviour that lets -reject and -blackhole drop traffic on purpose. It is also why you can pre-install a route before its gateway comes online. add checks the address, never the host behind it.
Routes that survive a reboot
This is the most common follow-up. You add a route, reboot, and it is gone. That is expected, not a bug. route(8) calls itself a tool to “manually manipulate the routing tables” through a routing socket. There is no config file involved. It edits the kernel’s live state, so a fresh boot rebuilds that state and the manual entry is simply not there.
For a route that has to persist, Apple documents networksetup. Its man page calls it the “configuration tool for network settings in System Preferences.” In other words, it writes to the same store the Network pane uses, not just the running table:
# destination, subnet mask, gateway — in that order
sudo networksetup -setadditionalroutes "Wi-Fi" 192.168.50.0 255.255.255.0 192.168.1.1
# check what is configured
networksetup -getadditionalroutes "Wi-Fi"
# clear it (call with no route tuples)
sudo networksetup -setadditionalroutes "Wi-Fi"
The service name — “Wi-Fi” here — has to match one from networksetup -listallnetworkservices. The route belongs to that service, not to the system as a whole. For an IPv6 destination, use -setv6additionalroutes, which takes a prefix length instead of a subnet mask.
Split-tunneling through a VPN
Sending one network’s traffic through a VPN is the other frequent case. It is also where the point-to-point exception matters. macOS VPN clients create a utun interface, and utun interfaces are point-to-point. So here the bare interface name is correct, not a mistake:
# send one network's traffic through the tunnel
sudo route -n add -net 10.0.0.0/8 -interface utun0
# everything else keeps the existing default route
The tunnel number — utun0, utun1, and so on — is assigned when the client connects, and it is not fixed. Check ifconfig -a or netstat -rn after connecting to see which one is active. And per the reboot rule above, a route added this way lasts only for that session. Most VPN clients re-add their own routes on each connect, so this rarely needs scripting.
Windows and Linux habits
route print, the Windows way to dump the table, has no macOS equivalent. Pass the keyword and route simply rejects it:
$ route print
route: bad keyword: print
usage: route [-dnqtv] command [[modifiers] args]
Linux is closer, but still different. Both the older route -n and the newer ip route print a table there. However, the names and flag syntax do not carry over to the BSD-derived tools on a Mac. The Destination / Gateway / Flags / Netif layout above is specific to netstat -rn, and ip route output looks nothing like it.
One last habit is worth keeping: check the exit status. Say a route added with sudo route add does not appear in netstat -rn afterward. route did not fail silently. It exits non-zero and prints a reason — Network is unreachable, routing table overflow, and others listed in the route(8) DIAGNOSTICS section.
Quick reference
| Task | Command |
|---|---|
| Show the routing table | netstat -rn |
| IPv4 or IPv6 only | netstat -rn -f inet / -f inet6 |
| Route for one address | route -n get ADDR |
| Add a network route | sudo route -n add -net DEST -netmask MASK GW |
| Delete a route | sudo route -n delete -net DEST -netmask MASK GW |
| Change a route’s gateway | sudo route -n change -net DEST -netmask MASK GW |
| Make a route persist | sudo networksetup -setadditionalroutes "Wi-Fi" DEST MASK GW |
| Route through a VPN | sudo route -n add -net DEST -interface utunN |
The short version: netstat -rn shows the routing table, route edits the live one, and networksetup is what makes a route outlast a reboot. If you came from Windows expecting route print, that keyword does not exist here, and netstat -rn is its replacement.