diff --git a/LocalDevVPN.xcodeproj/project.xcworkspace/xcuserdata/lolo.xcuserdatad/UserInterfaceState.xcuserstate b/LocalDevVPN.xcodeproj/project.xcworkspace/xcuserdata/lolo.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..9c36bec Binary files /dev/null and b/LocalDevVPN.xcodeproj/project.xcworkspace/xcuserdata/lolo.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme b/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme index 8d51541..7c566e4 100644 --- a/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme +++ b/LocalDevVPN.xcodeproj/xcshareddata/xcschemes/TunnelProv.xcscheme @@ -74,6 +74,7 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES" + askForAppToLaunch = "Yes" launchAutomaticallySubstyle = "2"> diff --git a/LocalDevVPN/ContentView.swift b/LocalDevVPN/ContentView.swift index 34a41ba..bea6d5c 100644 --- a/LocalDevVPN/ContentView.swift +++ b/LocalDevVPN/ContentView.swift @@ -8,6 +8,7 @@ import Foundation import NetworkExtension import SwiftUI +import Darwin import NavigationBackport @@ -16,6 +17,30 @@ extension Bundle { var tunnelBundleID: String { bundleIdentifier!.appending(".TunnelProv") } } +private func getCurrentWiFiIP() -> String? { + var address: String? + var ifaddr: UnsafeMutablePointer? + guard getifaddrs(&ifaddr) == 0 else { return nil } + guard let firstAddr = ifaddr else { return nil } + + for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) { + let interface = ifptr.pointee + let addrFamily = interface.ifa_addr.pointee.sa_family + + if addrFamily == UInt8(AF_INET) { + let name = String(cString: interface.ifa_name) + if name == "en0" { + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), + &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) + address = String(cString: hostname) + } + } + } + freeifaddrs(ifaddr) + return address +} + // MARK: - Logging Utility class VPNLogger: ObservableObject { @@ -66,7 +91,11 @@ class TunnelManager: ObservableObject { private var tunnelSubnetMask: String { UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0" } - + + private var useWiFiSubnet: Bool { + UserDefaults.standard.bool(forKey: "useWiFiSubnet") + } + private var tunnelBundleId: String { Bundle.main.bundleIdentifier!.appending(".TunnelProv") } @@ -503,6 +532,7 @@ class TunnelManager: ObservableObject { "TunnelDeviceIP": self.tunnelDeviceIp as NSObject, "TunnelFakeIP": self.tunnelFakeIp as NSObject, "TunnelSubnetMask": self.tunnelSubnetMask as NSObject, + "UseWiFiSubnet": self.useWiFiSubnet as NSObject, ] do { @@ -783,6 +813,14 @@ extension View { struct StatusOverviewCard: View { @StateObject private var tunnelManager = TunnelManager.shared @AppStorage("TunnelDeviceIP") private var deviceIP = "10.7.0.0" + @AppStorage("useWiFiSubnet") private var useWiFiSubnet = false + + private var currentIP: String { + if useWiFiSubnet, let wifiIP = getCurrentWiFiIP() { + return wifiIP + } + return deviceIP + } var body: some View { DashboardCard { @@ -827,7 +865,7 @@ struct StatusOverviewCard: View { private var statusTip: String { switch tunnelManager.tunnelStatus { case .connected: - return String(format: NSLocalizedString("connected_to_ip", comment: ""), deviceIP) + return String(format: NSLocalizedString("connected_to_ip", comment: ""), currentIP) case .connecting: return NSLocalizedString("ios_might_ask_you_to_allow_the_vpn", comment: "") case .disconnecting: @@ -1087,6 +1125,7 @@ struct SettingsView: View { @AppStorage("TunnelDeviceIP") private var deviceIP = "10.7.0.0" @AppStorage("TunnelFakeIP") private var fakeIP = "10.7.0.1" @AppStorage("TunnelSubnetMask") private var subnetMask = "255.255.255.0" + @AppStorage("useWiFiSubnet") private var useWiFiSubnet = false @AppStorage("autoConnect") private var autoConnect = false @AppStorage("shownTunnelAlert") private var shownTunnelAlert = false @StateObject private var tunnelManager = TunnelManager.shared @@ -1106,10 +1145,14 @@ struct SettingsView: View { } Section(header: Text("network_configuration")) { - Group { - networkConfigRow(label: "tunnel_ip", text: $deviceIP) - networkConfigRow(label: "device_ip", text: $fakeIP) - networkConfigRow(label: "subnet_mask", text: $subnetMask) + Toggle("match_wifi_subnet", isOn: $useWiFiSubnet) + + if !useWiFiSubnet { + Group { + networkConfigRow(label: "tunnel_ip", text: $deviceIP) + networkConfigRow(label: "device_ip", text: $fakeIP) + networkConfigRow(label: "subnet_mask", text: $subnetMask) + } } } diff --git a/LocalDevVPN/Localization/en.lproj/Localizable.strings b/LocalDevVPN/Localization/en.lproj/Localizable.strings index b08c6c0..c85e0a3 100644 --- a/LocalDevVPN/Localization/en.lproj/Localizable.strings +++ b/LocalDevVPN/Localization/en.lproj/Localizable.strings @@ -57,6 +57,7 @@ "auto_connect_on_launch" = "Auto Connect on Launch"; "connection_logs" = "Connection Logs"; "network_configuration" = "Network Configuration"; +"match_wifi_subnet" = "Match WiFi Subnet"; "device_ip" = "Device IP"; "tunnel_ip" = "Tunnel IP"; "subnet_mask" = "Subnet Mask"; diff --git a/TunnelProv/PacketTunnelProvider.swift b/TunnelProv/PacketTunnelProvider.swift index 8d7c352..a0ef6ba 100644 --- a/TunnelProv/PacketTunnelProvider.swift +++ b/TunnelProv/PacketTunnelProvider.swift @@ -6,6 +6,7 @@ // import NetworkExtension +import Darwin class PacketTunnelProvider: NEPacketTunnelProvider { var tunnelDeviceIp: String = "10.7.0.0" @@ -16,11 +17,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private var fakeIpValue: UInt32 = 0 override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - if let deviceIp = options?["TunnelDeviceIP"] as? String { - tunnelDeviceIp = deviceIp - } - if let fakeIp = options?["TunnelFakeIP"] as? String { - tunnelFakeIp = fakeIp + let useWifiSubnet = options?["UseWiFiSubnet"] as? Bool ?? false + + if useWifiSubnet { + if let wifiInfo = getWiFiNetworkInfo() { + tunnelDeviceIp = wifiInfo.ipAddress + tunnelSubnetMask = wifiInfo.subnetMask + let fakeIpParts = wifiInfo.ipAddress.split(separator: ".") + if fakeIpParts.count == 4 { + tunnelFakeIp = "\(fakeIpParts[0]).\(fakeIpParts[1]).\(fakeIpParts[2]).\(Int(fakeIpParts[3])! + 1)" + } + } + } else { + if let deviceIp = options?["TunnelDeviceIP"] as? String { + tunnelDeviceIp = deviceIp + } + if let fakeIp = options?["TunnelFakeIP"] as? String { + tunnelFakeIp = fakeIp + } + if let subnetMask = options?["TunnelSubnetMask"] as? String { + tunnelSubnetMask = subnetMask + } } deviceIpValue = ipToUInt32(tunnelDeviceIp) @@ -28,8 +45,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider { let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIp) let ipv4 = NEIPv4Settings(addresses: [tunnelDeviceIp], subnetMasks: [tunnelSubnetMask]) - ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)] - ipv4.excludedRoutes = [.default()] + + let localSubnet = calculateNetworkAddress(ip: tunnelDeviceIp, mask: tunnelSubnetMask) + let localRoute = NEIPv4Route(destinationAddress: localSubnet, subnetMask: tunnelSubnetMask) + + if useWifiSubnet { + ipv4.includedRoutes = [localRoute] + ipv4.excludedRoutes = [.default()] + } else { + ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)] + ipv4.excludedRoutes = [.default()] + } + settings.ipv4Settings = ipv4 setTunnelNetworkSettings(settings) { error in @@ -69,4 +96,53 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 } + + private func getWiFiNetworkInfo() -> (ipAddress: String, subnetMask: String)? { + var address: String? + var subnetMask: String? + + var ifaddr: UnsafeMutablePointer? + guard getifaddrs(&ifaddr) == 0 else { return nil } + guard let firstAddr = ifaddr else { return nil } + + for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) { + let interface = ifptr.pointee + let addrFamily = interface.ifa_addr.pointee.sa_family + + if addrFamily == UInt8(AF_INET) { + let name = String(cString: interface.ifa_name) + if name == "en0" { + var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface.ifa_addr, socklen_t(interface.ifa_addr.pointee.sa_len), + &hostname, socklen_t(hostname.count), nil, socklen_t(0), NI_NUMERICHOST) + address = String(cString: hostname) + + var maskHostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + getnameinfo(interface.ifa_netmask, socklen_t(interface.ifa_netmask.pointee.sa_len), + &maskHostname, socklen_t(maskHostname.count), nil, socklen_t(0), NI_NUMERICHOST) + subnetMask = String(cString: maskHostname) + } + } + } + freeifaddrs(ifaddr) + + if let addr = address, let mask = subnetMask { + return (addr, mask) + } + return nil + } + + private func calculateNetworkAddress(ip: String, mask: String) -> String { + let ipParts = ip.split(separator: ".").compactMap { UInt32($0) } + let maskParts = mask.split(separator: ".").compactMap { UInt32($0) } + + guard ipParts.count == 4, maskParts.count == 4 else { return ip } + + let network = (ipParts[0] & maskParts[0]) | + (ipParts[1] & maskParts[1]) | + (ipParts[2] & maskParts[2]) | + (ipParts[3] & maskParts[3]) + + return "\(network >> 24 & 0xFF).\(network >> 16 & 0xFF).\(network >> 8 & 0xFF).\(network & 0xFF)" + } }