mirror of
https://github.com/jkcoxson/LocalDevVPN.git
synced 2026-03-02 06:26:16 +01:00
Fix Localizable, Update TunnelProv, Add fix for Duplicated VPNs
This commit is contained in:
Binary file not shown.
@@ -41,7 +41,8 @@ class TunnelManager: ObservableObject {
|
|||||||
|
|
||||||
static var shared = TunnelManager()
|
static var shared = TunnelManager()
|
||||||
|
|
||||||
private var vpnManager: NETunnelProviderManager?
|
@Published var waitingOnSettings: Bool = false
|
||||||
|
@Published var vpnManager: NETunnelProviderManager?
|
||||||
private var vpnObserver: NSObjectProtocol?
|
private var vpnObserver: NSObjectProtocol?
|
||||||
|
|
||||||
private var tunnelDeviceIp: String {
|
private var tunnelDeviceIp: String {
|
||||||
@@ -56,6 +57,10 @@ class TunnelManager: ObservableObject {
|
|||||||
UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0"
|
UserDefaults.standard.string(forKey: "TunnelSubnetMask") ?? "255.255.255.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var tunnelBundleId: String {
|
||||||
|
Bundle.main.bundleIdentifier!.appending(".TunnelProv")
|
||||||
|
}
|
||||||
|
|
||||||
enum TunnelStatus {
|
enum TunnelStatus {
|
||||||
case disconnected
|
case disconnected
|
||||||
case connecting
|
case connecting
|
||||||
@@ -83,22 +88,21 @@ class TunnelManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var localizedTitle: String {
|
var localizedTitle: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .disconnected:
|
case .disconnected:
|
||||||
return NSLocalizedString("disconnected", comment: "")
|
return "disconnected"
|
||||||
case .connecting:
|
case .connecting:
|
||||||
return NSLocalizedString("connecting", comment: "")
|
return "connecting"
|
||||||
case .connected:
|
case .connected:
|
||||||
return NSLocalizedString("connected", comment: "")
|
return "connected"
|
||||||
case .disconnecting:
|
case .disconnecting:
|
||||||
return NSLocalizedString("disconnecting", comment: "")
|
return "disconnecting"
|
||||||
case .error:
|
case .error:
|
||||||
return NSLocalizedString("error", comment: "")
|
return "error"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
loadTunnelPreferences()
|
loadTunnelPreferences()
|
||||||
@@ -117,21 +121,61 @@ class TunnelManager: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
self.waitingOnSettings = true
|
||||||
|
}
|
||||||
|
|
||||||
self.hasLocalDeviceSupport = true
|
self.hasLocalDeviceSupport = true
|
||||||
|
|
||||||
if let managers = managers, !managers.isEmpty {
|
if let managers = managers, !managers.isEmpty {
|
||||||
// Look specifically for StosVPN manager
|
var stosManagers = [NETunnelProviderManager]()
|
||||||
|
|
||||||
for manager in managers {
|
for manager in managers {
|
||||||
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
|
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
|
||||||
proto.providerBundleIdentifier == Bundle.main.tunnelBundleID {
|
proto.providerBundleIdentifier == self.tunnelBundleId {
|
||||||
self.vpnManager = manager
|
stosManagers.append(manager)
|
||||||
self.updateTunnelStatus(from: manager.connection.status)
|
|
||||||
VPNLogger.shared.log("Loaded existing StosVPN tunnel configuration")
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !stosManagers.isEmpty {
|
||||||
|
if stosManagers.count > 1 {
|
||||||
|
self.cleanupDuplicateManagers(stosManagers)
|
||||||
|
} else {
|
||||||
|
self.vpnManager = stosManagers.first
|
||||||
|
self.updateTunnelStatus(from: stosManagers.first!.connection.status)
|
||||||
|
VPNLogger.shared.log("Loaded existing StosVPN tunnel configuration")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
VPNLogger.shared.log("No StosVPN tunnel configuration found")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
VPNLogger.shared.log("No existing tunnel configuration found")
|
VPNLogger.shared.log("No existing tunnel configurations found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanupDuplicateManagers(_ managers: [NETunnelProviderManager]) {
|
||||||
|
VPNLogger.shared.log("Found \(managers.count) StosVPN configurations. Cleaning up duplicates...")
|
||||||
|
|
||||||
|
let activeManager = managers.first {
|
||||||
|
$0.connection.status == .connected || $0.connection.status == .connecting
|
||||||
|
}
|
||||||
|
|
||||||
|
let managerToKeep = activeManager ?? managers.first!
|
||||||
|
self.vpnManager = managerToKeep
|
||||||
|
self.updateTunnelStatus(from: managerToKeep.connection.status)
|
||||||
|
|
||||||
|
var removedCount = 0
|
||||||
|
for manager in managers {
|
||||||
|
if manager != managerToKeep {
|
||||||
|
manager.removeFromPreferences { error in
|
||||||
|
if let error = error {
|
||||||
|
VPNLogger.shared.log("Error removing duplicate VPN: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
removedCount += 1
|
||||||
|
VPNLogger.shared.log("Successfully removed duplicate VPN configuration")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,11 +192,7 @@ class TunnelManager: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only update status if it's our VPN connection
|
self.handleVPNStatusChange(notification: notification)
|
||||||
if let manager = self.vpnManager,
|
|
||||||
connection == manager.connection {
|
|
||||||
self.updateTunnelStatus(from: connection.status)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,35 +218,61 @@ class TunnelManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func createStosVPNConfiguration(completion: @escaping (NETunnelProviderManager?) -> Void) {
|
private func createStosVPNConfiguration(completion: @escaping (NETunnelProviderManager?) -> Void) {
|
||||||
let manager = NETunnelProviderManager()
|
NETunnelProviderManager.loadAllFromPreferences { [weak self] (managers, error) in
|
||||||
manager.localizedDescription = "StosVPN"
|
guard let self = self else { return }
|
||||||
|
|
||||||
let proto = NETunnelProviderProtocol()
|
if let error = error {
|
||||||
proto.providerBundleIdentifier = Bundle.main.tunnelBundleID
|
VPNLogger.shared.log("Error checking existing VPN configurations: \(error.localizedDescription)")
|
||||||
proto.serverAddress = NSLocalizedString("server_address_name", comment: "")
|
completion(nil)
|
||||||
manager.protocolConfiguration = proto
|
return
|
||||||
|
}
|
||||||
let onDemandRule = NEOnDemandRuleEvaluateConnection()
|
|
||||||
onDemandRule.interfaceTypeMatch = .any
|
if let managers = managers {
|
||||||
onDemandRule.connectionRules = [NEEvaluateConnectionRule(
|
let stosManagers = managers.filter { manager in
|
||||||
matchDomains: ["10.7.0.0", "10.7.0.1"],
|
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
andAction: .connectIfNeeded
|
return proto.providerBundleIdentifier == self.tunnelBundleId
|
||||||
)]
|
}
|
||||||
|
return false
|
||||||
manager.onDemandRules = [onDemandRule]
|
|
||||||
manager.isOnDemandEnabled = true
|
|
||||||
manager.isEnabled = true
|
|
||||||
|
|
||||||
manager.saveToPreferences { error in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if let error = error {
|
|
||||||
VPNLogger.shared.log("Error creating StosVPN configuration: \(error.localizedDescription)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VPNLogger.shared.log("StosVPN configuration created successfully")
|
if !stosManagers.isEmpty {
|
||||||
completion(manager)
|
let manager = stosManagers.first!
|
||||||
|
VPNLogger.shared.log("Found existing StosVPN configuration, using it instead of creating new one")
|
||||||
|
completion(manager)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = NETunnelProviderManager()
|
||||||
|
manager.localizedDescription = "StosVPN"
|
||||||
|
|
||||||
|
let proto = NETunnelProviderProtocol()
|
||||||
|
proto.providerBundleIdentifier = self.tunnelBundleId
|
||||||
|
proto.serverAddress = "StosVPN's Local Network Tunnel"
|
||||||
|
manager.protocolConfiguration = proto
|
||||||
|
|
||||||
|
let onDemandRule = NEOnDemandRuleEvaluateConnection()
|
||||||
|
onDemandRule.interfaceTypeMatch = .any
|
||||||
|
onDemandRule.connectionRules = [NEEvaluateConnectionRule(
|
||||||
|
matchDomains: ["10.7.0.0", "10.7.0.1"],
|
||||||
|
andAction: .connectIfNeeded
|
||||||
|
)]
|
||||||
|
|
||||||
|
manager.onDemandRules = [onDemandRule]
|
||||||
|
manager.isOnDemandEnabled = true
|
||||||
|
manager.isEnabled = true
|
||||||
|
|
||||||
|
manager.saveToPreferences { error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let error = error {
|
||||||
|
VPNLogger.shared.log("Error creating StosVPN configuration: \(error.localizedDescription)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
VPNLogger.shared.log("StosVPN configuration created successfully")
|
||||||
|
completion(manager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,6 +299,7 @@ class TunnelManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Public Methods
|
// MARK: - Public Methods
|
||||||
|
|
||||||
func toggleVPNConnection() {
|
func toggleVPNConnection() {
|
||||||
@@ -248,16 +315,14 @@ class TunnelManager: ObservableObject {
|
|||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
if let activeManager = activeManager,
|
if let activeManager = activeManager,
|
||||||
(activeManager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier != Bundle.main.tunnelBundleID {
|
(activeManager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier != self.tunnelBundleId {
|
||||||
VPNLogger.shared.log("Disconnecting existing VPN connection before starting StosVPN")
|
VPNLogger.shared.log("Disconnecting existing VPN connection before starting StosVPN")
|
||||||
|
|
||||||
// Set a flag to start StosVPN after disconnection
|
|
||||||
UserDefaults.standard.set(true, forKey: "ShouldStartStosVPNAfterDisconnect")
|
UserDefaults.standard.set(true, forKey: "ShouldStartStosVPNAfterDisconnect")
|
||||||
activeManager.connection.stopVPNTunnel()
|
activeManager.connection.stopVPNTunnel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self.initializeAndStartStosVPN()
|
self.initializeAndStartStosVPN()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -266,11 +331,44 @@ class TunnelManager: ObservableObject {
|
|||||||
if let manager = vpnManager {
|
if let manager = vpnManager {
|
||||||
startExistingVPN(manager: manager)
|
startExistingVPN(manager: manager)
|
||||||
} else {
|
} else {
|
||||||
createStosVPNConfiguration { [weak self] manager in
|
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
|
||||||
guard let self = self, let manager = manager else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
self.vpnManager = manager
|
if let error = error {
|
||||||
self.startExistingVPN(manager: manager)
|
VPNLogger.shared.log("Error reloading VPN configurations: \(error.localizedDescription)")
|
||||||
|
self.createStosVPNConfiguration { manager in
|
||||||
|
guard let manager = manager else { return }
|
||||||
|
self.vpnManager = manager
|
||||||
|
self.startExistingVPN(manager: manager)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let managers = managers {
|
||||||
|
let stosManagers = managers.filter { manager in
|
||||||
|
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
|
return proto.providerBundleIdentifier == self.tunnelBundleId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stosManagers.isEmpty {
|
||||||
|
self.vpnManager = stosManagers.first
|
||||||
|
|
||||||
|
if stosManagers.count > 1 {
|
||||||
|
self.cleanupDuplicateManagers(stosManagers)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.startExistingVPN(manager: stosManagers.first!)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.createStosVPNConfiguration { manager in
|
||||||
|
guard let manager = manager else { return }
|
||||||
|
self.vpnManager = manager
|
||||||
|
self.startExistingVPN(manager: manager)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,6 +438,62 @@ class TunnelManager: ObservableObject {
|
|||||||
self?.initializeAndStartStosVPN()
|
self?.initializeAndStartStosVPN()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a different StosVPN manager (perhaps a duplicate)
|
||||||
|
// This helps discover duplicates created by other means
|
||||||
|
NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, error in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if let managers = managers, !managers.isEmpty {
|
||||||
|
let stosManagers = managers.filter { manager in
|
||||||
|
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol {
|
||||||
|
return proto.providerBundleIdentifier == self.tunnelBundleId
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if stosManagers.count > 1 {
|
||||||
|
self.cleanupDuplicateManagers(stosManagers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Cleanup Utilities
|
||||||
|
|
||||||
|
func cleanupAllVPNConfigurations() {
|
||||||
|
NETunnelProviderManager.loadAllFromPreferences { managers, error in
|
||||||
|
if let error = error {
|
||||||
|
VPNLogger.shared.log("Error loading VPN configurations for cleanup: \(error.localizedDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let managers = managers else { return }
|
||||||
|
|
||||||
|
for manager in managers {
|
||||||
|
if let proto = manager.protocolConfiguration as? NETunnelProviderProtocol,
|
||||||
|
proto.providerBundleIdentifier == self.tunnelBundleId {
|
||||||
|
|
||||||
|
// If connected, disconnect first
|
||||||
|
if manager.connection.status == .connected ||
|
||||||
|
manager.connection.status == .connecting {
|
||||||
|
manager.connection.stopVPNTunnel()
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.removeFromPreferences { error in
|
||||||
|
if let error = error {
|
||||||
|
VPNLogger.shared.log("Error removing VPN configuration: \(error.localizedDescription)")
|
||||||
|
} else {
|
||||||
|
VPNLogger.shared.log("Successfully removed VPN configuration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
self.vpnManager = nil
|
||||||
|
self.tunnelStatus = .disconnected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -390,8 +544,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onChange(of: tunnelManager.waitingOnSettings) { finished in
|
||||||
if tunnelManager.tunnelStatus != .connected && autoConnect {
|
if tunnelManager.tunnelStatus != .connected && autoConnect && finished {
|
||||||
tunnelManager.startVPN()
|
tunnelManager.startVPN()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -541,24 +695,24 @@ struct ConnectionStatsView: View {
|
|||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
HStack(spacing: 30) {
|
HStack(spacing: 30) {
|
||||||
StatItemView(
|
StatItemView(
|
||||||
title: NSLocalizedString("time_connected", comment: ""),
|
title: "time_connected",
|
||||||
value: formattedTime,
|
value: formattedTime,
|
||||||
icon: "clock.fill"
|
icon: "clock.fill"
|
||||||
)
|
)
|
||||||
StatItemView(
|
StatItemView(
|
||||||
title: NSLocalizedString("status", comment: ""),
|
title: "status",
|
||||||
value: NSLocalizedString("active", comment: ""),
|
value: "active",
|
||||||
icon: "checkmark.circle.fill"
|
icon: "checkmark.circle.fill"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
HStack(spacing: 30) {
|
HStack(spacing: 30) {
|
||||||
StatItemView(
|
StatItemView(
|
||||||
title: NSLocalizedString("network_interface", comment: ""),
|
title: "network_interface",
|
||||||
value: NSLocalizedString("local", comment: ""),
|
value: "local",
|
||||||
icon: "network"
|
icon: "network"
|
||||||
)
|
)
|
||||||
StatItemView(
|
StatItemView(
|
||||||
title: NSLocalizedString("assigned_ip", comment: ""),
|
title: "assigned_ip",
|
||||||
value: "10.7.0.1",
|
value: "10.7.0.1",
|
||||||
icon: "number"
|
icon: "number"
|
||||||
)
|
)
|
||||||
@@ -589,7 +743,7 @@ struct ConnectionStatsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct StatItemView: View {
|
struct StatItemView: View {
|
||||||
let title: String
|
let title: LocalizedStringKey
|
||||||
let value: String
|
let value: String
|
||||||
let icon: String
|
let icon: String
|
||||||
|
|
||||||
@@ -620,7 +774,11 @@ struct SettingsView: View {
|
|||||||
@AppStorage("TunnelFakeIP") private var fakeIP = "10.7.0.1"
|
@AppStorage("TunnelFakeIP") private var fakeIP = "10.7.0.1"
|
||||||
@AppStorage("TunnelSubnetMask") private var subnetMask = "255.255.255.0"
|
@AppStorage("TunnelSubnetMask") private var subnetMask = "255.255.255.0"
|
||||||
@AppStorage("autoConnect") private var autoConnect = false
|
@AppStorage("autoConnect") private var autoConnect = false
|
||||||
|
@AppStorage("shownTunnelAlert") private var shownTunnelAlert = false
|
||||||
|
@StateObject private var tunnelManager = TunnelManager.shared
|
||||||
|
|
||||||
|
@State private var showNetworkWarning = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NBNavigationStack {
|
NBNavigationStack {
|
||||||
List {
|
List {
|
||||||
@@ -632,29 +790,10 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("network_configuration")) {
|
Section(header: Text("network_configuration")) {
|
||||||
HStack {
|
Group {
|
||||||
Text("device_ip")
|
networkConfigRow(label: "tunnel_ip", text: $deviceIP)
|
||||||
Spacer()
|
networkConfigRow(label: "device_ip", text: $fakeIP)
|
||||||
TextField("device_ip", text: $deviceIP)
|
networkConfigRow(label: "subnet_mask", text: $subnetMask)
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.keyboardType(.numbersAndPunctuation)
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
Text("tunnel_ip")
|
|
||||||
Spacer()
|
|
||||||
TextField("tunnel_ip", text: $fakeIP)
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.keyboardType(.numbersAndPunctuation)
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
Text("subnet_mask")
|
|
||||||
Spacer()
|
|
||||||
TextField("subnet_mask", text: $subnetMask)
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.keyboardType(.numbersAndPunctuation)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +829,19 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(isPresented: $showNetworkWarning) {
|
||||||
|
Alert(
|
||||||
|
title: Text("warning_alert"),
|
||||||
|
message: Text("warning_message"),
|
||||||
|
dismissButton: .cancel(Text("understand_button")) {
|
||||||
|
shownTunnelAlert = true
|
||||||
|
|
||||||
|
deviceIP = "10.7.0.0"
|
||||||
|
fakeIP = "10.7.0.1"
|
||||||
|
subnetMask = "255.255.255.0"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.navigationTitle(Text("settings"))
|
.navigationTitle(Text("settings"))
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -704,7 +856,29 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
private func dismiss() {
|
private func dismiss() {
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func networkConfigRow(label: LocalizedStringKey, text: Binding<String>) -> some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
TextField(label, text: text)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.keyboardType(.numbersAndPunctuation)
|
||||||
|
.onChange(of: text.wrappedValue) { newValue in
|
||||||
|
if !shownTunnelAlert {
|
||||||
|
showNetworkWarning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelManager.vpnManager?.saveToPreferences { error in
|
||||||
|
if let error = error {
|
||||||
|
VPNLogger.shared.log(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -939,10 +1113,10 @@ struct SetupView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct SetupPage {
|
struct SetupPage {
|
||||||
let title: String
|
let title: LocalizedStringKey
|
||||||
let description: String
|
let description: LocalizedStringKey
|
||||||
let imageName: String
|
let imageName: String
|
||||||
let details: String
|
let details: LocalizedStringKey
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SetupPageView: View {
|
struct SetupPageView: View {
|
||||||
@@ -1000,4 +1174,4 @@ class LanguageManager: ObservableObject {
|
|||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentView()
|
ContentView()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"italian" = "Italian";
|
"italian" = "Italian";
|
||||||
"settings" = "Settings";
|
"settings" = "Settings";
|
||||||
"done" = "Done";
|
"done" = "Done";
|
||||||
|
"warning_alert" = "Warning";
|
||||||
|
"warning_message" = "Changing tunnel IP settings can disrupt your network connection. Proceed only if you are sure of what you are doing.";
|
||||||
|
"understand_button" = "I Understand";
|
||||||
|
|
||||||
"data_collection_policy_title" = "Data Collection Policy";
|
"data_collection_policy_title" = "Data Collection Policy";
|
||||||
"no_data_collection" = "No Data Collection";
|
"no_data_collection" = "No Data Collection";
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"italian" = "Italiano";
|
"italian" = "Italiano";
|
||||||
"settings" = "Configuración";
|
"settings" = "Configuración";
|
||||||
"done" = "Hecho";
|
"done" = "Hecho";
|
||||||
|
"warning_alert" = "Advertencia";
|
||||||
|
"warning_message" = "Cambiar la configuración de la IP del túnel puede interrumpir tu conexión de red. Procede solo si estás seguro de lo que haces.";
|
||||||
|
"understand_button" = "Entiendo";
|
||||||
|
|
||||||
"data_collection_policy_title" = "Política de Recopilación de Datos";
|
"data_collection_policy_title" = "Política de Recopilación de Datos";
|
||||||
"no_data_collection" = "No Recopilación de Datos";
|
"no_data_collection" = "No Recopilación de Datos";
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"italian" = "Italiano";
|
"italian" = "Italiano";
|
||||||
"settings" = "Impostazioni";
|
"settings" = "Impostazioni";
|
||||||
"done" = "Fine";
|
"done" = "Fine";
|
||||||
|
"warning_alert" = "Avviso";
|
||||||
|
"warning_message" = "La modifica delle impostazioni IP del tunnel può interrompere la connessione di rete. Procedi solo se sei sicuro di quello che stai facendo.";
|
||||||
|
"understand_button" = "Ho capito";
|
||||||
|
|
||||||
"data_collection_policy_title" = "Politica di Raccolta Dati";
|
"data_collection_policy_title" = "Politica di Raccolta Dati";
|
||||||
"no_data_collection" = "Nessuna Raccolta Dati";
|
"no_data_collection" = "Nessuna Raccolta Dati";
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
var tunnelFakeIp: String = "10.7.0.1"
|
var tunnelFakeIp: String = "10.7.0.1"
|
||||||
var tunnelSubnetMask: String = "255.255.255.0"
|
var tunnelSubnetMask: String = "255.255.255.0"
|
||||||
|
|
||||||
|
private var deviceIpValue: UInt32 = 0
|
||||||
|
private var fakeIpValue: UInt32 = 0
|
||||||
|
|
||||||
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
||||||
if let deviceIp = options?["TunnelDeviceIP"] as? String {
|
if let deviceIp = options?["TunnelDeviceIP"] as? String {
|
||||||
tunnelDeviceIp = deviceIp
|
tunnelDeviceIp = deviceIp
|
||||||
@@ -20,11 +23,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
tunnelFakeIp = fakeIp
|
tunnelFakeIp = fakeIp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deviceIpValue = ipToUInt32(tunnelDeviceIp)
|
||||||
|
fakeIpValue = ipToUInt32(tunnelFakeIp)
|
||||||
|
|
||||||
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIp)
|
let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: tunnelDeviceIp)
|
||||||
let ipv4 = NEIPv4Settings(addresses: [tunnelDeviceIp], subnetMasks: [tunnelSubnetMask])
|
let ipv4 = NEIPv4Settings(addresses: [tunnelDeviceIp], subnetMasks: [tunnelSubnetMask])
|
||||||
ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)]
|
ipv4.includedRoutes = [NEIPv4Route(destinationAddress: tunnelDeviceIp, subnetMask: tunnelSubnetMask)]
|
||||||
ipv4.excludedRoutes = [.default()]
|
ipv4.excludedRoutes = [.default()]
|
||||||
settings.ipv4Settings = ipv4
|
settings.ipv4Settings = ipv4
|
||||||
|
|
||||||
setTunnelNetworkSettings(settings) { error in
|
setTunnelNetworkSettings(settings) { error in
|
||||||
if error == nil {
|
if error == nil {
|
||||||
self.readPackets()
|
self.readPackets()
|
||||||
@@ -34,120 +41,69 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||||
// Add code here to start the process of stopping the tunnel.
|
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) {
|
||||||
// Add code here to handle the message.
|
completionHandler?(messageData)
|
||||||
if let handler = completionHandler {
|
|
||||||
handler(messageData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func sleep(completionHandler: @escaping () -> Void) {
|
override func sleep(completionHandler: @escaping () -> Void) {
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func wake() {
|
override func wake() {}
|
||||||
// Add code here to wake up.
|
|
||||||
}
|
|
||||||
|
|
||||||
private func readPackets() {
|
private func readPackets() {
|
||||||
packetFlow.readPackets { packets, protocols in
|
packetFlow.readPackets { packets, protocols in
|
||||||
var output: [Data] = []
|
var output = [Data](repeating: Data(), count: packets.count)
|
||||||
|
|
||||||
for (i, packet) in packets.enumerated() {
|
for (i, packet) in packets.enumerated() {
|
||||||
var modifiedPacket = packet
|
guard protocols[i].int32Value == AF_INET, packet.count >= 20 else {
|
||||||
if protocols[i].int32Value == AF_INET {
|
output[i] = packet
|
||||||
modifiedPacket = self.packetReplaceIp(packet, self.tunnelDeviceIp, self.tunnelFakeIp, self.tunnelFakeIp, self.tunnelDeviceIp)
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if modifiedPacket.count >= 20 {
|
output[i] = self.processPacket(packet)
|
||||||
var mutableBytes = [UInt8](modifiedPacket)
|
|
||||||
|
|
||||||
// Swap bytes
|
|
||||||
(mutableBytes[12], mutableBytes[16]) = (mutableBytes[16], mutableBytes[12])
|
|
||||||
(mutableBytes[13], mutableBytes[17]) = (mutableBytes[17], mutableBytes[13])
|
|
||||||
(mutableBytes[14], mutableBytes[18]) = (mutableBytes[18], mutableBytes[14])
|
|
||||||
(mutableBytes[15], mutableBytes[19]) = (mutableBytes[19], mutableBytes[15])
|
|
||||||
|
|
||||||
modifiedPacket = Data(mutableBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
output.append(modifiedPacket)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.packetFlow.writePackets(output, withProtocols: protocols)
|
self.packetFlow.writePackets(output, withProtocols: protocols)
|
||||||
self.readPackets()
|
self.readPackets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func packetReplaceIp(_ data: Data, _ sourceSearch: String, _ sourceReplace: String, _ destSearch: String, _ destReplace: String) -> Data {
|
private func processPacket(_ packet: Data) -> Data {
|
||||||
// Check if packet is too small for IPv4 header
|
var bytes = [UInt8](packet)
|
||||||
if data.count < 20 {
|
|
||||||
return data
|
let srcIP = UInt32(bigEndian: bytes.withUnsafeBytes { $0.load(fromByteOffset: 12, as: UInt32.self) })
|
||||||
|
let dstIP = UInt32(bigEndian: bytes.withUnsafeBytes { $0.load(fromByteOffset: 16, as: UInt32.self) })
|
||||||
|
|
||||||
|
if srcIP == deviceIpValue {
|
||||||
|
let replacement = fakeIpValue.bigEndian
|
||||||
|
withUnsafeBytes(of: replacement) { bytes.replaceSubrange(12..<16, with: $0) }
|
||||||
|
}
|
||||||
|
if dstIP == fakeIpValue {
|
||||||
|
let replacement = deviceIpValue.bigEndian
|
||||||
|
withUnsafeBytes(of: replacement) { bytes.replaceSubrange(16..<20, with: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert IP strings to Data with network byte order (big-endian)
|
bytes.swapAt(12, 16)
|
||||||
func ipToUInt32(_ ipString: String) -> UInt32 {
|
bytes.swapAt(13, 17)
|
||||||
let components = ipString.split(separator: ".")
|
bytes.swapAt(14, 18)
|
||||||
var result: UInt32 = 0
|
bytes.swapAt(15, 19)
|
||||||
|
|
||||||
if components.count == 4,
|
|
||||||
let byte1 = UInt32(components[0]),
|
|
||||||
let byte2 = UInt32(components[1]),
|
|
||||||
let byte3 = UInt32(components[2]),
|
|
||||||
let byte4 = UInt32(components[3]) {
|
|
||||||
result = (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert IP strings to UInt32
|
return Data(bytes)
|
||||||
let sourceSearchIP = ipToUInt32(sourceSearch)
|
|
||||||
let sourceReplaceIP = ipToUInt32(sourceReplace)
|
|
||||||
let destSearchIP = ipToUInt32(destSearch)
|
|
||||||
let destReplaceIP = ipToUInt32(destReplace)
|
|
||||||
|
|
||||||
// Extract source and destination IPs from packet
|
|
||||||
var sourcePacketIP: UInt32 = 0
|
|
||||||
var destPacketIP: UInt32 = 0
|
|
||||||
|
|
||||||
(data as NSData).getBytes(&sourcePacketIP, range: NSRange(location: 12, length: 4))
|
|
||||||
(data as NSData).getBytes(&destPacketIP, range: NSRange(location: 16, length: 4))
|
|
||||||
|
|
||||||
if sourceSearchIP != sourcePacketIP && destSearchIP != destPacketIP {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
let mutableData = NSMutableData(data: data)
|
|
||||||
|
|
||||||
if sourceSearchIP == sourcePacketIP {
|
|
||||||
var sourceIP = sourceReplaceIP
|
|
||||||
mutableData.replaceBytes(in: NSRange(location: 12, length: 4), withBytes: &sourceIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if destSearchIP == destPacketIP {
|
|
||||||
var destIP = destReplaceIP
|
|
||||||
mutableData.replaceBytes(in: NSRange(location: 16, length: 4), withBytes: &destIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mutableData as Data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to convert IP string to Data
|
private func ipToUInt32(_ ipString: String) -> UInt32 {
|
||||||
private func ipToData(_ ip: String) -> Data {
|
let components = ipString.split(separator: ".")
|
||||||
let components = ip.split(separator: ".")
|
guard components.count == 4,
|
||||||
var data = Data(capacity: 4)
|
let b1 = UInt32(components[0]),
|
||||||
|
let b2 = UInt32(components[1]),
|
||||||
for component in components {
|
let b3 = UInt32(components[2]),
|
||||||
if let byte = UInt8(component) {
|
let b4 = UInt32(components[3]) else {
|
||||||
data.append(byte)
|
return 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user