fix(ios-vpn): K1/K2/H1 — Upstream-Fehler, In-Flight-Cap, Response-Layout
K2: forwardPacket schreibt jetzt bei jedem Upstream-Fehler eine synthetische SERVFAIL ins TUN (zentrale finish(gotAnswer:)) — DNS-Client haengt nicht mehr. H1: harter In-Flight-Cap (32) statt Connection-pro-Query — ueber dem Cap sofort SERVFAIL; deckelt den NE-Memory-Footprint. K1: buildErrorResponse + dnsQuestionEnd schneiden das Paket hinter der Question- Section ab (EDNS-OPT-Muell weg), IP/UDP-Length neu berechnet. H3: Compression-Pointer-QNAME → fail-open (.forward) statt stillem Drop. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dfcee68dc8
commit
1aa86c2c0c
@ -7,6 +7,8 @@
|
||||
- Block-Query → synthetische NXDOMAIN-Response (RCODE=3), synchron gebaut.
|
||||
- Allow-Query → an Upstream-Resolver (1.1.1.1:53) forwarden, Response wird
|
||||
wieder als IPv4/UDP-Paket verpackt zurückgegeben.
|
||||
- Upstream-Fehler → synthetische SERVFAIL-Response (RCODE=2), damit der
|
||||
Client schnell failt statt in seinen Timeout zu laufen.
|
||||
- Drop (kein DNS / IPv6 / Nicht-UDP) → nil, das Paket wird verworfen.
|
||||
|
||||
Unterschied zu Android: Android schreibt selbst ins TUN (FileOutputStream).
|
||||
@ -82,12 +84,20 @@ enum DnsFilter {
|
||||
guard dnsLen >= 12 else { return .drop }
|
||||
|
||||
// QNAME parsen — beginnt nach dem 12-Byte-DNS-Header.
|
||||
// H3-Fix: `parseQname` liefert `nil`, wenn die QNAME nicht parsebar ist
|
||||
// (Compression-Pointer in der Frage oder Out-of-Bounds). Früher führte
|
||||
// das zu `.drop` → die Auflösung schlug für den Client stillschweigend
|
||||
// fehl. Korrekt ist FAIL-OPEN: solche Pakete an den Upstream forwarden,
|
||||
// damit reguläre Namensauflösung nicht heimlich kaputtgeht. Ein
|
||||
// unparsebares QNAME bedeutet nur, dass WIR es nicht matchen können —
|
||||
// nicht, dass die Query verworfen werden darf.
|
||||
guard let domain = parseQname(bytes, start: dnsStart + 12, end: length) else {
|
||||
return .drop
|
||||
return .forward(domain: "<unparsebar>")
|
||||
}
|
||||
|
||||
if hashList.matchesAnySuffix(domain) {
|
||||
let response = buildNxDomainResponse(bytes, length: length, ihl: ihl)
|
||||
let response = buildErrorResponse(
|
||||
bytes, length: length, ihl: ihl, rcode: 3 /* NXDOMAIN */)
|
||||
return .block(response: response, domain: domain)
|
||||
}
|
||||
return .forward(domain: domain)
|
||||
@ -97,7 +107,11 @@ enum DnsFilter {
|
||||
// ─── QNAME-Parser ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Parst den QNAME ab `start`. Gibt `nil` zurück bei DNS-Compression-Pointer
|
||||
/// (0xC0-Bit gesetzt) oder Out-of-Bounds — identisch zu Androids `parseQname`.
|
||||
/// (0xC0-Bit gesetzt) oder Out-of-Bounds.
|
||||
///
|
||||
/// `nil` heißt NICHT „verwerfen": der Aufrufer (`classify`) behandelt `nil`
|
||||
/// als FAIL-OPEN und forwardet die Query an den Upstream (H3-Fix). Ein
|
||||
/// nicht-parsebares QNAME darf nie zum stillen Verlust der Auflösung führen.
|
||||
private static func parseQname(
|
||||
_ bytes: UnsafeBufferPointer<UInt8>, start: Int, end: Int
|
||||
) -> String? {
|
||||
@ -106,8 +120,8 @@ enum DnsFilter {
|
||||
while pos < end {
|
||||
let len = Int(bytes[pos]) & 0xFF
|
||||
if len == 0 { break }
|
||||
// Compression-Pointer (zwei höchste Bits) — in einer Query unüblich,
|
||||
// wir behandeln ihn defensiv als nicht-parsebar.
|
||||
// Compression-Pointer (zwei höchste Bits) — in einer Query unüblich.
|
||||
// Nicht parsebar → nil → Aufrufer forwardet fail-open (H3-Fix).
|
||||
if (len & 0xC0) != 0 { return nil }
|
||||
pos += 1
|
||||
guard pos + len <= end else { return nil }
|
||||
@ -123,19 +137,83 @@ enum DnsFilter {
|
||||
return host.isEmpty ? nil : host
|
||||
}
|
||||
|
||||
// ─── NXDOMAIN-Response-Builder ─────────────────────────────────────────────
|
||||
// ─── Question-Section-Ende ─────────────────────────────────────────────────
|
||||
|
||||
/// Baut die synthetische NXDOMAIN-Response aus der Request-Kopie.
|
||||
/// Logik 1:1 aus `DnsFilter.kt#buildNxDomainResponse`:
|
||||
/// Ermittelt den Byte-Offset DIREKT HINTER der Question-Section, gemessen ab
|
||||
/// dem Paket-Anfang (nicht ab `dnsStart`).
|
||||
///
|
||||
/// Layout DNS-Question: QNAME (variabel) + QTYPE (2 B) + QCLASS (2 B).
|
||||
/// QNAME ist eine Folge von Längen-prefixed Labels, terminiert durch ein
|
||||
/// 0x00-Byte. Ein Compression-Pointer (0xC0-Bit) kommt in einer Query
|
||||
/// normalerweise nicht vor — taucht er auf, geben wir `nil` zurück.
|
||||
///
|
||||
/// Gibt `nil` zurück, wenn die Question-Section out-of-bounds läuft oder
|
||||
/// einen Pointer enthält. Der Aufrufer darf das Paket dann NICHT abschneiden
|
||||
/// und sollte fail-open forwarden.
|
||||
private static func dnsQuestionEnd(
|
||||
_ bytes: UnsafeBufferPointer<UInt8>, dnsStart: Int, length: Int
|
||||
) -> Int? {
|
||||
// QNAME beginnt nach dem 12-Byte-DNS-Header.
|
||||
var pos = dnsStart + 12
|
||||
while pos < length {
|
||||
let len = Int(bytes[pos]) & 0xFF
|
||||
if len == 0 {
|
||||
// 0x00 terminiert die QNAME. Danach QTYPE(2) + QCLASS(2).
|
||||
let end = pos + 1 + 4
|
||||
guard end <= length else { return nil }
|
||||
return end
|
||||
}
|
||||
// Compression-Pointer in einer Query → nicht abschneidbar.
|
||||
if (len & 0xC0) != 0 { return nil }
|
||||
pos += 1 + len
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ─── DNS-Error-Response-Builder (NXDOMAIN / SERVFAIL) ──────────────────────
|
||||
|
||||
/// Baut eine synthetische DNS-Error-Response aus der Request-Kopie.
|
||||
/// Generischer Builder für NXDOMAIN (RCODE=3) und SERVFAIL (RCODE=2).
|
||||
///
|
||||
/// Logik (Basis aus Androids `DnsFilter.kt#buildNxDomainResponse`):
|
||||
/// - Src-/Dst-IP tauschen, Src-/Dst-Port tauschen.
|
||||
/// - DNS-Flags auf 0x8183 setzen (QR=1, RD=1, RA=1, RCODE=3 NXDOMAIN).
|
||||
/// - DNS-Flags: QR=1, RD=1, RA=1, OPCODE/AA/TC=0, RCODE = Parameter.
|
||||
/// - Answer/Authority/Additional-Counts auf 0.
|
||||
/// - IP-Checksum neu berechnen, UDP-Checksum auf 0 (optional in IPv4).
|
||||
private static func buildNxDomainResponse(
|
||||
_ bytes: UnsafeBufferPointer<UInt8>, length: Int, ihl: Int
|
||||
///
|
||||
/// K1-Fix: Das Response-Paket wird NACH dem Ende der Question-Section
|
||||
/// abgeschnitten. Früher kopierte der Builder das gesamte Request-Paket und
|
||||
/// nullte nur ARCOUNT — ein evtl. vorhandener EDNS-OPT-Record blieb dann
|
||||
/// physisch als Müll-Bytes hinter der Question-Section stehen, obwohl
|
||||
/// ARCOUNT=0 sagt „keine Additional-Records". Manche Resolver/Clients
|
||||
/// verwerfen so eine Response als korrupt. Jetzt: Paket auf
|
||||
/// IP-Header + UDP-Header + DNS-Header + Question-Section gekürzt,
|
||||
/// IP-Total-Length und UDP-Length entsprechend neu berechnet.
|
||||
///
|
||||
/// Kann die Question-Section nicht sicher vermessen werden (Pointer /
|
||||
/// Out-of-Bounds), fällt der Builder auf das ungeschnittene Paket zurück
|
||||
/// (besser eine evtl. „dreckige" Response als gar keine).
|
||||
private static func buildErrorResponse(
|
||||
_ bytes: UnsafeBufferPointer<UInt8>, length: Int, ihl: Int, rcode: UInt8
|
||||
) -> Data {
|
||||
var resp = [UInt8](repeating: 0, count: length)
|
||||
for i in 0..<length { resp[i] = bytes[i] }
|
||||
let udpStart = ihl
|
||||
let dnsStart = udpStart + 8
|
||||
|
||||
// K1: Ende der Question-Section bestimmen → Paket dahinter abschneiden.
|
||||
let respLen: Int
|
||||
if let qEnd = dnsQuestionEnd(bytes, dnsStart: dnsStart, length: length) {
|
||||
respLen = qEnd
|
||||
} else {
|
||||
// Fallback: ungeschnitten (Pointer/OOB) — markiert als ungeprüfter Pfad.
|
||||
respLen = length
|
||||
}
|
||||
|
||||
var resp = [UInt8](repeating: 0, count: respLen)
|
||||
for i in 0..<respLen { resp[i] = bytes[i] }
|
||||
|
||||
// IP total-length (Bytes 2..3) auf die neue, evtl. gekürzte Länge setzen.
|
||||
resp[2] = UInt8((respLen >> 8) & 0xFF)
|
||||
resp[3] = UInt8(respLen & 0xFF)
|
||||
|
||||
// Src-IP (12..15) und Dst-IP (16..19) tauschen.
|
||||
for i in 0..<4 {
|
||||
@ -149,7 +227,6 @@ enum DnsFilter {
|
||||
resp[11] = 0
|
||||
|
||||
// UDP-Ports tauschen.
|
||||
let udpStart = ihl
|
||||
let srcPort0 = bytes[udpStart]
|
||||
let srcPort1 = bytes[udpStart + 1]
|
||||
let dstPort0 = bytes[udpStart + 2]
|
||||
@ -158,14 +235,19 @@ enum DnsFilter {
|
||||
resp[udpStart + 1] = dstPort1
|
||||
resp[udpStart + 2] = srcPort0
|
||||
resp[udpStart + 3] = srcPort1
|
||||
// UDP-Length (Bytes 4..5) = 8 (Header) + DNS-Payload-Länge.
|
||||
let udpLen = respLen - udpStart
|
||||
resp[udpStart + 4] = UInt8((udpLen >> 8) & 0xFF)
|
||||
resp[udpStart + 5] = UInt8(udpLen & 0xFF)
|
||||
// UDP-Checksum auf 0 (in IPv4 erlaubt, Empfänger überspringt die Prüfung).
|
||||
resp[udpStart + 6] = 0
|
||||
resp[udpStart + 7] = 0
|
||||
|
||||
// DNS-Flags: 0x8183 = QR=1 OPCODE=0 AA=0 TC=0 RD=1 RA=1 RCODE=3 (NXDOMAIN).
|
||||
let dnsStart = udpStart + 8
|
||||
// DNS-Flags: QR=1 OPCODE=0 AA=0 TC=0 RD=1 | RA=1 Z=0 RCODE=<rcode>.
|
||||
// High-Byte 0x81 = QR=1, RD=1.
|
||||
// Low-Byte 0x80 | rcode = RA=1, RCODE = 3 (NXDOMAIN) bzw. 2 (SERVFAIL).
|
||||
resp[dnsStart + 2] = 0x81
|
||||
resp[dnsStart + 3] = 0x83
|
||||
resp[dnsStart + 3] = 0x80 | (rcode & 0x0F)
|
||||
// ANCOUNT / NSCOUNT / ARCOUNT (Bytes 6..11 im DNS-Header) auf 0 —
|
||||
// QDCOUNT (Bytes 4..5) bleibt unangetastet, die Frage spiegeln wir zurück.
|
||||
for i in 6...11 {
|
||||
@ -176,6 +258,36 @@ enum DnsFilter {
|
||||
return Data(resp)
|
||||
}
|
||||
|
||||
// ─── SERVFAIL-Response aus einem rohen Request-Paket (K2) ──────────────────
|
||||
|
||||
/// Baut eine SERVFAIL-Response (RCODE=2) direkt aus einem rohen
|
||||
/// IPv4/UDP/DNS-Request-Paket — für den Fall, dass das Forwarding an den
|
||||
/// Upstream-Resolver fehlschlägt (Timeout, recvError, leere Antwort,
|
||||
/// Connection `.failed`).
|
||||
///
|
||||
/// K2-Fix: Ohne diese Response bekäme der DNS-Client des iPhones bei jedem
|
||||
/// kurzen Upstream-Hänger GAR NICHTS zurück und liefe in seinen eigenen,
|
||||
/// mehrsekündigen Timeout → die komplette Namensauflösung des Geräts steht.
|
||||
/// Mit einer sofortigen SERVFAIL-Response failt der Client schnell und kann
|
||||
/// retryen / den nächsten Resolver fragen.
|
||||
///
|
||||
/// Nutzt exakt dieselbe Paket-Bau-Logik wie die NXDOMAIN-Response inkl. des
|
||||
/// K1-Abschneidens hinter der Question-Section.
|
||||
///
|
||||
/// Gibt `nil` zurück, wenn das Request-Paket kein valides IPv4/UDP-Paket ist.
|
||||
static func buildServFailResponse(request: Data) -> Data? {
|
||||
let length = request.count
|
||||
guard length >= 28 else { return nil }
|
||||
return request.withUnsafeBytes { (raw: UnsafeRawBufferPointer) -> Data? in
|
||||
let bytes = raw.bindMemory(to: UInt8.self)
|
||||
let version = (Int(bytes[0]) >> 4) & 0xF
|
||||
guard version == 4 else { return nil }
|
||||
let ihl = (Int(bytes[0]) & 0xF) * 4
|
||||
guard ihl >= 20, length >= ihl + 8 + 12 else { return nil }
|
||||
return buildErrorResponse(bytes, length: length, ihl: ihl, rcode: 2 /* SERVFAIL */)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Forward-Response-Builder ──────────────────────────────────────────────
|
||||
|
||||
/// Verpackt die rohe DNS-Antwort des Upstream-Resolvers wieder als IPv4/UDP-
|
||||
|
||||
@ -15,6 +15,10 @@
|
||||
zurückgeschrieben.
|
||||
- Miss → Query an Upstream-Resolver (1.1.1.1:53) via `NWConnection`,
|
||||
Response wieder als IPv4/UDP-Paket verpackt und ins TUN geschrieben.
|
||||
- Upstream-Fehler (Timeout / recvError / leere Antwort / Connection failed)
|
||||
oder In-Flight-Cap überschritten → synthetische SERVFAIL-Response ins TUN,
|
||||
damit der DNS-Client des Geräts schnell failt statt in seinen eigenen
|
||||
mehrsekündigen Timeout zu laufen.
|
||||
|
||||
Blocklist: `blocklist.bin` aus dem App-Group-Container, mmap'd via `HashList`.
|
||||
`syncBlocklist` (Haupt-App) postet die Darwin-Notification
|
||||
@ -82,6 +86,39 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
/// Ist der Read-Loop aktiv?
|
||||
private var running = false
|
||||
|
||||
// ─── In-Flight-Cap (H1) ─────────────────────────────────────────────────────
|
||||
//
|
||||
// H1-Fix: Pro erlaubter Query wurde bisher eine frische `NWConnection`
|
||||
// geöffnet (erst nach Antwort/Timeout bis 4 s gecancelt). Bei DNS-Bursts
|
||||
// (App-Start, Webseite mit vielen Subresources) liefen so Dutzende
|
||||
// gleichzeitige Connections + Timeout-WorkItems an — NE-Packet-Tunnel-
|
||||
// Extensions haben aber ein hartes Memory-Limit (~15 MB) → Jetsam-Crash.
|
||||
//
|
||||
// Gewählte Variante: harter In-Flight-CAP statt Connection-Pooling.
|
||||
// Begründung:
|
||||
// - UDP-`NWConnection`-Pooling auf einen einzigen Socket ist fehleranfällig:
|
||||
// ein gemeinsamer UDP-Socket vermischt Antworten mehrerer paralleler
|
||||
// Queries — korrektes Demultiplexing bräuchte ein eigenes Pending-Mapping
|
||||
// über die DNS-Transaction-ID. Das ist mehr Zustand (= mehr Speicher) und
|
||||
// mehr Crash-Fläche als das Problem rechtfertigt.
|
||||
// - Ein simpler Zähler ist O(1)-Zustand und deckelt den Speicher hart.
|
||||
// - DNS ist von sich aus retry-tolerant: wird der Cap überschritten,
|
||||
// antworten wir der Query SOFORT mit SERVFAIL (K2-Pfad) — der Client
|
||||
// retryt dann von selbst, sobald der Burst abgeebbt ist. Besser ein
|
||||
// schneller SERVFAIL als ein Extension-Crash, der den ganzen Tunnel
|
||||
// (= den Schutz) reißt.
|
||||
//
|
||||
// UNGETESTETE ANNAHME: Der konkrete Cap-Wert (32) ist eine konservative
|
||||
// Schätzung. Eine `NWConnection` + Timeout-WorkItem kostet grob einige KB;
|
||||
// 32 gleichzeitige Verbindungen bleiben damit klar unter dem ~15-MB-Limit,
|
||||
// decken aber typische Burst-Größen ab. Auf Gerät unter Last verifizieren
|
||||
// (Instruments / Memory-Graph der Extension).
|
||||
private static let maxInFlightUpstream = 32
|
||||
|
||||
/// Zähler aktuell offener Upstream-Connections. Nur auf `forwardQueue`
|
||||
/// gelesen/geschrieben → keine zusätzliche Synchronisation nötig.
|
||||
private var inFlightUpstream = 0
|
||||
|
||||
// ─── startTunnel ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Vom System aufgerufen, sobald `connection.startVPNTunnel()` (aus dem
|
||||
@ -257,56 +294,112 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
// Gesamte Connection-Lebenszeit läuft auf `forwardQueue` — damit ist auch
|
||||
// der `inFlightUpstream`-Zähler ohne extra Lock konsistent.
|
||||
forwardQueue.async { [weak self] in
|
||||
guard let self = self, self.running else { return }
|
||||
|
||||
// ── H1: In-Flight-Cap ──
|
||||
// Burst über dem Cap → keine neue Connection öffnen, sondern die Query
|
||||
// sofort mit SERVFAIL beantworten. Der Client retryt selbst; der
|
||||
// Speicher der Extension bleibt gedeckelt.
|
||||
if self.inFlightUpstream >= PacketTunnelProvider.maxInFlightUpstream {
|
||||
ExtLog.write("⚠️ Upstream-Cap erreicht — SERVFAIL (in-flight=\(self.inFlightUpstream))")
|
||||
self.replyServFail(for: packet)
|
||||
return
|
||||
}
|
||||
|
||||
let connection = NWConnection(host: host, port: port, using: .udp)
|
||||
var didFinish = false
|
||||
self.inFlightUpstream += 1
|
||||
|
||||
// Timeout — wie Androids `soTimeout = 4000`.
|
||||
let timeoutWork = DispatchWorkItem {
|
||||
if !didFinish {
|
||||
// Timeout-WorkItem — vorab deklariert, damit `finish()` es referenzieren
|
||||
// kann; die eigentliche Item-Closure wird gleich darunter zugewiesen.
|
||||
var timeoutWork: DispatchWorkItem?
|
||||
|
||||
// Zentrale Abschluss-Logik: idempotent, dekrementiert den Cap-Zähler
|
||||
// genau einmal und schreibt — wenn keine echte Antwort kam — eine
|
||||
// SERVFAIL-Response ins TUN (K2). `gotAnswer = true` heißt: es wurde
|
||||
// bereits eine valide Upstream-Antwort geschrieben.
|
||||
func finish(gotAnswer: Bool) {
|
||||
// Läuft immer auf `forwardQueue` (alle Aufrufer tun das).
|
||||
if didFinish { return }
|
||||
didFinish = true
|
||||
timeoutWork?.cancel()
|
||||
connection.cancel()
|
||||
self.inFlightUpstream -= 1
|
||||
if !gotAnswer {
|
||||
// K2: Upstream lieferte nichts Brauchbares → Client nicht hängen
|
||||
// lassen, sondern schnell mit SERVFAIL failen.
|
||||
self.replyServFail(for: packet)
|
||||
}
|
||||
}
|
||||
forwardQueue.asyncAfter(deadline: .now() + 4.0, execute: timeoutWork)
|
||||
|
||||
// Timeout — wie Androids `soTimeout = 4000`. Bei Ablauf: SERVFAIL.
|
||||
let work = DispatchWorkItem {
|
||||
ExtLog.write("Upstream-Timeout — SERVFAIL")
|
||||
finish(gotAnswer: false)
|
||||
}
|
||||
timeoutWork = work
|
||||
self.forwardQueue.asyncAfter(deadline: .now() + 4.0, execute: work)
|
||||
|
||||
connection.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
connection.send(content: query, completion: .contentProcessed { sendError in
|
||||
if sendError != nil {
|
||||
if !didFinish {
|
||||
didFinish = true
|
||||
timeoutWork.cancel()
|
||||
connection.cancel()
|
||||
}
|
||||
// Send fehlgeschlagen → SERVFAIL.
|
||||
self.forwardQueue.async { finish(gotAnswer: false) }
|
||||
return
|
||||
}
|
||||
// Antwort empfangen.
|
||||
connection.receiveMessage { [weak self] data, _, _, recvError in
|
||||
guard let self = self else { return }
|
||||
self.forwardQueue.async {
|
||||
if didFinish { return }
|
||||
didFinish = true
|
||||
timeoutWork.cancel()
|
||||
defer { connection.cancel() }
|
||||
guard recvError == nil, let answer = data, !answer.isEmpty else {
|
||||
guard recvError == nil, let answer = data, !answer.isEmpty,
|
||||
let response = DnsFilter.buildForwardResponse(
|
||||
request: packet, dnsAnswer: answer) else {
|
||||
// recvError / leere / unverpackbare Antwort → SERVFAIL (K2).
|
||||
finish(gotAnswer: false)
|
||||
return
|
||||
}
|
||||
// Antwort als IPv4/UDP-Paket verpacken und ins TUN schreiben.
|
||||
if let response = DnsFilter.buildForwardResponse(
|
||||
request: packet, dnsAnswer: answer) {
|
||||
self?.writeToTun(response)
|
||||
// Echte Antwort als IPv4/UDP-Paket ins TUN schreiben.
|
||||
self.writeToTun(response)
|
||||
finish(gotAnswer: true)
|
||||
}
|
||||
}
|
||||
})
|
||||
case .failed, .cancelled:
|
||||
if !didFinish {
|
||||
didFinish = true
|
||||
timeoutWork.cancel()
|
||||
}
|
||||
case .failed:
|
||||
// Connection endgültig fehlgeschlagen (z.B. kein Netz) → SERVFAIL
|
||||
// statt den Client hängen zu lassen (K2).
|
||||
self.forwardQueue.async { finish(gotAnswer: false) }
|
||||
case .cancelled:
|
||||
// `cancelled` ist Folge eines bereits gelaufenen finish() —
|
||||
// finish() ist idempotent, hier ist nichts mehr zu tun.
|
||||
break
|
||||
case .waiting:
|
||||
// `.waiting` (kein Pfad) ist NICHT zwingend final — die Connection
|
||||
// kann sich erholen, sobald Konnektivität zurückkommt. Wir brechen
|
||||
// hier deshalb NICHT sofort ab, sondern überlassen es dem 4-s-
|
||||
// Timeout (der dann SERVFAIL liefert). Ein vorzeitiger Abbruch bei
|
||||
// kurzem Netz-Flackern wäre unnötig aggressiv.
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
connection.start(queue: forwardQueue)
|
||||
connection.start(queue: self.forwardQueue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Synthetisiert eine SERVFAIL-Response für ein Request-Paket und schreibt
|
||||
/// sie ins TUN. K2: gemeinsamer Fehler-Pfad für Timeout, recvError, leere
|
||||
/// Antwort, Connection-`.failed` und den H1-Cap.
|
||||
private func replyServFail(for requestPacket: Data) {
|
||||
if let response = DnsFilter.buildServFailResponse(request: requestPacket) {
|
||||
writeToTun(response)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── TUN-Write ──────────────────────────────────────────────────────────────
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user