Skip to content

Commit 6c7c9d4

Browse files
committed
feat: native netlink route flush, agent-accelerated teardown, CI suite
Add --flush-routes <iface>[:family] to rxnm-agent. Performs RTM_GETROUTE dump filtered by interface index (RTA_OIF), then fires RTM_DELROUTE for each match. Completes the zero-loss teardown sequence alongside --flush-addrs — static routes and default gateways that survive address removal are now scrubbed via raw netlink. Shell-side: - _task_set_link(): flush-routes called after flush-addrs on disable - action_route_flush(): agent path with --iface for interface-scoped flush - Global IPv6 disable: flush addrs+routes per-interface during teardown All flush paths follow agent-preferred/ip-fallback pattern consistently across interfaces, routes, and system modules. CI: - Add v1.1.1 branch to workflow triggers - Add v1.1.1 regression suite step (28-point structural validation) - tests/test_v111_regressions.sh covers: flock compat, confirm_action, reload ordering, flush-addrs/flush-routes integration, profile .default tracker, route agent acceleration, 80-wired.network Type= removal
1 parent e77ab7a commit 6c7c9d4

6 files changed

Lines changed: 312 additions & 14 deletions

File tree

.github/workflows/integration.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: RXNM CI (Native Nspawn)
22

33
on:
44
push:
5-
branches: [ "main", "master", "v1.1.0", "1.1.x-CI", "agentic-review" ]
5+
branches: [ "main", "master", "v1.1.0", "v1.1.1", "1.1.x-CI", "agentic-review" ]
66
pull_request:
7-
branches: [ "main", "master", "v1.1.0" ]
7+
branches: [ "main", "master", "v1.1.0", "v1.1.1" ]
88

99
jobs:
1010
unit-and-posix:
@@ -28,6 +28,12 @@ jobs:
2828
export GITHUB_ACTIONS=true
2929
make test-all
3030
31+
- name: "v1.1.1 Regression Suite"
32+
run: |
33+
echo "=== v1.1.1 Structural Regression Tests ==="
34+
chmod +x tests/test_v111_regressions.sh
35+
bash tests/test_v111_regressions.sh
36+
3137
- name: "Path B: POSIX/dash compatibility"
3238
run: |
3339
echo "=== Step 1: dash -n syntax check on lib/*.sh ==="

lib/rxnm-interfaces.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,20 +154,25 @@ _task_set_link() {
154154
reload_networkd
155155
reconfigure_iface "$iface"
156156

157-
# Flush addresses for disabled protocols using agent to bypass busybox limitations
157+
# Flush addresses and routes for disabled protocols using agent's native
158+
# netlink to completely bypass busybox ip deficiencies.
158159
if [ "$ipv6" = "off" ]; then
159160
if [ -x "$RXNM_AGENT_BIN" ]; then
160161
"$RXNM_AGENT_BIN" --flush-addrs "$iface:6" >/dev/null 2>&1 || true
162+
"$RXNM_AGENT_BIN" --flush-routes "$iface:6" >/dev/null 2>&1 || true
161163
else
162164
ip -6 addr flush dev "$iface" scope global 2>/dev/null || true
163165
ip -6 addr flush dev "$iface" scope link 2>/dev/null || true
166+
ip -6 route flush dev "$iface" 2>/dev/null || true
164167
fi
165168
fi
166169
if [ "$ipv4" = "off" ]; then
167170
if [ -x "$RXNM_AGENT_BIN" ]; then
168171
"$RXNM_AGENT_BIN" --flush-addrs "$iface:4" >/dev/null 2>&1 || true
172+
"$RXNM_AGENT_BIN" --flush-routes "$iface:4" >/dev/null 2>&1 || true
169173
else
170174
ip -4 addr flush dev "$iface" 2>/dev/null || true
175+
ip -4 route flush dev "$iface" 2>/dev/null || true
171176
fi
172177
fi
173178
}

lib/rxnm-routes.sh

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,31 +184,43 @@ action_route_get() {
184184
}
185185

