Overview

Apex is a lab machine located in E4-07-11. It uses two network interfaces with policy-based routing:

  • WiFi (wlo1) — default route, internet, Tailscale
  • Ethernet (enp6s0) — NUS campus network, inbound access (SSH, ping)

The system uses dual policy routing (interface-based + source-based) to ensure traffic arriving on Ethernet replies via Ethernet, avoiding asymmetric routing failures.

Network Interfaces

InterfaceRoleIP
wlo1Default route, internet, Tailscale172.24.x.x
enp6s0Campus network access10.246.87.232
tailscale0Overlay networkBound to WiFi

Network Topology

graph TB
    subgraph Internet
        WEB["🌐 Internet"]
        TS["🔒 Tailscale Network"]
    end

    subgraph "NUS Campus"
        GW["NUS Gateway<br/>10.246.80.1"]
        CAMPUS["Campus Devices<br/>(SSH, ping)"]
    end

    subgraph "Apex — E4-07-11"
        WIFI["wlo1 (WiFi)<br/>172.24.x.x<br/>Primary route"]
        ETH["enp6s0 (Ethernet)<br/>10.246.87.232<br/>Campus route"]
        HOST["Apex<br/>Linux Host"]
    end

    WEB <-->"default route<br/>(table main)"<--> WIFI
    TS <--> "tailscale0<br/>(bound to wlo1)" <--> WIFI
    CAMPUS <--> "campus traffic<br/>(table campus)" <--> ETH

    WIFI --- HOST
    ETH --- HOST

    style WIFI fill:#4a9,color:#fff
    style ETH fill:#48d,color:#fff
    style HOST fill:#f90,color:#fff
    style GW fill:#999,color:#fff

Routing Logic

flowchart LR
    IN["Incoming packet<br/>from campus"] --> ETH["Arrives on<br/>enp6s0"]
    ETH --> RULE1{"iif enp6s0<br/>rule match?"}
    RULE1 -->|"Yes"| CAMPUS["→ table campus<br/>→ reply via enp6s0"]
    
    LOCAL["Local process<br/>generates reply"] --> RULE2{"from 10.246.87.232<br/>rule match?"}
    RULE2 -->|"Yes"| CAMPUS

    OUT["Outbound traffic<br/>(internet)"] --> MAIN["→ table main<br/>→ via wlo1"]

    style RULE1 fill:#48d,color:#fff
    style RULE2 fill:#48d,color:#fff
    style CAMPUS fill:#4a9,color:#fff
    style MAIN fill:#f90,color:#fff

The Problem

Without policy routing:

  • Incoming campus traffic arrives via enp6s0
  • Replies go out via wlo1 (default route)
  • Connection fails due to asymmetric routing

Using only iif enp6s0 is not enough — locally generated replies also need to go via enp6s0.

Setup

1. Disable default route on Ethernet

nmcli connection modify enp6s0 ipv4.never-default yes
nmcli connection modify enp6s0 ipv4.ignore-auto-dns yes

2. Add routing table

Edit /etc/iproute2/rt_tables, add:

100 campus

3. Add both policy routing rules

ip rule add from 10.246.87.232 lookup campus
ip rule add iif enp6s0 lookup campus
RulePurpose
from 10.246.87.232Routes locally generated replies via enp6s0
iif enp6s0Routes forwarded/arriving traffic back via enp6s0

4. Add campus route

ip route add default via 10.246.80.1 dev enp6s0 table campus

5. Make it persistent

Create /etc/NetworkManager/dispatcher.d/10-enp6s0-policy-routing:

#!/bin/bash
IFACE="$1"
 
if [ "$IFACE" = "enp6s0" ]; then
  ip route replace default via 10.246.80.1 dev enp6s0 table campus
  ip rule show | grep -q "from 10.246.87.232 lookup campus" || \
    ip rule add from 10.246.87.232 lookup campus
  ip rule show | grep -q "iif enp6s0 lookup campus" || \
    ip rule add iif enp6s0 lookup campus
fi
chmod +x /etc/NetworkManager/dispatcher.d/10-enp6s0-policy-routing

Traffic Summary

Traffic TypeInterface
Internet / Tailscalewlo1
Campus inbound (SSH, ping)enp6s0
Replies to campus trafficenp6s0

Verification

ip rule
# Expected:
# from 10.246.87.232 lookup campus
# from all iif enp6s0 lookup campus
 
ip route show table campus
# Expected: default via 10.246.80.1 dev enp6s0

Key Insight

Multi-NIC asymmetric routing requires both rules:

  • Interface-based (iif) — handles forwarded traffic
  • Source-based (from) — handles locally generated replies

Using only one will fail silently.

See Also