Il bypass cgroup di nftables richiede anche una correzione dell'indirizzo sorgente
Contrassegnare un cgroup lo invia a un'altra tabella e il socket mantiene l'indirizzo che aveva già. Classificare, instradare e tradurre come tre passaggi separati.
Contrassegnare il traffico di un cgroup è la parte facile. Tre decisioni separate devono allinearsi prima che una singola applicazione esca dall'interfaccia fisica mentre tutto il resto resta nel tunnel: quale socket appartiene all'eccezione, quale ricerca di routing attiva il mark e quale indirizzo sorgente porta il pacchetto quando esce. La maggior parte dei tentativi falliti azzecca la prima, fa metà della seconda e non guarda mai la terza.
Tre decisioni, non una
La classificazione risponde a chi è esente. L'instradamento risponde a dove va invece il pacchetto marcato. La selezione della sorgente risponde a cosa vedrà l'estremità remota e dove deve tornare la risposta.
Queste cose falliscono in modo indipendente. Un pacchetto può essere classificato, reindirizzato sull'interfaccia fisica e continuare a portare un indirizzo privato che nessun router a monte può restituire, il che produce un timeout che sembra esattamente un blocco del firewall. Nulla nel passaggio di classificazione ti avverte di questo. Nulla nel passaggio di instradamento nemmeno.
Anche l'ordine conta. La classificazione avviene quando il pacchetto viene generato, l'instradamento avviene dopo e la traduzione avviene nel punto di uscita.
Se non sai dire quale dei tre stadi è rotto, non stai facendo debug: stai riordinando regole e sperando.
Trova un cgroup che esisterà ancora domani
nftables può verificare l'appartenenza a cgroup v2 di un socket, cosa davvero utile perché segue il processo invece della destinazione. Il problema è l'identificatore. Un'applicazione desktop avviata da un menu vive spesso in uno scope transitorio il cui nome cambia al successivo avvio.
nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroupCopiare un nome di scope da una sessione e codificarlo in modo fisso produce una regola che funziona oggi e smette silenziosamente di corrispondere domani. Metti invece l'applicazione in un contesto di esecuzione stabile e intenzionale, come uno scope o un'unità dedicata che controlli, e usa quello come corrispondenza. La regola sopravvive così ai riavvii e agli aggiornamenti dei pacchetti. Diventa anche qualcosa su cui un collega può ragionare senza la tua cronologia di sessione.
Sii anche preciso su quale meccanismo stai usando. La classificazione net_cls di cgroup v1 e la corrispondenza dei socket di cgroup v2 sono cose diverse, e un campo con nome simile nella documentazione non le rende intercambiabili.
È la catena di routing che attiva la seconda ricerca
Per il traffico generato localmente, il tipo di catena decide se succede qualcosa dopo che il mark è impostato. Una catena output di tipo filter può marcare un pacchetto e il kernel continuerà a usare la rotta che ha già scelto. Una catena di tipo route è il meccanismo che chiede una nuova ricerca.
table inet bypass {
chain mark {
type route hook output priority mangle; policy accept;
socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
}
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200Il mark ha poi bisogno di una regola di policy che lo invii a una tabella, e quella tabella deve essere completa: una rotta predefinita, le destinazioni locali che l'applicazione richiede ancora e un gateway a monte che risponda davvero.
Scegli il valore del mark con criterio. WireGuard usa il proprio fwmark per il traffico di trasporto esterno, e finire per caso sullo stesso numero può interferire con l'eccezione di routing del tunnel in modi dolorosi da diagnosticare.
L'indirizzo sorgente non segue la rotta
Un socket che ha già scelto un indirizzo sorgente per il percorso del tunnel mantiene quell'indirizzo dopo il reindirizzamento. Nulla nel cambio di rotta lo riscrive, ed è per questo che il pacchetto esce dall'interfaccia fisica con una sorgente privata che la rete a monte non può restituire.
Il sintomo visibile è un'applicazione che sembra inviare dati e non riceve mai risposta, e un log del firewall che non mostra nulla perché il pacchetto non è mai tornato.
La correzione mirata è una regola di traduzione della sorgente limitata al mark di quell'applicazione e alla sua vera interfaccia di uscita, integrata con il NAT già esistente invece di sostituirlo:
table inet bypass {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
meta mark 0x1 oifname "wlan0" masquerade
}
}Traduci l'eccezione e nulla più. Una regola masquerade generale corregge il sintomo e distrugge la tua capacità di capire quale traffico ha preso quale percorso. Fai corrispondere il mark, la vera interfaccia di uscita e, se puoi, l'indirizzo dell'applicazione stessa. Le regole ampie sono facili da scrivere e quasi impossibili da verificare in seguito.
Cosa sfugge ancora all'eccezione
La risoluzione dei nomi può arrivare da un processo resolver separato in un cgroup diverso, quindi classificare il socket del browser non classifica le sue ricerche. Se le query del resolver restano nel tunnel mentre la connessione esce fuori, le risposte split-horizon possono non corrispondere al percorso. Quella discrepanza produce errori che sembrano bug DNS e non lo sono.
I processi figli ereditano il cgroup nella maggior parte delle configurazioni, ma un helper che si sposta nel proprio scope no. Verifica invece di presumere, soprattutto per qualsiasi cosa che si ri-esegua.
Infine, decidi cosa deve succedere quando il tunnel è giù. Un'eccezione che diventa silenziosamente un bypass completo è una postura di sicurezza diversa da una che fallisce in modo chiuso, e solo una delle due è quella che intendevi.