186186
action_route_flush() {
187-
local target="$1" # e.g., table ID, or cache
187+
local target="$1" # e.g., table ID, cache, or interface name
188188
shift
189-
local table=""
190-
191-
# Parse args to be safe
189+
local table="" iface=""
190+
191+
# Parse args
192192
if [ "$target" = "cache" ]; then
193-
ip route flush cache
193+
ip route flush cache 2>/dev/null || true
194194
json_success '{"action": "route_flush", "target": "cache"}'
195195
return 0
196196
fi
197-
197+
198198
while [ "$#" -gt 0 ]; do
199199
case "$1" in
200200
--table) table="$2"; shift 2 ;;
201+
--iface) iface="$2"; shift 2 ;;
201202
*) shift ;;
202203
esac
203204
done
204-
205+
206+
# Interface-scoped flush: prefer agent netlink, fallback to ip
207+
if [ -n "$iface" ]; then
208+
if [ -x "$RXNM_AGENT_BIN" ]; then
209+
"$RXNM_AGENT_BIN" --flush-routes "$iface" >/dev/null 2>&1
210+
else
211+
ip route flush dev "$iface" 2>/dev/null || true
212+
fi
213+
json_success '{"action": "route_flush", "iface": "'"$iface"'"}'
214+
return 0
215+
fi
216+
205217
if [ -n "$table" ]; then
206-
if ip route flush table "$table"; then
218+
if ip route flush table "$table" 2>/dev/null; then
207219
json_success '{"action": "route_flush", "table": "'"$table"'"}'
208220
else
209221
json_error "Failed to flush table $table"
210222
fi
211223
else
212-
json_error "Specify target to flush (e.g. 'rxnm route flush cache' or 'rxnm route flush --table 100')"
224+
json_error "Specify target to flush (e.g. 'rxnm route flush cache' or 'rxnm route flush --iface wlan0')"
213225
fi
214226
}

