mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:25:20 +00:00
Debugging the integration build on real iOS 26.5 devices over the air. - BitLogger DEBUG default minimumLevel now .debug (was .info) so a tap-launched sideload surfaces .debug decision logs without the BITCHAT_LOG_LEVEL env var; env still wins; release unchanged (.info, emits nothing). Extracted defaultMinimumLevel + a test asserting it. - Greppable bracket decision tags at .info (visible even at .info) via SecureLogger, ID-prefixes only, sanitized: [ROUTE] source-route vs flood origination (+reason) + fragment targeted-resync; [GW] uplink accept/reject, downlink rebroadcast/drop, loop skips, drain-timer; [WIFI] offer/accept/decline, AWDL connect, transfer start/complete+bytes, fallback+reason; [PREKEY] seal choice, consume, unknown-prekey open fail, bundle ingest, re-gossip; [SYNC] one concise summary per cycle + per-request served counts (no per-packet spam); [PING]/[TRACE] RTT + computed path. - New OSLog categories: mesh, gateway, transport. - Refactored BLESourceRouteOriginationPolicy.route -> decide returning a Decision enum (flood(reason)/route(hops)) so the flood reason is greppable and unit-tested. - DEBUG-only in-memory ring buffer (LogExportBuffer, ~2000 lines/512KB, thread-safe, off main thread) + App Info "Export Logs" share sheet (iOS UIActivityViewController / macOS NSSavePanel), VoiceOver labels. - DEBUG-only UDP LAN log sink (LogNetworkSink): opt-in, off by default, fire-and-forget, never blocks/throws; each line prefixed with the device nickname for demux; configured via App Info host:port fields. Same sanitized formatted line feeds buffer, sink, and export. All new code is #if DEBUG; release emits nothing and ships no export/sink UI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
4.2 KiB
Swift
105 lines
4.2 KiB
Swift
//
|
|
// LogNetworkSink.swift
|
|
// BitLogger
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
#if DEBUG
|
|
import Foundation
|
|
#if canImport(Network)
|
|
import Network
|
|
#endif
|
|
|
|
/// Best-effort, fire-and-forget UDP forwarder for sanitized log lines, so a
|
|
/// sideloaded device on iOS 17+ (where `idevicesyslog` Wi-Fi streaming no
|
|
/// longer works) can stream its logs to a LAN collector with no cable.
|
|
///
|
|
/// Design constraints:
|
|
/// - DEBUG-only: never compiled into release. A privacy-first release build
|
|
/// carries zero network-log code.
|
|
/// - Opt-in and off by default: nothing is ever sent until a collector host
|
|
/// is configured. An empty host means no egress.
|
|
/// - Never blocks the caller and never throws into the app: every send runs
|
|
/// on a private queue and failures are silently dropped. A dead or absent
|
|
/// collector cannot affect app behavior (UDP is connectionless — datagrams
|
|
/// to nowhere are simply lost).
|
|
/// - Forwards the exact same sanitized text the ring buffer captures, so no
|
|
/// secrets leave the device. Each line is prefixed with the device's label
|
|
/// (`[nickname] …`) so one Mac-side `nc -lu <port>` can demux all devices.
|
|
public final class LogNetworkSink {
|
|
public static let shared = LogNetworkSink()
|
|
|
|
/// UserDefaults keys shared with the in-app config UI (App Info sheet).
|
|
public static let hostDefaultsKey = "debug.logSink.host"
|
|
public static let portDefaultsKey = "debug.logSink.port"
|
|
public static let defaultPort = 9999
|
|
/// Where the device label is read from (the app's chosen nickname).
|
|
public static let nicknameDefaultsKey = "bitchat.nickname"
|
|
|
|
private let queue = DispatchQueue(label: "chat.bitchat.securelogger.netsink", qos: .utility)
|
|
private let defaults: UserDefaults
|
|
private var label = ""
|
|
private var enabled = false
|
|
#if canImport(Network)
|
|
private var connection: NWConnection?
|
|
#endif
|
|
|
|
init(defaults: UserDefaults = .standard) {
|
|
self.defaults = defaults
|
|
}
|
|
|
|
/// Re-read collector host/port and the device label from UserDefaults and
|
|
/// (re)build the UDP connection. Call on launch and whenever the config UI
|
|
/// changes. An empty host tears the sink down (no egress).
|
|
public func reloadConfiguration() {
|
|
let host = (defaults.string(forKey: Self.hostDefaultsKey) ?? "")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let storedPort = defaults.integer(forKey: Self.portDefaultsKey)
|
|
let port = storedPort > 0 ? storedPort : Self.defaultPort
|
|
let label = (defaults.string(forKey: Self.nicknameDefaultsKey) ?? "")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
configure(host: host, port: port, label: label)
|
|
}
|
|
|
|
/// Point the sink at `host:port` with a device `label`. Empty host or an
|
|
/// invalid port disables egress.
|
|
public func configure(host: String, port: Int, label: String) {
|
|
queue.async {
|
|
self.label = label
|
|
#if canImport(Network)
|
|
self.connection?.cancel()
|
|
self.connection = nil
|
|
guard !host.isEmpty,
|
|
(1...65535).contains(port),
|
|
let nwPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
|
|
self.enabled = false
|
|
return
|
|
}
|
|
let connection = NWConnection(host: NWEndpoint.Host(host), port: nwPort, using: .udp)
|
|
connection.start(queue: self.queue)
|
|
self.connection = connection
|
|
self.enabled = true
|
|
#else
|
|
self.enabled = false
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// Forward one already-formatted, already-sanitized line. Non-blocking;
|
|
/// drops silently when disabled or on any send failure.
|
|
func send(_ line: String) {
|
|
queue.async {
|
|
guard self.enabled else { return }
|
|
#if canImport(Network)
|
|
guard let connection = self.connection else { return }
|
|
let prefixed = self.label.isEmpty ? line : "[\(self.label)] \(line)"
|
|
guard let data = (prefixed + "\n").data(using: .utf8) else { return }
|
|
connection.send(content: data, completion: .idempotent)
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
#endif
|