diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0c65191..cddbe40 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -29,7 +29,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.9 + flutter-version: 3.38.10 - name: Use homebrew ruby run: | @@ -89,7 +89,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.7 + flutter-version: 3.38.10 - name: Accept licenses run: yes | flutter doctor --android-licenses @@ -159,7 +159,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.7 + flutter-version: 3.38.10 - name: Install Android SDK components run: | diff --git a/.github/workflows/lint-and-test.yaml b/.github/workflows/lint-and-test.yaml index 2b719f3..31bb7ce 100644 --- a/.github/workflows/lint-and-test.yaml +++ b/.github/workflows/lint-and-test.yaml @@ -47,7 +47,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: stable - flutter-version: 3.38.7 + flutter-version: 3.38.10 - name: get deps run: flutter pub get @@ -73,7 +73,7 @@ jobs: # uses: subosito/flutter-action@v2 # with: # channel: stable - # flutter-version: 3.38.7 + # flutter-version: 3.38.10 # - name: get deps # run: flutter pub get diff --git a/client/ios/Podfile.lock b/client/ios/Podfile.lock index c7747bb..ff20001 100644 --- a/client/ios/Podfile.lock +++ b/client/ios/Podfile.lock @@ -113,23 +113,23 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wireguard_plugin/darwin" SPEC CHECKSUMS: - app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a - cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c - device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + app_links: 585674be3c6661708e6cd794ab4f39fb9d8356f9 + cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 - local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb - mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100 + mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f + shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 52ecc4dfaae71f496da86159263dbce5d23a051a - url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b - wireguard_plugin: c2f4d5382eecd7bcd07c027642c75e0569f91ff8 + sqlite3_flutter_libs: 7bea6d85399aebaeb54e4f9845dcac6f5033cf22 + url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa + wireguard_plugin: 4d2720563b180d23101f7162ecdbf57203e82e8e PODFILE CHECKSUM: ae9e65fc23486119b8e977fd41d9213f251e537a diff --git a/client/ios/VPNExtension/Adapter.swift b/client/ios/VPNExtension/Adapter.swift index 713fed9..bd36500 100644 --- a/client/ios/VPNExtension/Adapter.swift +++ b/client/ios/VPNExtension/Adapter.swift @@ -1,7 +1,6 @@ import Foundation import Network import NetworkExtension -import os /// State of Adapter. enum State { @@ -13,7 +12,7 @@ enum State { case dormant } -final class Adapter /*: Sendable*/ { +@preconcurrency final class Adapter /*: Sendable*/ { /// Packet tunnel provider. private weak var packetTunnelProvider: NEPacketTunnelProvider? /// BortingTun tunnel @@ -25,17 +24,26 @@ final class Adapter /*: Sendable*/ { /// Network routes monitor. private var networkMonitor: NWPathMonitor? /// Keep alive timer - private var keepAliveTimer: Timer? - /// Logging - private lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Adapter") + private var keepAliveTimer: DispatchSourceTimer? + /// Unified logger (writes to both system log and file) + private let log = Log(category: "Adapter") /// Adapter state. private var state: State = .stopped - private var reconnectOnExpiry: Bool = false + /// Serialize tunnel I/O and connection state changes off the main queue. + private let ioQueue = DispatchQueue(label: "net.defguard.VPNExtension.adapter") + private let ioQueueKey = DispatchSpecificKey() + + /// For statistics returned to Rust code. + var locationId: UInt64? + var tunnelId: UInt64? + + private let notificationCenter = CFNotificationCenterGetDarwinNotifyCenter() /// Designated initializer. /// - Parameter packetTunnelProvider: an instance of `NEPacketTunnelProvider`. Internally stored init(with packetTunnelProvider: NEPacketTunnelProvider) { self.packetTunnelProvider = packetTunnelProvider + self.ioQueue.setSpecific(key: ioQueueKey, value: ()) } deinit { @@ -43,8 +51,49 @@ final class Adapter /*: Sendable*/ { } func start(tunnelConfiguration: TunnelConfiguration) throws { - if let _ = tunnel { - logger.info("Cleaning exiting Tunnel") + try syncOnQueue { + try startOnQueue(tunnelConfiguration: tunnelConfiguration) + } + } + + func stop() { + syncOnQueue { + stopOnQueue() + } + } + + // Obtain tunnel statistics. + func stats() -> Stats? { + syncOnQueue { + guard let stats = tunnel?.stats() else { return nil } + return Stats( + txBytes: stats.txBytes, + rxBytes: stats.rxBytes, + lastHandshake: stats.lastHandshake, + locationId: locationId, + tunnelId: tunnelId + ) + } + } + + private func syncOnQueue(_ work: () throws -> T) rethrows -> T { + if DispatchQueue.getSpecific(key: ioQueueKey) == nil { + return try ioQueue.sync { + try work() + } + } + return try work() + } + + private func startOnQueue(tunnelConfiguration: TunnelConfiguration) throws { + guard case .stopped = self.state else { + log.error("Invalid state - cannot start tunnel") + // TODO: throw invalid state + return + } + + if tunnel != nil { + log.info("Cleaning existing Tunnel") tunnel = nil connection = nil } @@ -53,108 +102,102 @@ final class Adapter /*: Sendable*/ { networkMonitor.pathUpdateHandler = { [weak self] path in self?.networkPathUpdate(path: path) } - networkMonitor.start(queue: .main) + networkMonitor.start(queue: ioQueue) self.networkMonitor = networkMonitor - logger.info("Initializing Tunnel") + log.info("Initializing Tunnel") tunnel = try Tunnel.init( - privateKey: tunnelConfiguration.interface.privateKey, + privateKey: tunnelConfiguration.privateKey, serverPublicKey: tunnelConfiguration.peers[0].publicKey, presharedKey: tunnelConfiguration.peers[0].preSharedKey, keepAlive: tunnelConfiguration.peers[0].persistentKeepAlive, index: 0 ) + locationId = tunnelConfiguration.locationId + tunnelId = tunnelConfiguration.tunnelId - if tunnelConfiguration.peers[0].preSharedKey != nil { - logger.info("Using pre-shared key, the tunnel won't be re-established on expiry") - reconnectOnExpiry = false - } else { - logger.info("No pre-shared key, the tunnel will be re-established on expiry") - reconnectOnExpiry = true - } - - logger.info("Connecting to endpoint") + log.info( + "Connecting to endpoint (locationId: \(tunnelConfiguration.locationId ?? 0), tunnelId: \(tunnelConfiguration.tunnelId ?? 0))" + ) guard let endpoint = tunnelConfiguration.peers[0].endpoint else { - logger.error("Endpoint is nil") + log.error("Endpoint is nil, cannot connect") return } self.endpoint = endpoint.asNWEndpoint() initEndpoint() - logger.info("Sniffing packets") + log.info("Starting to sniff packets") readPackets() state = .running + log.info("Tunnel started successfully") } - func stop() { - logger.info("Stopping Adapter") + private func stopOnQueue() { + log.info("Stopping Adapter") connection?.cancel() connection = nil tunnel = nil - keepAliveTimer?.invalidate() + keepAliveTimer?.cancel() keepAliveTimer = nil // Cancel network monitor networkMonitor?.cancel() networkMonitor = nil + state = .stopped - logger.info("Tunnel stopped") + log.info("Tunnel stopped") + log.flush() } private func handleTunnelResult(_ result: TunnelResult) { + var tunnelPackets = [NEPacket]() + handleTunnelResult(result, tunnelPackets: &tunnelPackets) + flushTunnelPackets(tunnelPackets) + } + + private func handleTunnelResult(_ result: TunnelResult, tunnelPackets: inout [NEPacket]) { switch result { - case .done: - // Nothing to do. - break - case .err(let error): - logger.error("Tunnel error \(error, privacy: .public)") - switch error { - case .InvalidAeadTag: - logger.error("Invalid pre-shared key; stopping tunnel") - // The correct way is to call the packet tunnel provider, if there is one. - if let provider = packetTunnelProvider { - provider.cancelTunnelWithError(error) - } else { - stop() - } - case .ConnectionExpired: - packetTunnelProvider?.reasserting = true - if self.reconnectOnExpiry { - logger.error("Connecion has expired; re-connecting") - initEndpoint() - logger.info("Finished re-connecting") - } else { - logger.error("Connection has expired; stopping tunnel") - let defaults = UserDefaults(suiteName: suiteName) - defaults?.set( - TunnelStopError.mfaSessionExpired.rawValue - , forKey: "lastTunnelError") - if let provider = packetTunnelProvider { - provider.cancelTunnelWithError(error) - } else { - stop() - } - } - packetTunnelProvider?.reasserting = false - default: - break + case .done: + // Nothing to do. + break + case .err(let error): + log.error("Tunnel error: \(error)") + switch error { + case .InvalidAeadTag: + log.error("Invalid pre-shared key; stopping tunnel") + // The correct way is to call the packet tunnel provider, if there is one. + if let provider = packetTunnelProvider { + provider.cancelTunnelWithError(error) + } else { + stop() } - case .writeToNetwork(let data): - sendToEndpoint(data: data) - case .writeToTunnelV4(let data): - packetTunnelProvider?.packetFlow.writePacketObjects([ - NEPacket(data: data,protocolFamily: sa_family_t(AF_INET))]) - case .writeToTunnelV6(let data): - packetTunnelProvider?.packetFlow.writePacketObjects([ - NEPacket(data: data, protocolFamily: sa_family_t(AF_INET6))]) + case .ConnectionExpired: + log.warning("Connection has expired; re-connecting") + packetTunnelProvider?.reasserting = true + initEndpoint() + packetTunnelProvider?.reasserting = false + default: + break + } + case .writeToNetwork(let data): + sendToEndpoint(data: data) + case .writeToTunnelV4(let data): + tunnelPackets.append(NEPacket(data: data, protocolFamily: sa_family_t(AF_INET))) + case .writeToTunnelV6(let data): + tunnelPackets.append(NEPacket(data: data, protocolFamily: sa_family_t(AF_INET6))) } } + private func flushTunnelPackets(_ tunnelPackets: [NEPacket]) { + guard !tunnelPackets.isEmpty else { return } + packetTunnelProvider?.packetFlow.writePacketObjects(tunnelPackets) + } + /// Initialise UDP connection to endpoint. private func initEndpoint() { guard let endpoint = endpoint else { return } - logger.info("Init Endpoint") + log.info("Initializing endpoint connection to: \(endpoint)") // Cancel previous connection connection?.cancel() connection = nil @@ -166,43 +209,53 @@ final class Adapter /*: Sendable*/ { self?.endpointStateChange(state: state) } - connection.start(queue: .main) + connection.start(queue: ioQueue) self.connection = connection } /// Setup UDP connection to endpoint. This method should be called when UDP connection is ready to send and receive. private func setupEndpoint() { - logger.info("Setup endpoint") + log.info("Setting up endpoint") // Send initial handshake packet if let tunnel = self.tunnel { + log.info("Sending initial handshake") handleTunnelResult(tunnel.forceHandshake()) } - logger.info("Receiving UDP from endpoint") + log.info("Starting UDP receive loop") + log.debug("NWConnection path: \(String(describing: self.connection?.currentPath))") receive() - // Use Timer to send keep-alive packets. - keepAliveTimer?.invalidate() - logger.info("Creating keep-alive timer") - let timer = Timer(timeInterval: 0.25, repeats: true) { [weak self] timer in + // Use a dispatch timer to avoid bouncing keep-alives through the main run loop. + keepAliveTimer?.cancel() + log.info("Creating keep-alive timer") + let timer = DispatchSource.makeTimerSource(queue: ioQueue) + timer.schedule( + deadline: .now() + .milliseconds(250), + repeating: .milliseconds(250), + leeway: .milliseconds(25) + ) + timer.setEventHandler { [weak self] in guard let self = self, let tunnel = self.tunnel else { return } self.handleTunnelResult(tunnel.tick()) } keepAliveTimer = timer - RunLoop.main.add(timer, forMode: .common) + timer.resume() } /// Send packets to UDP endpoint. private func sendToEndpoint(data: Data) { guard let connection = connection else { return } if connection.state == .ready { - connection.send(content: data, completion: .contentProcessed { error in - if let error = error { - self.logger.error("UDP connection send error: \(error, privacy: .public)") - } - }) + connection.send( + content: data, + completion: .contentProcessed { [weak self] error in + if let error = error { + self?.log.error("UDP connection send error: \(error)") + } + }) } else { - logger.warning("UDP connection not ready to send") + log.warning("UDP connection not ready to send") } } @@ -211,60 +264,89 @@ final class Adapter /*: Sendable*/ { connection?.receiveMessage { [weak self] data, context, isComplete, error in guard let self = self else { return } if let data = data, let tunnel = self.tunnel { - self.handleTunnelResult(tunnel.read(src: data)) + autoreleasepool { + self.handleTunnelResult(tunnel.read(src: data)) + } } if error == nil { // continue receiving self.receive() + } else { + self.log.error("receive() error: \(String(describing: error))") } } } /// Read tunnel packets. private func readPackets() { + // Packets received to the tunnel's virtual interface. + packetTunnelProvider?.packetFlow.readPacketObjects { [weak self] packets in + guard let self = self else { return } + + self.ioQueue.async { + self.processTunnelPackets(packets) + + // continue reading + self.readPackets() + } + } + } + + private func processTunnelPackets(_ packets: [NEPacket]) { guard let tunnel = self.tunnel else { return } - // Packets received to the tunnel's virtual interface. - packetTunnelProvider?.packetFlow.readPacketObjects { packets in - for packet in packets { - self.handleTunnelResult(tunnel.write(src: packet.data)) + var tunnelPackets = [NEPacket]() + tunnelPackets.reserveCapacity(packets.count) + + for packet in packets { + autoreleasepool { + self.handleTunnelResult(tunnel.write(src: packet.data), tunnelPackets: &tunnelPackets) } - // continue reading - self.readPackets() } + + flushTunnelPackets(tunnelPackets) } /// Handle UDP connection state changes. private func endpointStateChange(state: NWConnection.State) { - logger.debug("UDP connection state: \(String(describing: state), privacy: .public)") + log.debug("UDP connection state changed: \(state)") switch state { - case .ready: - setupEndpoint() - case .failed(let error): - logger.error("Failed to establish endpoint connection: \(error)") - // The correct way is to call the packet tunnel provider, if there is one. - if let provider = packetTunnelProvider { - provider.cancelTunnelWithError(error) - } else { - stop() - } - default: - break + case .ready: + setupEndpoint() + //case .waiting(let error): + // switch error { + // case .posix(_): + // connection?.restart() + // default: + // self.stop() + // } + case .failed(let error): + log.error("Failed to establish endpoint connection: \(error)") + // The correct way is to call the packet tunnel provider, if there is one. + if let provider = packetTunnelProvider { + provider.cancelTunnelWithError(error) + } else { + stop() + } + default: + break } } /// Handle network path updates. private func networkPathUpdate(path: Network.NWPath) { + log.debug( + "Network path update - status: \(path.status), interfaces: \(path.availableInterfaces)") if path.status == .unsatisfied { if state == .running { - logger.warning("Unsatisfied network path: going dormant") + log.warning("Unsatisfied network path: going dormant") connection?.cancel() connection = nil state = .dormant } } else { if state == .dormant { - logger.warning("Satisfied network path: going running") + log.warning("Satisfied network path: going running") initEndpoint() state = .running } diff --git a/client/ios/VPNExtension/Decodabe+Encodable.swift b/client/ios/VPNExtension/Decodabe+Encodable.swift index bf01a54..d3f8a8f 100644 --- a/client/ios/VPNExtension/Decodabe+Encodable.swift +++ b/client/ios/VPNExtension/Decodabe+Encodable.swift @@ -2,7 +2,7 @@ import Foundation extension Decodable { static func from(dictionary: [String: Any]) throws -> Self { - let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) + let data = try JSONSerialization.data(withJSONObject: dictionary) let decoder = JSONDecoder() return try decoder.decode(Self.self, from: data) } @@ -13,7 +13,9 @@ extension Encodable { let data = try JSONEncoder().encode(self) let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) guard let dictionary = jsonObject as? [String: Any] else { - throw NSError(domain: "EncodingError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to convert to dictionary"]) + throw NSError( + domain: "EncodingError", code: 0, + userInfo: [NSLocalizedDescriptionKey: "Failed to convert to dictionary"]) } return dictionary } diff --git a/client/ios/VPNExtension/Endpoint.swift b/client/ios/VPNExtension/Endpoint.swift index 790aee3..64f50a4 100644 --- a/client/ios/VPNExtension/Endpoint.swift +++ b/client/ios/VPNExtension/Endpoint.swift @@ -1,3 +1,4 @@ +import Foundation import Network struct Endpoint: Codable, CustomStringConvertible { @@ -15,9 +16,11 @@ struct Endpoint: Codable, CustomStringConvertible { var endpointHost = trimmedEndpoint // Extract host, supporting IPv4, IPv6, and domains - if trimmedEndpoint.hasPrefix("[") { // IPv6 with port, e.g. [fd00::1]:51820 + if trimmedEndpoint.hasPrefix("[") { // IPv6 with port, e.g. [fd00::1]:51820 if let closing = trimmedEndpoint.firstIndex(of: "]") { - endpointHost = String(trimmedEndpoint[trimmedEndpoint.index(after: trimmedEndpoint.startIndex).. String { + "\(host):\(port)" } + // Encode to a single string "host:port", to smoothly encode into JSON. func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode("\(host)", forKey: .host) - try container.encode(port.rawValue, forKey: .port) + var container = encoder.singleValueContainer() + try container.encode(self.toString()) } + // Decode from a single string "host:port", to smoothly decode from JSON. init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - - host = try NWEndpoint.Host(values.decode(String.self, forKey: .host)) - port = try NWEndpoint.Port(rawValue: values.decode(UInt16.self, forKey: .port)) ?? 0 + let container = try decoder.singleValueContainer() + let value = try container.decode(String.self) + guard let endpoint = Endpoint(from: value) else { + throw + DecodingError + .dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Not in host:port format") + ) + } + self = endpoint } func asNWEndpoint() -> NWEndpoint { diff --git a/client/ios/VPNExtension/FileLogger.swift b/client/ios/VPNExtension/FileLogger.swift new file mode 100644 index 0000000..71d0297 --- /dev/null +++ b/client/ios/VPNExtension/FileLogger.swift @@ -0,0 +1,251 @@ +import Foundation +import os + +/// Log levels +enum LogLevel: String { + case debug = "DEBUG" + case info = "INFO" + case warning = "WARN" + case error = "ERROR" + + var osLogType: OSLogType { + switch self { + case .debug: return .debug + case .info: return .info + case .warning: return .default + case .error: return .error + } + } +} + +/// Logger that writes to both system log (os.Logger) and file. +/// Use this instead of os.Logger directly to get dual logging with a single call. +final class Log { + /// The category for this logger instance (usually class name), e.g. "PacketTunnelProvider" + let category: String + private let systemLogger: Logger +#if os(macOS) + private let fileLogger = FileLogger.shared +#endif + + init(category: String) { + self.category = category + self.systemLogger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "net.defguard.VPNExtension", + category: category + ) + } + + func debug(_ message: String) { + systemLogger.debug("\(message, privacy: .public)") +#if os(macOS) + fileLogger.log(level: .debug, message: message, category: category) +#endif + } + + func info(_ message: String) { + systemLogger.info("\(message, privacy: .public)") +#if os(macOS) + fileLogger.log(level: .info, message: message, category: category) +#endif + } + + func warning(_ message: String) { + systemLogger.warning("\(message, privacy: .public)") +#if os(macOS) + fileLogger.log(level: .warning, message: message, category: category) +#endif + } + + func error(_ message: String) { + systemLogger.error("\(message, privacy: .public)") +#if os(macOS) + fileLogger.log(level: .error, message: message, category: category) +#endif + } + + func flush() { +#if os(macOS) + fileLogger.flush() +#endif + } +} + +#if os(macOS) +/// A file-based logger that writes to an App Group shared container. +/// This allows the main rust app to read logs from the network extension. +/// Use the `Log` class instead of this directly for unified logging. +final class FileLogger { + static let shared = FileLogger() + static let appGroupIdentifier = "group.net.defguard" + private let logFileName = "vpn-extension.log" + private let maxLogFileSize: UInt64 = 5 * 1024 * 1024 // 5 MB + private let maxBackupFiles = 3 + private let flushInterval = 5 // Flush every N log entries + private var fileHandle: FileHandle? + private var logFileURL: URL? + private var unflushedCount = 0 + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone(identifier: "UTC") + return formatter + }() + + private let queue = DispatchQueue(label: "net.defguard.VPNExtension.filelogger") + + private let internalLogger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "net.defguard.VPNExtension", + category: "FileLogger") + + private init() { + setupLogFile() + } + + deinit { + closeLogFile() + } + + private func setupLogFile() { + guard + let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: Self.appGroupIdentifier) + else { + internalLogger.error( + "Failed to get App Group container URL for \(Self.appGroupIdentifier)") + return + } + + let logsDirectory = containerURL.appendingPathComponent("Logs", isDirectory: true) + + do { + try FileManager.default.createDirectory( + at: logsDirectory, withIntermediateDirectories: true, attributes: nil) + } catch { + internalLogger.error("Failed to create Logs directory: \(error.localizedDescription)") + return + } + + logFileURL = logsDirectory.appendingPathComponent(logFileName) + + guard let logFileURL = logFileURL else { return } + + if !FileManager.default.fileExists(atPath: logFileURL.path) { + FileManager.default.createFile(atPath: logFileURL.path, contents: nil, attributes: nil) + } + + do { + fileHandle = try FileHandle(forWritingTo: logFileURL) + fileHandle?.seekToEndOfFile() + + let startupMessage = + "# VPN Extension Log Started at \(dateFormatter.string(from: Date()))\n" + if let data = startupMessage.data(using: .utf8) { + fileHandle?.write(data) + } + } catch { + internalLogger.error( + "Failed to open log file for writing: \(error.localizedDescription)") + } + + internalLogger.info("FileLogger initialized at: \(logFileURL.path)") + } + + private func closeLogFile() { + queue.sync { + try? fileHandle?.synchronize() + try? fileHandle?.close() + fileHandle = nil + } + } + + /// Rotate log files if the current one exceeds the maximum size + private func rotateLogFilesIfNeeded() { + guard let logFileURL = logFileURL else { return } + + do { + let attributes = try FileManager.default.attributesOfItem(atPath: logFileURL.path) + if let fileSize = attributes[.size] as? UInt64, fileSize >= maxLogFileSize { + rotateLogFiles() + } + } catch { + } + } + + private func rotateLogFiles() { + guard let logFileURL = logFileURL else { return } + + try? fileHandle?.synchronize() + try? fileHandle?.close() + fileHandle = nil + + let fileManager = FileManager.default + let directory = logFileURL.deletingLastPathComponent() + let baseName = logFileURL.deletingPathExtension().lastPathComponent + let ext = logFileURL.pathExtension + + // Remove oldest backup if it exists + let oldestBackup = directory.appendingPathComponent("\(baseName).\(maxBackupFiles).\(ext)") + try? fileManager.removeItem(at: oldestBackup) + + for i in stride(from: maxBackupFiles - 1, through: 1, by: -1) { + let current = directory.appendingPathComponent("\(baseName).\(i).\(ext)") + let next = directory.appendingPathComponent("\(baseName).\(i + 1).\(ext)") + try? fileManager.moveItem(at: current, to: next) + } + + let firstBackup = directory.appendingPathComponent("\(baseName).1.\(ext)") + try? fileManager.moveItem(at: logFileURL, to: firstBackup) + + fileManager.createFile(atPath: logFileURL.path, contents: nil, attributes: nil) + + do { + fileHandle = try FileHandle(forWritingTo: logFileURL) + fileHandle?.seekToEndOfFile() + } catch { + internalLogger.error( + "Failed to reopen log file after rotation: \(error.localizedDescription)") + } + } + + /// Write a log message to the file + /// - level: Log level (debug, info, warning, error) + /// - message: The message to log + /// - category: Optional category/subsystem + func log(level: LogLevel, message: String, category: String? = nil) { + queue.async { [weak self] in + guard let self = self, let fileHandle = self.fileHandle else { return } + + self.rotateLogFilesIfNeeded() + + let timestamp = self.dateFormatter.string(from: Date()) + let categoryStr = category.map { "[\($0)] " } ?? "" + let logLine = "\(timestamp) [\(level.rawValue)] \(categoryStr)\(message)\n" + + if let data = logLine.data(using: .utf8) { + fileHandle.write(data) + self.unflushedCount += 1 + + // Flush for important messages or periodically + if level == .error || level == .warning || self.unflushedCount >= self.flushInterval + { + try? fileHandle.synchronize() + self.unflushedCount = 0 + } + } + } + } + + func flush() { + queue.sync { + try? fileHandle?.synchronize() + } + } + + var logFilePath: String? { + return logFileURL?.path + } +} +#endif diff --git a/client/ios/VPNExtension/InterfaceConfiguration.swift b/client/ios/VPNExtension/InterfaceConfiguration.swift deleted file mode 100644 index c73f539..0000000 --- a/client/ios/VPNExtension/InterfaceConfiguration.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Foundation -import NetworkExtension - -final class InterfaceConfiguration: Codable { - var privateKey: String - var addresses: [IpAddrMask] = [] - var listenPort: UInt16? - var mtu: UInt32? - var dns: [String] = [] - var dnsSearch: [String] = [] - - init(privateKey: String) { - self.privateKey = privateKey - } -} diff --git a/client/ios/VPNExtension/IpAddrMask.swift b/client/ios/VPNExtension/IpAddrMask.swift index 32f972d..fefd845 100644 --- a/client/ios/VPNExtension/IpAddrMask.swift +++ b/client/ios/VPNExtension/IpAddrMask.swift @@ -44,7 +44,7 @@ struct IpAddrMask: Codable, Equatable { /// Conform to `Encodable`. func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(address.rawValue, forKey: .address) + try container.encode("\(address)", forKey: .address) try container.encode(cidr, forKey: .cidr) } @@ -52,39 +52,21 @@ struct IpAddrMask: Codable, Equatable { init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - let address_data = try values.decode(Data.self, forKey: .address) - switch address_data.count { - case 4: - guard let ipv4 = IPv4Address(address_data) else { - throw - DecodingError - .dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Unable to decode IP v4 address" - )) - - } + let address_string = try values.decode(String.self, forKey: .address) + if let ipv4 = IPv4Address(address_string) { address = ipv4 - case 16: - guard let ipv6 = IPv6Address(address_data) else { - throw - DecodingError - .dataCorrupted( - DecodingError.Context( - codingPath: decoder.codingPath, - debugDescription: "Unable to decode IP v6 address" - )) - - } + } else if let ipv6 = IPv6Address(address_string) { address = ipv6 - default: - throw DecodingError.typeMismatch( - IpAddrMask.self, - DecodingError.Context( - codingPath: decoder.codingPath, debugDescription: "Invalid IP address length" - )) + } else { + throw + DecodingError + .dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unable to decode IP address" + )) } + cidr = try values.decode(UInt8.self, forKey: .cidr) } diff --git a/client/ios/VPNExtension/PacketTunnelProvider.swift b/client/ios/VPNExtension/PacketTunnelProvider.swift index 159bf97..d6aa56a 100644 --- a/client/ios/VPNExtension/PacketTunnelProvider.swift +++ b/client/ios/VPNExtension/PacketTunnelProvider.swift @@ -1,96 +1,97 @@ import NetworkExtension -import os -import Network -enum VPNEventType: String { - case tunnelUp = "tunnel_up" - case tunnelDown = "tunnel_down" - case tunnelError = "tunnel_error" - case connectionStatusChanged = "connection_status_changed" - case bytesTransferred = "bytes_transferred" +enum WireGuardTunnelError: Error { + case invalidTunnelConfiguration } class PacketTunnelProvider: NEPacketTunnelProvider { - /// Logging - private var logger = Logger( - subsystem: Bundle.main.bundleIdentifier!, - category: "PacketTunnelProvider" - ) + /// Unified logger (writes to both system log and file) + private let log = Log(category: "PacketTunnelProvider") private lazy var adapter: Adapter = { return Adapter(with: self) }() - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - guard let tunnelConfig = extractTunnelConfiguration() else { - let error = NSError(domain: "VPNExtension", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Tunnel configuration is missing or invalid."]) - logger.error("Tunnel configuration is missing or invalid.") - completionHandler(error) - return + override func startTunnel( + options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void + ) { + if let options = options { + log.debug("Options: \(options)") } - logger.log("Starting tunnel with configuration: \(String(describing: tunnelConfig), privacy: .public)") + guard let protocolConfig = self.protocolConfiguration as? NETunnelProviderProtocol, + let providerConfig = protocolConfig.providerConfiguration + else { + log.error("Failed to parse provider configuration") + completionHandler(WireGuardTunnelError.invalidTunnelConfiguration) + return + } - guard Endpoint(from: tunnelConfig.endpoint) != nil else { - let error = NSError(domain: "VPNExtension", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid endpoint format: \(tunnelConfig.endpoint)"]) - logger.error("Invalid endpoint format: \(tunnelConfig.endpoint, privacy: .public)") - completionHandler(error) +#if os(macOS) + guard let tunnelConfig = try? TunnelConfiguration.from(dictionary: providerConfig) + else { + log.error("Failed to parse tunnel configuration") + completionHandler(WireGuardTunnelError.invalidTunnelConfiguration) return } +#else + guard let startData = try? TunnelStartData.from(dictionary: providerConfig) + else { + log.error("Failed to parse tunnel configuration") + completionHandler(WireGuardTunnelError.invalidTunnelConfiguration) + return + } + let tunnelConfig = TunnelConfiguration(fromStartData: startData) +#endif - let tunnelConfiguration = TunnelConfiguration(fromStartData: tunnelConfig) - let networkSettings = tunnelConfiguration.asNetworkSettings() - - setTunnelNetworkSettings(networkSettings) { [weak self] error in - guard let self = self else { return } - - if let error = error { - logger.warning("Set tunnel network settings returned an error \(error, privacy: .public)") - completionHandler(error) - return - } - - do { - try self.adapter.start(tunnelConfiguration: tunnelConfiguration) - } catch { - logger.error("Failed to start adapter with error: \(error.localizedDescription, privacy: .public)") - completionHandler(error) - return + let networkSettings = tunnelConfig.asNetworkSettings() + self.setTunnelNetworkSettings(networkSettings) { error in + if error != nil { + self.log.error("Failed to set tunnel network settings: \(String(describing: error))") } + completionHandler(error) + return + } - logger.log("Tunnel started successfully") - completionHandler(nil) + do { + try adapter.start(tunnelConfiguration: tunnelConfig) + } catch { + log.error("Failed to start tunnel: \(error)") + completionHandler(error) } + log.info("Tunnel started successfully") + + completionHandler(nil) } - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - self.adapter.stop() + override func stopTunnel( + with reason: NEProviderStopReason, completionHandler: @escaping () -> Void + ) { + adapter.stop() + log.info("Tunnel stopped") completionHandler() } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - logger.debug("\(#function)") + // TODO: messageData should contain a valid message. if let handler = completionHandler { - handler(messageData) + if let stats = adapter.stats() { + let data = try? JSONEncoder().encode(stats) + handler(data) + } else { + handler(nil) + } } } override func sleep(completionHandler: @escaping () -> Void) { - logger.debug("\(#function)") + log.info("System going to sleep") + // Add code here to get ready to sleep. completionHandler() } override func wake() { - logger.debug("\(#function)") - } - - // MARK: - Helpers - - private func extractTunnelConfiguration() -> TunnelStartData? { - guard let providerConfig = (self.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration as? [String: Any] else { - return nil - } - return try? TunnelStartData.from(dictionary: providerConfig) + log.info("System waking up") + // Add code here to wake up. } } diff --git a/client/ios/VPNExtension/Peer.swift b/client/ios/VPNExtension/Peer.swift index e30b61b..fc07220 100644 --- a/client/ios/VPNExtension/Peer.swift +++ b/client/ios/VPNExtension/Peer.swift @@ -4,23 +4,26 @@ final class Peer: Codable { var publicKey: String var preSharedKey: String? var endpoint: Endpoint? + var persistentKeepAlive: UInt16? + var allowedIPs = [IpAddrMask]() + // Statistics var lastHandshake: Date? var txBytes: UInt64 = 0 var rxBytes: UInt64 = 0 - var persistentKeepAlive: UInt16? - var allowedIPs = [IpAddrMask]() - init(publicKey: String, preSharedKey: String? = nil, endpoint: Endpoint? = nil, - lastHandshake: Date? = nil, txBytes: UInt64 = 0, rxBytes: UInt64 = 0, - persistentKeepAlive: UInt16? = nil, allowedIPs: [IpAddrMask] = [IpAddrMask]()) { + init( + publicKey: String, preSharedKey: String? = nil, endpoint: Endpoint? = nil, + persistentKeepAlive: UInt16? = nil, allowedIPs: [IpAddrMask] = [IpAddrMask](), + lastHandshake: Date? = nil, txBytes: UInt64 = 0, rxBytes: UInt64 = 0, + ) { self.publicKey = publicKey self.preSharedKey = preSharedKey self.endpoint = endpoint + self.persistentKeepAlive = persistentKeepAlive + self.allowedIPs = allowedIPs self.lastHandshake = lastHandshake self.txBytes = txBytes self.rxBytes = rxBytes - self.persistentKeepAlive = persistentKeepAlive - self.allowedIPs = allowedIPs } init(publicKey: String) { @@ -31,10 +34,11 @@ final class Peer: Codable { case publicKey case preSharedKey case endpoint - case lastHandshake - case txBytes - case rxBytes case persistentKeepAlive case allowedIPs + // There isn't any need to encode/decode these ephemeral fields. + // case lastHandshake + // case txBytes + // case rxBytes } } diff --git a/client/ios/VPNExtension/Stats.swift b/client/ios/VPNExtension/Stats.swift new file mode 100644 index 0000000..2c83344 --- /dev/null +++ b/client/ios/VPNExtension/Stats.swift @@ -0,0 +1,18 @@ +import ObjectiveC + +public class Stats: NSObject, Codable { + var txBytes: UInt64 + var rxBytes: UInt64 + var lastHandshake: UInt64 + // One or the other. + var locationId: UInt64? + var tunnelId: UInt64? + + init(txBytes: UInt64, rxBytes: UInt64, lastHandshake: UInt64, locationId: UInt64?, tunnelId: UInt64?) { + self.txBytes = txBytes + self.rxBytes = rxBytes + self.lastHandshake = lastHandshake + self.locationId = locationId + self.tunnelId = tunnelId + } +} diff --git a/client/ios/VPNExtension/TunnelConfiguration.swift b/client/ios/VPNExtension/TunnelConfiguration.swift index 33765df..ca4f76e 100644 --- a/client/ios/VPNExtension/TunnelConfiguration.swift +++ b/client/ios/VPNExtension/TunnelConfiguration.swift @@ -2,14 +2,23 @@ import Foundation import NetworkExtension final class TunnelConfiguration: Codable { - var name: String - var interface: InterfaceConfiguration - var peers: [Peer] + // One or the other. + var locationId: UInt64? + var tunnelId: UInt64? - init(name: String, interface: InterfaceConfiguration, peers: [Peer]) { - self.interface = interface - self.peers = peers + var name: String + var privateKey: String + var addresses: [IpAddrMask] = [] + var listenPort: UInt16? + var peers: [Peer] = [] + var mtu: UInt32? + var dns: [String] = [] + var dnsSearch: [String] = [] + + init(name: String, privateKey: String, peers: [Peer]) { self.name = name + self.privateKey = privateKey + self.peers = peers let peerPublicKeysArray = peers.map { $0.publicKey } let peerPublicKeysSet = Set(peerPublicKeysArray) @@ -18,11 +27,18 @@ final class TunnelConfiguration: Codable { } } - // Only encode these properties. + /// Only encode these properties. enum CodingKeys: String, CodingKey { + case locationId + case tunnelId case name - case interface + case privateKey + case addresses + case listenPort case peers + case mtu + case dns + case dnsSearch } func asNetworkSettings() -> NEPacketTunnelNetworkSettings { @@ -32,31 +48,31 @@ final class TunnelConfiguration: Codable { let (ipv4IncludedRoutes, ipv6IncludedRoutes) = routes() // IPv4 addresses - let addrs_v4 = interface.addresses.filter { $0.address is IPv4Address } + let addrs_v4 = addresses.filter { $0.address is IPv4Address } .map { String(describing: $0.address) } - let masks_v4 = interface.addresses.filter { $0.address is IPv4Address } + let masks_v4 = addresses.filter { $0.address is IPv4Address } .map { String(describing: $0.mask()) } let ipv4Settings = NEIPv4Settings(addresses: addrs_v4, subnetMasks: masks_v4) ipv4Settings.includedRoutes = ipv4IncludedRoutes networkSettings.ipv4Settings = ipv4Settings // IPv6 addresses - let addrs_v6 = interface.addresses.filter { $0.address is IPv6Address } + let addrs_v6 = addresses.filter { $0.address is IPv6Address } .map { String(describing: $0.address) } // IMPORTANT: macOS/iOS has limitations handling IPv6 prefix masks longer than /120 due to // standards compliance and implementation choices in its network stack. - let masks_v6 = interface.addresses.filter { $0.address is IPv6Address } + let masks_v6 = addresses.filter { $0.address is IPv6Address } .map { NSNumber(value: min(120, $0.cidr)) } let ipv6Settings = NEIPv6Settings(addresses: addrs_v6, networkPrefixLengths: masks_v6) ipv6Settings.includedRoutes = ipv6IncludedRoutes networkSettings.ipv6Settings = ipv6Settings - networkSettings.mtu = interface.mtu as NSNumber? + networkSettings.mtu = mtu as NSNumber? networkSettings.tunnelOverheadBytes = 80 - let dnsSettings = NEDNSSettings(servers: interface.dns) - dnsSettings.searchDomains = interface.dnsSearch - if !interface.dns.isEmpty { + let dnsSettings = NEDNSSettings(servers: dns) + dnsSettings.searchDomains = dnsSearch + if !dns.isEmpty { // Make all DNS queries go through the tunnel. dnsSettings.matchDomains = [""] } @@ -71,7 +87,7 @@ final class TunnelConfiguration: Codable { var ipv6IncludedRoutes = [NEIPv6Route]() // Routes to interface addresses. - for addr_mask in interface.addresses { + for addr_mask in addresses { if addr_mask.address is IPv4Address { let route = NEIPv4Route( destinationAddress: "\(addr_mask.maskedAddress())", @@ -108,6 +124,7 @@ final class TunnelConfiguration: Codable { return (ipv4IncludedRoutes, ipv6IncludedRoutes) } +#if os(iOS) /// Helper function allowing to parse comma-separated string of addresses. private func parseAddresses(fromString string: String) -> [IpAddrMask] { var addresses: [IpAddrMask] = [] @@ -125,23 +142,22 @@ final class TunnelConfiguration: Codable { init(fromStartData startData: TunnelStartData) { name = startData.locationName - interface = InterfaceConfiguration(privateKey: startData.privateKey) + privateKey = startData.privateKey let peer = Peer(publicKey: startData.publicKey) peers = [peer] - interface.addresses = self.parseAddresses(fromString: startData.address) + addresses = self.parseAddresses(fromString: startData.address) // DNS settings - let dnsRecords = - startData.dns?.split(separator: ",").map { - $0.trimmingCharacters(in: .whitespaces) - } ?? [] + let dnsRecords = startData.dns?.split(separator: ",").map { + $0.trimmingCharacters(in: .whitespaces) + } ?? [] if !dnsRecords.isEmpty { for record in dnsRecords { if IPv4Address(record) != nil || IPv6Address(record) != nil { - interface.dns.append(record) + dns.append(record) } else { - interface.dnsSearch.append(record) + dnsSearch.append(record) } } } @@ -151,7 +167,7 @@ final class TunnelConfiguration: Codable { peer.endpoint = Endpoint(from: startData.endpoint) peer.persistentKeepAlive = UInt16(startData.keepalive) peer.allowedIPs = - switch startData.traffic { + switch startData.traffic { case .All: [ IpAddrMask(address: IPv4Address.any, cidr: 0), @@ -159,14 +175,12 @@ final class TunnelConfiguration: Codable { ] case .Predefined: self.parseAddresses(fromString: startData.allowedIps) - } + } } -} +#endif -//extension TunnelConfiguration: Equatable { -// public static func == (lhs: TunnelConfiguration, rhs: TunnelConfiguration) -> Bool { -// return lhs.name == rhs.name && -// lhs.interface == rhs.interface && -// Set(lhs.peers) == Set(rhs.peers) -// } -//} + /// Client connection expects one peer, so check for that. + func isValidForClientConnection() -> Bool { + return peers.count == 1 + } +} diff --git a/client/ios/boringtun b/client/ios/boringtun index b990805..b7c2922 160000 --- a/client/ios/boringtun +++ b/client/ios/boringtun @@ -1 +1 @@ -Subproject commit b990805fc1637eeaa401bc156adf0dd28b2e50b8 +Subproject commit b7c29222f9881165e514088cc8f6c6463e0aa452 diff --git a/client/pubspec.lock b/client/pubspec.lock index dfff894..11d313c 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" boolean_selector: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: built_value - sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" url: "https://pub.dev" source: hosted - version: "8.12.4" + version: "8.12.6" characters: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: "direct main" description: name: cookie_jar - sha256: a6ac027d3ed6ed756bfce8f3ff60cb479e266f3b0fdabd6242b804b6765e52de + sha256: "963da02c1ef64cb5ac20de948c9e5940aa351f1e34a12b1d327c83d85b7e8fff" url: "https://pub.dev" source: hosted - version: "4.0.8" + version: "4.0.9" coverage: dependency: transitive description: @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" custom_lint: dependency: "direct dev" description: @@ -397,26 +397,26 @@ packages: dependency: "direct main" description: name: dio - sha256: b9d46faecab38fc8cc286f80bc4d61a3bb5d4ac49e51ed877b4d6706efe57b25 + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c url: "https://pub.dev" source: hosted - version: "5.9.1" + version: "5.9.2" dio_cookie_manager: dependency: "direct main" description: name: dio_cookie_manager - sha256: d39c16abcc711c871b7b29bd51c6b5f3059ef39503916c6a9df7e22c4fc595e0 + sha256: "0db1a7b997a0455e488ac35744c68eed3f2a4280d3ab531835a65641b0a08744" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.4.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" drift: dependency: "direct main" description: @@ -562,10 +562,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: ee8068e0e1cd16c4a82714119918efdeed33b3ba7772c54b5d094ab53f9b7fd1 + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" url: "https://pub.dev" source: hosted - version: "2.0.33" + version: "2.0.34" flutter_riverpod: dependency: "direct main" description: @@ -634,10 +634,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -708,18 +708,18 @@ packages: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" hooks: dependency: transitive description: name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" hooks_riverpod: dependency: "direct main" description: @@ -732,10 +732,10 @@ packages: dependency: transitive description: name: hotreloader - sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b + sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" html: dependency: transitive description: @@ -964,10 +964,10 @@ packages: dependency: transitive description: name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" url: "https://pub.dev" source: hosted - version: "0.17.4" + version: "0.17.6" node_preamble: dependency: transitive description: @@ -1036,10 +1036,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba" url: "https://pub.dev" source: hosted - version: "2.2.22" + version: "2.2.23" path_provider_foundation: dependency: transitive description: @@ -1192,6 +1192,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" riverpod: dependency: transitive description: @@ -1260,18 +1268,18 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.4.20" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: @@ -1292,10 +1300,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -1569,10 +1577,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" url: "https://pub.dev" source: hosted - version: "6.3.28" + version: "6.3.29" url_launcher_ios: dependency: transitive description: @@ -1609,10 +1617,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -1633,10 +1641,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: @@ -1649,10 +1657,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" vector_math: dependency: transitive description: @@ -1665,10 +1673,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.2.0" watcher: dependency: transitive description: