Skip to content

Commit 004fca8

Browse files
committed
Add option to use includeAllNetworks.
In OS-independant parts of the code I called this `strictLeakPrevention`. My main concern here is that we are just tracking the macOS property. I'm worried that if saving fails we may show the wrong value. Probably the best option would be to have our own vairable that we track before attempting changes and clear only when we are sure we know the current state again. But that can be added before shipping the end-user UI, for now at least the core functionality works. Bug: https://linear.app/soveng/issue/OBS-234/consider-setting-the-includeallnetworks-network-extension-flag
1 parent 8a07e71 commit 004fca8

File tree

8 files changed

+121
-9
lines changed

8 files changed

+121
-9
lines changed

apple/client/OsStatus.swift

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,29 @@ class OsStatus: Encodable {
1010
var internetAvailable: Bool = false
1111
var osVpnStatus: NEVPNStatus
1212
let srcVersion = sourceVersion()
13+
var strictLeakPrevention: Bool
1314
var updaterStatus = UpdaterStatus()
1415
var debugBundleStatus = DebugBundleStatus()
1516

16-
init(osVpnStatus: NEVPNStatus) {
17+
init(
18+
strictLeakPrevention: Bool,
19+
osVpnStatus: NEVPNStatus,
20+
) {
1721
self.osVpnStatus = osVpnStatus
22+
self.strictLeakPrevention = strictLeakPrevention
1823
}
1924

20-
static func watchable(connection: NEVPNConnection) -> WatchableValue<OsStatus> {
21-
let notifications = NotificationCenter.default.notifications(named: .NEVPNStatusDidChange, object: connection)
22-
let w = WatchableValue(OsStatus(osVpnStatus: connection.status))
25+
static func watchable(
26+
manager: NEVPNManager,
27+
) -> WatchableValue<OsStatus> {
28+
var lastIncludeAllNetworks = switch manager.protocolConfiguration {
29+
case let .some(proto): proto.includeAllNetworks
30+
case nil: false // Report safe default.
31+
}
32+
let w = WatchableValue(OsStatus(
33+
strictLeakPrevention: lastIncludeAllNetworks,
34+
osVpnStatus: manager.connection.status
35+
))
2336
Task {
2437
for await path in NWPathMonitor().stream() {
2538
logger.info("NWPathMonitor event: \(path.debugDescription, privacy: .public)")
@@ -29,16 +42,44 @@ class OsStatus: Encodable {
2942
}
3043
}
3144
}
45+
46+
let vpnConfigNotifications = NotificationCenter.default.notifications(named: .NEVPNConfigurationChange, object: manager)
3247
Task {
33-
for await _ in notifications {
34-
let osVpnStatus = connection.status
48+
for await _ in vpnConfigNotifications {
49+
let includeAllNetworks: Bool
50+
if let proto = manager.protocolConfiguration {
51+
includeAllNetworks = proto.includeAllNetworks
52+
} else {
53+
logger.warning("NEVPNManager.protocolConfiguration is nil")
54+
includeAllNetworks = false // Safe default
55+
}
56+
57+
logger.info("NEVPNConfigurationChangeNotification includeAllNetworks \(includeAllNetworks, privacy: .public)")
58+
59+
if includeAllNetworks == lastIncludeAllNetworks {
60+
continue
61+
}
62+
63+
lastIncludeAllNetworks = includeAllNetworks
64+
_ = w.update { value in
65+
value.strictLeakPrevention = includeAllNetworks
66+
value.version = UUID()
67+
}
68+
}
69+
}
70+
71+
let vpnStatusNotifications = NotificationCenter.default.notifications(named: .NEVPNStatusDidChange, object: manager.connection)
72+
Task {
73+
for await _ in vpnStatusNotifications {
74+
let osVpnStatus = manager.connection.status
3575
logger.info("NEVPNStatus event: \(osVpnStatus, privacy: .public)")
3676
_ = w.update { value in
3777
value.osVpnStatus = osVpnStatus
3878
value.version = UUID()
3979
}
4080
}
4181
}
82+
4283
return w
4384
}
4485
}

apple/client/TunnelProvider.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class TunnelProviderInit {
7777
let proto = NETunnelProviderProtocol()
7878
proto.providerBundleIdentifier = extensionBundleID()
7979
proto.serverAddress = "obscura.net"
80+
proto.includeAllNetworks = manager.protocolConfiguration?.includeAllNetworks ?? false
8081
manager.protocolConfiguration = proto
8182
manager.isEnabled = true
8283

apple/client/app_state.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class AppState: ObservableObject {
1616
) {
1717
self.manager = manager
1818
self.status = initialStatus
19-
self.osStatus = OsStatus.watchable(connection: manager.connection)
19+
self.osStatus = OsStatus.watchable(manager: manager)
2020

2121
Task { @MainActor in
2222
var version: UUID = initialStatus.version
@@ -63,6 +63,36 @@ class AppState: ObservableObject {
6363
}
6464
}
6565

66+
func setIncludeAllNetworks(enable: Bool) async throws {
67+
guard let proto = self.manager.protocolConfiguration else {
68+
throw "NEVPNManager.protocolConfiguration is nil"
69+
}
70+
71+
Self.logger.info("setIncludeAllNetworks \(proto.includeAllNetworks, privacy: .public)\(enable, privacy: .public)")
72+
73+
if proto.includeAllNetworks == enable { return }
74+
75+
proto.includeAllNetworks = enable
76+
do {
77+
try await self.manager.saveToPreferences()
78+
return
79+
} catch {
80+
Self.logger.error("Failed to save NEVPNManager: \(error.localizedDescription)")
81+
}
82+
83+
do {
84+
try await self.manager.loadFromPreferences()
85+
return
86+
} catch {
87+
Self.logger.error("Failed to reload NEVPNManager: \(error.localizedDescription)")
88+
}
89+
90+
proto.includeAllNetworks = false
91+
Self.logger.warning("Marking local includeAllNetworks to false as a safe default.")
92+
93+
throw "Unable to save VPN configuration."
94+
}
95+
6696
func enableTunnel(_ tunnelArgs: TunnelArgs) async throws(String) {
6797
for _ in 1 ..< 3 {
6898
// Checking the status to decide whether to use `startVPNTunnel` or the `setTunnelArgs` command is not necessary for correct behavior. Handling of the `errorCodeTunnelInactive` error code is sufficient to always do the right thing eventually. However, this does require an app message round-trip to the NE, which can be a little slow at times.

apple/client/command.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ enum Command: Codable {
1010
case stopTunnel
1111
case debuggingArchive
1212
case revealItemInDir(path: String)
13+
case setStrictLeakPrevention(enable: Bool)
1314
case registerAsLoginItem
1415
case unregisterAsLoginItem
1516
case isRegisteredAsLoginItem
@@ -55,6 +56,13 @@ func handleWebViewCommand(command: Command) async throws(String) -> String {
5556
case .resetUserDefaults:
5657
// NOTE: only shown in the Developer View
5758
appState.resetUserDefaults()
59+
case .setStrictLeakPrevention(let enable):
60+
do {
61+
try await appState.setIncludeAllNetworks(enable: enable)
62+
} catch {
63+
logger.error("Could not set includeAllNetworks \(error, privacy: .public)")
64+
throw errorCodeOther
65+
}
5866
case .jsonFfiCmd(cmd: let jsonCmd, let timeoutMs):
5967
let attemptTimeout: Duration? = switch timeoutMs {
6068
case .some(let ms): .milliseconds(ms)

obscura-ui/src/bridge/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export function logout() {
7373
return jsonFfiCmd('logout');
7474
}
7575

76+
export async function setStrictLeakPrevention(enable: boolean): Promise<void> {
77+
await invoke('setStrictLeakPrevention', { enable });
78+
}
79+
7680
export function connect(exit: string | null = null) {
7781
let jsonTunnelArgs = JSON.stringify(({ exit }));
7882
return invoke('startTunnel', { tunnelArgs: jsonTunnelArgs });

obscura-ui/src/common/appContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface OsStatus {
3838
internetAvailable: boolean,
3939
osVpnStatus: NEVPNStatus,
4040
srcVersion: string
41+
strictLeakPrevention: boolean,
4142
updaterStatus: UpdaterStatus,
4243
debugBundleStatus: {
4344
inProgress?: boolean,

obscura-ui/src/common/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ export function percentEncodeQuery(params: Record<string, string>) {
110110

111111
const DEFAULT_ERROR_MSG = "An unexpected error has occurred.";
112112

113+
export function errMsg(error: unknown): string {
114+
if (error instanceof Error) {
115+
return error.message;
116+
}
117+
console.warn(fmt`errMsg: error = ${error} is not an instance of Error`);
118+
return DEFAULT_ERROR_MSG;
119+
}
120+
113121
export function normalizeError(error: unknown): Error {
114122
if (error instanceof Error) {
115123
return error;

obscura-ui/src/views/DeveloperView.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Accordion, Autocomplete, Button, Group, JsonInput, Stack, Text, TextInput, Title } from '@mantine/core';
1+
import { Accordion, Autocomplete, Button, Group, JsonInput, Stack, Switch, Text, TextInput, Title } from '@mantine/core';
22
import { useInterval } from '@mantine/hooks';
33
import { notifications } from '@mantine/notifications';
44
import Cookies from 'js-cookie';
@@ -8,7 +8,7 @@ import * as commands from '../bridge/commands';
88
import { Exit } from '../common/api';
99
import { AppContext } from '../common/appContext';
1010
import { localStorageGet, LocalStorageKey } from '../common/localStorage';
11-
import { IS_WK_WEB_VIEW } from '../common/utils';
11+
import { errMsg, IS_WK_WEB_VIEW } from '../common/utils';
1212
import DevSendCommand from '../components/DevSendCommand';
1313
import DevSetApiUrl from '../components/DevSetApiUrl';
1414

@@ -36,6 +36,9 @@ export default function DeveloperViewer() {
3636

3737
const [accordionValues, setAccordionValues] = useState<string[]>([]);
3838

39+
const [strictLeakLoading, setStrictLeakLoading] = useState(false);
40+
const [strictLeakError, setStrictLeakError] = useState<string>();
41+
3942
return <Stack p={20} mb={50}>
4043
<Title order={3}>Developer View</Title>
4144
<Title order={4}>Statuses</Title>
@@ -78,6 +81,22 @@ export default function DeveloperViewer() {
7881
<Autocomplete onChange={setLocalStorageKey} label='local storage key' data={Object.values(LocalStorageKey)} />
7982
<Button onClick={() => setLocalStorageValue(localStorageGet(localStorageKey as LocalStorageKey))}>Get</Button>
8083
</Group>
84+
<Switch
85+
error={strictLeakError}
86+
checked={osStatus.strictLeakPrevention}
87+
disabled={strictLeakLoading}
88+
onChange={async event => {
89+
try {
90+
setStrictLeakLoading(true);
91+
setStrictLeakError(undefined);
92+
await commands.setStrictLeakPrevention(event.currentTarget.checked);
93+
} catch (err) {
94+
setStrictLeakError(errMsg(err));
95+
} finally {
96+
setStrictLeakLoading(false);
97+
}
98+
}}
99+
label="Enable Strict Leak Prevention" />
81100
<JsonInput value={localStorageValue ?? 'null'} contentEditable={false} />
82101
</Stack>;
83102
}

0 commit comments

Comments
 (0)