Clientul oficial WireGuard pentru macOS tine un singur tunel activ: cand il pornesti pe al doilea, primul se opreste. Pe iOS asta reflecta o limita reala a sistemului, pe macOS nu — acolo sistemul stie sa tina mai multe sesiuni de tunel in paralel. Am scris patch-urile care ridica restrictia si, mai important, tot ce a mai fost nevoie ca lucrurile sa chiar functioneze: fiecare tunel sa isi primeasca propriul dispozitiv utun, DNS-ul sa nu se calce intre tunele, si endpoint-urile sa fie rescrise cand treci pe alta retea.
Limita e a aplicatiei, nu a sistemului
Punctul unde se decide totul e in TunnelsManager.swift.
Cand ceri activarea unui tunel, aplicatia se uita daca exista deja unul
care nu e inactiv; daca da, il parcheaza pe cel nou intr-o stare
inventata de ea, .waiting, il opreste pe cel activ si abia
dupa ce acela ajunge complet jos porneste ce i-ai cerut:
if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
tunnel.status = .waiting
activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
startDeactivation(of: tunnelInOperation)
return
}
Codul e comun pentru iOS si macOS, iar singurul loc din tot proiectul
unde limita e pusa explicit pe seama sistemului de operare e imprejmuit
cu #if os(iOS). Pe macOS nu exista niciun echivalent:
fiecare tunel e propriul NETunnelProviderManager, iar
sistemul accepta mai multe sesiuni deodata. Restrictia e o decizie de
interfata mostenita din iOS, nu o constrangere tehnica.
Ridicarea politicii nu e de ajuns: doua tunele, un singur utun
Aici e miezul problemei, si motivul pentru care incercarile anterioare pareau ca merg dar nu mergeau. Pe macOS, mai multe sesiuni ruleaza in acelasi proces de extensie. Adaptorul isi cauta dispozitivul de tunel scanand descriptorii de fisier ai procesului si luand primul socket de control utun gasit. Cu un singur tunel per proces, alegerea era mereu corecta. Cu doua, al doilea adaptor se ataseaza la dispozitivul primului: ramane fara cititor pe al lui, deci nu trece niciun pachet, in timp ce pe primul se bat doua backend-uri pe acelasi descriptor.
Simptomul e exact cel raportat pe lista de discutii inca din iulie 2021: ambele tunele arata Connected, dar doar unul duce trafic. Cine se uita la starea din interfata crede ca merge; cine se uita la pachete vede altceva.
Fix-ul tine un registru de dispozitive deja revendicate in proces si,
dintre cele libere, il alege pe cel care poarta deja adresele tunelului
curent — setTunnelNetworkSettings le-a atribuit pana cand
porneste backend-ul, deci potrivirea e sigura. Revendicarea se elibereaza
la oprire, la esec de pornire si la dealocare.
Hack-ul cu exit(0)
Extensia de retea are pe macOS un ocol pentru un bug Apple vechi
(32073323): la oprirea tunelului isi omoara procesul cu
exit(0). Cu un singur tunel per proces e inofensiv. Cu mai
multe, oprirea unuia le-ar lua pe toate cu el.
Patch-ul numara tunelele pornite in proces si iese doar la ultimul. Numaratoarea acopera si pornirile in curs, nu doar cele terminate: rezolvarea DNS si urcarea interfetei pot dura secunde, iar un tunel oprit intre timp s-ar fi crezut ultimul si ar fi omorat o activare care tocmai se desfasura.
DNS: cine pune mana pe resolver
Cand un tunel are servere DNS in configuratie, aplicatia le instala
ca resolver pentru tot sistemul — matchDomains
setat pe sirul gol inseamna „toate domeniile". Cu un singur tunel activ
e o alegere apararabila. Cu mai multe, tunelele se bat pe resolverul de
sistem si castiga, nedeterminist, ultimul pornit.
Am adaugat un comutator per tunel, Split DNS, oprit
implicit. Cand il pornesti, serverele DNS ale tunelului sunt folosite
doar pentru domeniile lui de cautare — restul interogarilor raman pe
resolverul retelei. Sintaxa ramane cea din wg-quick,
intrarile care nu sunt adrese IP fiind deja tratate ca domenii:
DNS = 10.0.0.53, corp.example.com
Comutatorul e explicit, si nu implicit, dintr-un motiv de siguranta:
pornit automat pentru oricine are domenii in DNS=, ar fi
transformat configuratii existente de tunel-total in scurgeri de
interogari catre reteaua locala.
O capcana inrudita, care nu tine de patch dar deruteaza pe toata
lumea: un tunel cu AllowedIPs = 0.0.0.0/0 si fara
DNS= pare ca nu atinge DNS-ul, dar il rupe oricum — nu prin
setari, ci prin rutare, fiindca ruta implicita inghite si pachetele
catre resolverul din reteaua locala. Pe un tunel-total, DNS=
e practic obligatoriu.
Ce se rupe la schimbarea retelei
Ultima problema a aparut abia in folosinta zilnica: inchizi laptopul pe o retea, il deschizi pe alta, si un tunel nu se mai leaga niciodata, desi celelalte revin. Prima ipoteza — adresa ramasa veche dupa re-rezolvare — s-a dovedit gresita, iar log-ul a aratat de ce:
DNS64: mapped vpn.example.com to 198.51.100.7
peer(...) - Failed to send handshake initiation:
write udp4 0.0.0.0:49722->10.20.30.1:51820: sendto: no route to host
Aplicatia rezolvase corect numele, la adresa publica. Backend-ul trimitea in cu totul alta parte: la adresa interna a serverului. Explicatia e roaming-ul din WireGuard — daca serverul a fost atins candva la adresa lui din reteaua locala, sau prin alt tunel, peer-ul retine acea adresa. Pe alta retea ea nu mai exista, iar tunelul bate in gol pana il opresti si il pornesti manual.
Pe iOS problema nu apare, fiindca la schimbarea retelei aplicatia
rescrie endpoint-urile tuturor peer-ilor — ceea ce, ca efect secundar,
anuleaza roaming-ul. Pe macOS se facea doar un wgBumpSockets:
#if os(macOS)
if case .started(let handle, _) = self.state {
wgBumpSockets(handle)
}
#endif
Patch-ul face si pe macOS rescrierea endpoint-urilor dupa o schimbare de retea, re-rezolvand numele acolo unde configuratia are un hostname. Daca reteaua noua nu are DNS functional, se pastreaza ultima adresa cunoscuta, ca sa nu ramana tunelul fara endpoint. Operatia e amanata si comasata, fiindca o singura schimbare de retea produce o rafala de notificari, iar rezolvarea blocheaza coada de lucru.
Cifrele
Masurat in folosinta reala, pe macOS 26, la trecerea intre retele: handshake-uri complete raportate la initieri, inainte si dupa ultimul patch.
| Tunel | Inainte | Dupa |
|---|---|---|
| tunel-total (0.0.0.0/0), endpoint cu hostname | 2 / 130 | 83 / 129 |
| split A, endpoint cu adresa IP | 11 / 39 | 85 / 129 |
| split B, endpoint cu adresa IP | 10 / 34 | 80 / 117 |
Tunelul cu hostname era singurul afectat grav — celelalte doua se legau oricum, cifrele lor mai slabe dinainte venind din ferestrele in care reteaua chiar lipsea. Erorile ramase dupa patch tintesc numai adrese publice, in intervalul dintre pierderea unei retele si urcarea celeilalte, si sunt normale.
Unde e codul
Seria are zece commit-uri, fiecare compilabil separat pentru macOS si
iOS, cu comportamentul de pe iOS neschimbat peste tot. A fost trimisa pe
wireguard@lists.zx2c4.com, canalul de contributie documentat
al proiectului, dar mesajele nu au iesit din coada de moderare a listei.
Codul e deschis ca pull request pe oglinda de GitHub a proiectului:
- WireGuard/wireguard-apple#63 — seria completa;
- cremenescu/wireguard-apple, ramura
macos-multiple-tunnels— de unde se poate compila direct.
Proiectul wireguard-apple e marcat oficial drept complet
si isi cauta un intretinator nou, iar partea de macOS nu a mai fost
atinsa din februarie 2023. Pana se schimba asta, patch-urile raman la
indemana oricui trece prin aceleasi probleme.