lib/rxnm-system.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ action_system_ipv6() {
272272
iname=$(basename "$iface")
273273
[ "$iname" = "lo" ] && continue
274274
sysctl -w "net.ipv6.conf.${iname}.disable_ipv6=1" >/dev/null 2>&1 || true
275+
# Flush IPv6 addresses and routes for clean teardown
276+
if [ -x "$RXNM_AGENT_BIN" ]; then
277+
"$RXNM_AGENT_BIN" --flush-addrs "$iname:6" >/dev/null 2>&1 || true
278+
"$RXNM_AGENT_BIN" --flush-routes "$iname:6" >/dev/null 2>&1 || true
279+
else
280+
ip -6 addr flush dev "$iname" scope global 2>/dev/null || true
281+
ip -6 route flush dev "$iname" 2>/dev/null || true
282+
fi
275283
done
276284

277285
printf "net.ipv6.conf.all.disable_ipv6=1\nnet.ipv6.conf.default.disable_ipv6=1\n" > "$conf_file" 2>/dev/null || true

src/rxnm-agent.c

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,123 @@ void cmd_check_internet() {
14841484
printf("{\n \"%s\": true,\n \"connected\": %s,\n \"ipv4\": %s,\n \"ipv6\": %s\n}\n", KEY_SUCCESS, (v4 || v6) ? "true" : "false", v4 ? "true" : "false", v6 ? "true" : "false");
14851485
}
14861486

1487+
int cmd_flush_routes(const char *arg) {
1488+
char arg_copy[256];
1489+
strncpy(arg_copy, arg, sizeof(arg_copy) - 1);
1490+
arg_copy[sizeof(arg_copy)-1] = '\0';
1491+
1492+
char *iface = strtok(arg_copy, ":");
1493+
char *fam_str = strtok(NULL, ":");
1494+
int target_family = AF_UNSPEC;
1495+
if (fam_str) {
1496+
if (strcmp(fam_str, "4") == 0) target_family = AF_INET;
1497+
else if (strcmp(fam_str, "6") == 0) target_family = AF_INET6;
1498+
}
1499+
1500+
if (!iface) return 1;
1501+
int ifindex = if_nametoindex(iface);
1502+
if (ifindex == 0) {
1503+
fprintf(stderr, "Interface %s not found\n", iface);
1504+
return 1;
1505+
}
1506+
1507+
int sock = open_netlink_rt();
1508+
if (sock < 0) return 1;
1509+
1510+
struct { struct nlmsghdr nlh; struct rtgenmsg rtg; } req;
1511+
memset(&req, 0, sizeof(req));
1512+
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));
1513+
req.nlh.nlmsg_type = RTM_GETROUTE;
1514+
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
1515+
req.nlh.nlmsg_seq = time(NULL);
1516+
req.rtg.rtgen_family = (target_family == AF_UNSPEC) ? AF_UNSPEC : target_family;
1517+
send(sock, &req, req.nlh.nlmsg_len, 0);
1518+
1519+
#define MAX_FLUSH_ROUTES 128
1520+
struct rtmsg to_delete[MAX_FLUSH_ROUTES];
1521+
struct rtattr *rta_dst[MAX_FLUSH_ROUTES];
1522+
struct rtattr *rta_gw[MAX_FLUSH_ROUTES];
1523+
struct rtattr *rta_oif[MAX_FLUSH_ROUTES];
1524+
struct rtattr *rta_priority[MAX_FLUSH_ROUTES];
1525+
int del_count = 0;
1526+
1527+
char buf[BUF_SIZE];
1528+
int len;
1529+
while ((len = recv(sock, buf, sizeof(buf), 0)) > 0) {
1530+
struct nlmsghdr *nh = (struct nlmsghdr *)buf;
1531+
for (; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
1532+
if (nh->nlmsg_type == NLMSG_DONE || nh->nlmsg_type == NLMSG_ERROR) goto rt_dump_done;
1533+
if (nh->nlmsg_type == RTM_NEWROUTE) {
1534+
struct rtmsg *rt = NLMSG_DATA(nh);
1535+
struct rtattr *tb[RTA_MAX + 1];
1536+
parse_rtattr(tb, RTA_MAX, RTM_RTA(rt), nh->nlmsg_len - NLMSG_LENGTH(sizeof(*rt)));
1537+
1538+
if (tb[RTA_OIF] && *(int *)RTA_DATA(tb[RTA_OIF]) == ifindex) {
1539+
if (target_family == AF_UNSPEC || rt->rtm_family == target_family) {
1540+
if (del_count < MAX_FLUSH_ROUTES) {
1541+
to_delete[del_count] = *rt;
1542+
rta_dst[del_count] = NULL;
1543+
rta_gw[del_count] = NULL;
1544+
rta_oif[del_count] = NULL;
1545+
rta_priority[del_count] = NULL;
1546+
1547+
if (tb[RTA_DST]) {
1548+
int rta_len = tb[RTA_DST]->rta_len;
1549+
rta_dst[del_count] = malloc(rta_len);
1550+
if (rta_dst[del_count]) memcpy(rta_dst[del_count], tb[RTA_DST], rta_len);
1551+
}
1552+
if (tb[RTA_GATEWAY]) {
1553+
int rta_len = tb[RTA_GATEWAY]->rta_len;
1554+
rta_gw[del_count] = malloc(rta_len);
1555+
if (rta_gw[del_count]) memcpy(rta_gw[del_count], tb[RTA_GATEWAY], rta_len);
1556+
}
1557+
if (tb[RTA_OIF]) {
1558+
int rta_len = tb[RTA_OIF]->rta_len;
1559+
rta_oif[del_count] = malloc(rta_len);
1560+
if (rta_oif[del_count]) memcpy(rta_oif[del_count], tb[RTA_OIF], rta_len);
1561+
}
1562+
if (tb[RTA_PRIORITY]) {
1563+
int rta_len = tb[RTA_PRIORITY]->rta_len;
1564+
rta_priority[del_count] = malloc(rta_len);
1565+
if (rta_priority[del_count]) memcpy(rta_priority[del_count], tb[RTA_PRIORITY], rta_len);
1566+
}
1567+
del_count++;
1568+
}
1569+
}
1570+
}
1571+
}
1572+
}
1573+
}
1574+
rt_dump_done:
1575+
1576+
for (int i = 0; i < del_count; i++) {
1577+
struct {
1578+
struct nlmsghdr n;
1579+
struct rtmsg rtm;
1580+
char attrbuf[512];
1581+
} del_req;
1582+
memset(&del_req, 0, sizeof(del_req));
1583+
del_req.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
1584+
del_req.n.nlmsg_type = RTM_DELROUTE;
1585+
del_req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
1586+
del_req.n.nlmsg_seq = time(NULL) + i;
1587+
del_req.rtm = to_delete[i];
1588+
1589+
if (rta_dst[i]) { add_nl_attr(&del_req.n, sizeof(del_req), RTA_DST, RTA_DATA(rta_dst[i]), RTA_PAYLOAD(rta_dst[i])); free(rta_dst[i]); }
1590+
if (rta_gw[i]) { add_nl_attr(&del_req.n, sizeof(del_req), RTA_GATEWAY, RTA_DATA(rta_gw[i]), RTA_PAYLOAD(rta_gw[i])); free(rta_gw[i]); }
1591+
if (rta_oif[i]) { add_nl_attr(&del_req.n, sizeof(del_req), RTA_OIF, RTA_DATA(rta_oif[i]), RTA_PAYLOAD(rta_oif[i])); free(rta_oif[i]); }
1592+
if (rta_priority[i]) { add_nl_attr(&del_req.n, sizeof(del_req), RTA_PRIORITY, RTA_DATA(rta_priority[i]), RTA_PAYLOAD(rta_priority[i])); free(rta_priority[i]); }
1593+
1594+
send(sock, &del_req, del_req.n.nlmsg_len, 0);
1595+
char ackbuf[1024];
1596+
recv(sock, ackbuf, sizeof(ackbuf), 0);
1597+
}
1598+
close(sock);
1599+
1600+
printf("{\"success\": true, \"action\": \"flush_routes\", \"iface\": \"%s\", \"count\": %d}\n", iface, del_count);
1601+
return 0;
1602+
}
1603+
14871604
/* --- JSON Formatting --- */
14881605

