@@ -1291,6 +1291,105 @@ void ds_net_rewire_gateway_clients(const char *gateway_name,
12911291 closedir (d );
12921292}
12931293
1294+ /* ---------------------------------------------------------------------------
1295+ * ds_net_gateway_teardown
1296+ *
1297+ * Called when a container that ACTS AS A GATEWAY stops. The gateway-side veth
1298+ * ds-g<hash> lives in the host netns; its peer is the gateway's eth1. When the
1299+ * gateway stops the kernel does NOT auto-reap ds-g: the host-side veth itself
1300+ * pins its now-process-less peer netns (a veth end holds a reference to its
1301+ * peer's namespace), and that netns can only be freed by a cleanup_net that
1302+ * cannot run while ds-g pins it - a self-sustaining orphan. So we delete ds-g
1303+ * explicitly, exactly as NAT mode deletes ds-v<pid>.
1304+ *
1305+ * We do not track our own segments (clients choose --gateway-net), so scan the
1306+ * client configs that delegate to us, derive each segment's bridge + gateway
1307+ * veth, delete the veth (reaping the peer and freeing the netns), and reap the
1308+ * bridge once no client veths remain on it. Per-segment work runs under the
1309+ * same advisory lock client setup/cleanup use, and bridges are de-duplicated
1310+ * since many clients can share one segment. A no-op for a container that is
1311+ * nobody's gateway.
1312+ * ---------------------------------------------------------------------------*/
1313+ void ds_net_gateway_teardown (const char * gateway_name ) {
1314+ if (!gateway_name || !gateway_name [0 ])
1315+ return ;
1316+
1317+ char containers_dir [PATH_MAX ];
1318+ snprintf (containers_dir , sizeof (containers_dir ), "%s/Containers" ,
1319+ get_workspace_dir ());
1320+ DIR * d = opendir (containers_dir );
1321+ if (!d )
1322+ return ;
1323+
1324+ /* De-dupe segments: many clients can share one --gateway-net (one bridge). */
1325+ char seen [32 ][IFNAMSIZ ];
1326+ int seen_count = 0 ;
1327+
1328+ struct dirent * ent ;
1329+ while ((ent = readdir (d )) != NULL ) {
1330+ if (ent -> d_name [0 ] == '.' )
1331+ continue ;
1332+
1333+ struct ds_config c = {0 };
1334+ if (ds_config_load_by_name (ent -> d_name , & c ) != 0 )
1335+ continue ;
1336+
1337+ if (!(c .net_mode == DS_NET_GATEWAY && c .gateway_container [0 ] &&
1338+ strcmp (c .gateway_container , gateway_name ) == 0 )) {
1339+ ds_config_free (& c );
1340+ continue ;
1341+ }
1342+
1343+ char bridge [IFNAMSIZ ], gw_host [IFNAMSIZ ], gw_peer [IFNAMSIZ ];
1344+ gateway_bridge_name (& c , bridge , sizeof (bridge ));
1345+ gateway_veth_names (& c , gw_host , sizeof (gw_host ), gw_peer , sizeof (gw_peer ));
1346+
1347+ int dup = 0 ;
1348+ for (int i = 0 ; i < seen_count ; i ++ )
1349+ if (strcmp (seen [i ], bridge ) == 0 ) {
1350+ dup = 1 ;
1351+ break ;
1352+ }
1353+ if (dup ) {
1354+ ds_config_free (& c );
1355+ continue ;
1356+ }
1357+ if (seen_count < (int )(sizeof (seen ) / sizeof (seen [0 ])))
1358+ safe_strncpy (seen [seen_count ++ ], bridge , IFNAMSIZ );
1359+
1360+ ds_nl_ctx_t * ctx = ds_nl_open ();
1361+ if (!ctx ) {
1362+ ds_config_free (& c );
1363+ continue ;
1364+ }
1365+
1366+ /* Same lock client setup/cleanup take, so we cannot race a concurrent
1367+ * client start/wire or the gateway's own rewire on the segment. */
1368+ int lock = gateway_segment_lock (bridge );
1369+
1370+ ds_nl_del_link (ctx , gw_host );
1371+ ds_log ("[NET] Gateway teardown: removed gateway veth %s (segment %s)" ,
1372+ gw_host , bridge );
1373+
1374+ int clients = ds_nl_count_bridge_members_with_prefix (
1375+ ctx , bridge , app_veth_host_prefix (& c ));
1376+ if (clients > 0 ) {
1377+ ds_log ("[NET] Gateway teardown: %d client(s) still on %s - keeping "
1378+ "bridge" ,
1379+ clients , bridge );
1380+ } else {
1381+ ds_nl_del_link (ctx , bridge );
1382+ ds_log ("[NET] Gateway teardown: reaped idle delegated LAN bridge %s" ,
1383+ bridge );
1384+ }
1385+
1386+ gateway_segment_unlock (lock );
1387+ ds_nl_close (ctx );
1388+ ds_config_free (& c );
1389+ }
1390+ closedir (d );
1391+ }
1392+
12941393/* ---------------------------------------------------------------------------
12951394 * setup_veth_child_side_named
12961395 *
@@ -1999,6 +2098,12 @@ void ds_net_start_route_monitor(void) {
19992098 * ---------------------------------------------------------------------------*/
20002099
20012100void ds_net_cleanup (struct ds_config * cfg , pid_t container_pid ) {
2101+ /* If this container is a gateway for others, explicitly tear down the
2102+ * gateway-side veth(s) it serves: the kernel will not auto-reap them (the
2103+ * host-side veth pins its orphan peer netns). No-op when nobody delegates
2104+ * to us, so it is safe to run for every stopping container. */
2105+ ds_net_gateway_teardown (cfg -> container_name );
2106+
20022107 if (cfg -> net_mode == DS_NET_GATEWAY ) {
20032108 ds_nl_ctx_t * ctx = ds_nl_open ();
20042109 if (!ctx )
@@ -2037,8 +2142,10 @@ void ds_net_cleanup(struct ds_config *cfg, pid_t container_pid) {
20372142 "- keeping bridge to avoid flapping its LAN iface" ,
20382143 bridge , cfg -> gateway_container );
20392144 } else {
2040- /* No clients and the gateway is gone (its netns death already took the
2041- * gateway veth with its peer) - safe to reap the now-idle bridge. */
2145+ /* No clients and the gateway is gone. The gateway's own stop already
2146+ * ran ds_net_gateway_teardown(), which explicitly deleted the gateway
2147+ * veth (the kernel does not auto-reap it), so the bridge is now idle
2148+ * and safe to reap. */
20422149 ds_nl_del_link (ctx , bridge );
20432150 ds_log ("[NET] Gateway cleanup: reaped idle delegated LAN bridge %s" ,
20442151 bridge );
0 commit comments