14891606
void get_proxy_config(char* http, char* https, char* no_proxy) {
@@ -2513,6 +2630,7 @@ char *g_ns_delete = NULL;
25132630
char *g_ns_exec_name = NULL;
25142631
char *g_route_table = NULL;
25152632
char *g_flush_addrs = NULL;
2633+
char *g_flush_routes = NULL;
25162634

25172635
int main(int argc, char *argv[]) {
25182636
atexit(cleanup_ifaces);
@@ -2541,6 +2659,7 @@ int main(int argc, char *argv[]) {
25412659
{"nullify-xdp", required_argument, 0, 'X'},
25422660
{"encode-ssid", required_argument, 0, 'E'},
25432661
{"flush-addrs", required_argument, 0, 'F'},
2662+
{"flush-routes", required_argument, 0, 'R'},
25442663
{"ns-create", required_argument, 0, 1001},
25452664
{"ns-delete", required_argument, 0, 1002},
25462665
{"ns-list", no_argument, 0, 1003},
@@ -2551,10 +2670,10 @@ int main(int argc, char *argv[]) {
25512670
};
25522671

25532672
int opt, option_index = 0;
2554-
while ((opt = getopt_long(argc, argv, "vhHtdLcrC:i:g:W:P:T:A:l:M:S:N:X:E:F:", long_options, &option_index)) != -1) {
2673+
while ((opt = getopt_long(argc, argv, "vhHtdLcrC:i:g:W:P:T:A:l:M:S:N:X:E:F:R:", long_options, &option_index)) != -1) {
25552674
switch (opt) {
25562675
case 'v': cmd_version(); return 0;
2557-
case 'h': printf("Usage: rxnm-agent [options]\n--dump Full JSON status\n--ns-create <name> Create namespace\n--route-dump <table_id> Dump routing table\n--nullify-xdp <iface>\n--flush-addrs <iface>[:family]\n"); return 0;
2676+
case 'h': printf("Usage: rxnm-agent [options]\n--dump Full JSON status\n--ns-create <name> Create namespace\n--route-dump <table_id> Dump routing table\n--nullify-xdp <iface>\n--flush-addrs <iface>[:family]\n--flush-routes <iface>[:family]\n"); return 0;
25582677
case 'H': cmd_health(); return 0;
25592678
case 't': cmd_time(); return 0;
25602679
case 'L': cmd_is_low_power(); return 0;
@@ -2575,6 +2694,7 @@ int main(int argc, char *argv[]) {
25752694
case 'X': g_nullify_xdp_iface = optarg; break;
25762695
case 'E': cmd_encode_ssid(optarg); return 0;
25772696
case 'F': g_flush_addrs = optarg; break;
2697+
case 'R': g_flush_routes = optarg; break;
25782698
case 1001: g_ns_create = optarg; break;
25792699
case 1002: g_ns_delete = optarg; break;
25802700
case 1003: cmd_ns_list(); return 0;
@@ -2614,6 +2734,7 @@ int main(int argc, char *argv[]) {
26142734
}
26152735

26162736
if (g_flush_addrs) { return cmd_flush_addrs(g_flush_addrs); }
2737+
if (g_flush_routes) { return cmd_flush_routes(g_flush_routes); }
26172738
if (g_atomic_path) { return cmd_atomic_write(g_atomic_path, g_perm_str); }
26182739
if (g_append_path && g_append_line) { return cmd_append_config(g_append_path, g_append_line); }
26192740
if (g_monitor_iface) { cmd_monitor_roam(g_monitor_iface, g_monitor_thresh); return 0; }

0 commit comments

Comments
 (0)