Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for custom OpenVPN socks5 bridge client in daemon #5587

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Line wrap the file at 100 chars. Th
### Changed
- Remove `--location` flag from `mullvad status` CLI. Location and IP will now always
be printed (if available). `mullvad status listen` no longer prints location info.
- Custom socks5 bridges get a new CLI interface and now work without split tunneling or root.
In the CLI these can be found under `mullvad bridge set custom`.

#### Android
- Migrate to Compose Navigation which also improves screen transition animations.
Expand Down
140 changes: 97 additions & 43 deletions gui/src/main/daemon-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
AuthFailedError,
BridgeSettings,
BridgeState,
BridgeType,
ConnectionConfig,
Constraint,
CustomListError,
Expand Down Expand Up @@ -51,7 +52,6 @@ import {
ObfuscationSettings,
ObfuscationType,
Ownership,
ProxySettings,
ProxyType,
RelayEndpointType,
RelayLocation,
Expand Down Expand Up @@ -341,13 +341,52 @@ export class DaemonRpc {
public async setBridgeSettings(bridgeSettings: BridgeSettings): Promise<void> {
const grpcBridgeSettings = new grpcTypes.BridgeSettings();

if ('normal' in bridgeSettings) {
const normalSettings = convertToNormalBridgeSettings(bridgeSettings.normal);
grpcBridgeSettings.setNormal(normalSettings);
if (bridgeSettings.type === 'custom') {
throw configNotSupported;
}

if ('custom' in bridgeSettings) {
throw configNotSupported;
grpcBridgeSettings.setBridgeType(grpcTypes.BridgeSettings.BridgeType.NORMAL);

const normalSettings = convertToNormalBridgeSettings(bridgeSettings.normal);
grpcBridgeSettings.setNormal(normalSettings);

if (bridgeSettings.custom) {
const customProxy = new grpcTypes.CustomProxy();

const customSettings = bridgeSettings.custom;

if ('local' in customSettings) {
const local = customSettings.local;
const socks5Local = new grpcTypes.Socks5Local();
socks5Local.setLocalPort(local.localPort);
socks5Local.setRemoteIp(local.remoteIp);
socks5Local.setRemotePort(local.remotePort);
customProxy.setSocks5local(socks5Local);
}
if ('remote' in customSettings) {
const remote = customSettings.remote;
const socks5Remote = new grpcTypes.Socks5Remote();
if (remote.auth) {
const auth = new grpcTypes.SocksAuth();
auth.setUsername(remote.auth.username);
auth.setPassword(remote.auth.password);
socks5Remote.setAuth(auth);
}
socks5Remote.setIp(remote.ip);
socks5Remote.setPort(remote.port);
customProxy.setSocks5remote(socks5Remote);
}
if ('shadowsocks' in customSettings) {
const shadowsocks = customSettings.shadowsocks;
const shadowOut = new grpcTypes.Shadowsocks();
shadowOut.setCipher(shadowsocks.cipher);
shadowOut.setIp(shadowsocks.ip);
shadowOut.setPort(shadowsocks.port);
shadowOut.setPassword(shadowsocks.password);
customProxy.setShadowsocks(shadowOut);
}

grpcBridgeSettings.setCustom(customProxy);
}

await this.call<grpcTypes.BridgeSettings, Empty>(
Expand Down Expand Up @@ -1140,49 +1179,64 @@ function convertFromRelaySettings(

function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings {
const bridgeSettingsObject = bridgeSettings.toObject();
const normalSettings = bridgeSettingsObject.normal;
if (normalSettings) {
const locationConstraint = convertFromLocationConstraint(
bridgeSettings.getNormal()?.getLocation(),
);
const location = wrapConstraint(locationConstraint);
const providers = normalSettings.providersList;
const ownership = convertFromOwnership(normalSettings.ownership);
return {
normal: {
location,
providers,
ownership,
},
};
}

const customSettings = (settings: ProxySettings): BridgeSettings => {
return { custom: settings };
const detailsMap: Record<grpcTypes.BridgeSettings.BridgeType, BridgeType> = {
[grpcTypes.BridgeSettings.BridgeType.NORMAL]: 'normal',
[grpcTypes.BridgeSettings.BridgeType.CUSTOM]: 'custom',
};
const type = detailsMap[bridgeSettingsObject.bridgeType];

const localSettings = bridgeSettingsObject.local;
if (localSettings) {
return customSettings({
port: localSettings.port,
peer: localSettings.peer,
});
}
const normalSettings = bridgeSettingsObject.normal;
const locationConstraint = convertFromLocationConstraint(
bridgeSettings.getNormal()?.getLocation(),
);
const location = wrapConstraint(locationConstraint);
const providers = normalSettings!.providersList;
const ownership = convertFromOwnership(normalSettings!.ownership);

const normal = {
location,
providers,
ownership,
};

const remoteSettings = bridgeSettingsObject.remote;
if (remoteSettings) {
return customSettings({
address: remoteSettings.address,
auth: remoteSettings.auth && { ...remoteSettings.auth },
});
let custom = undefined;

if (bridgeSettingsObject.custom) {
const localSettings = bridgeSettingsObject.custom.socks5local;
if (localSettings) {
custom = {
local: {
localPort: localSettings.localPort,
remoteIp: localSettings.remoteIp,
remotePort: localSettings.remotePort,
},
};
}
const remoteSettings = bridgeSettingsObject.custom.socks5remote;
if (remoteSettings) {
custom = {
remote: {
ip: remoteSettings.ip,
port: remoteSettings.port,
auth: remoteSettings.auth && { ...remoteSettings.auth },
},
};
}
const shadowsocksSettings = bridgeSettingsObject.custom.shadowsocks;
if (shadowsocksSettings) {
custom = {
shadowsocks: {
ip: shadowsocksSettings.ip,
port: shadowsocksSettings.port,
password: shadowsocksSettings.password,
cipher: shadowsocksSettings.cipher,
},
};
}
}

const shadowsocksSettings = bridgeSettingsObject.shadowsocks!;
return customSettings({
peer: shadowsocksSettings.peer!,
password: shadowsocksSettings.password!,
cipher: shadowsocksSettings.cipher!,
});
return { type, normal, custom };
}

function convertFromConnectionConfig(
Expand Down
2 changes: 2 additions & 0 deletions gui/src/main/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ export function getDefaultSettings(): ISettings {
},
},
bridgeSettings: {
type: 'normal',
normal: {
location: 'any',
providers: [],
ownership: Ownership.any,
},
custom: undefined,
},
bridgeState: 'auto',
tunnelOptions: {
Expand Down
18 changes: 7 additions & 11 deletions gui/src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -624,17 +624,13 @@ export default class AppRenderer {
private setBridgeSettings(bridgeSettings: BridgeSettings) {
const actions = this.reduxActions;

if ('normal' in bridgeSettings) {
actions.settings.updateBridgeSettings({
normal: {
location: liftConstraint(bridgeSettings.normal.location),
},
});
} else if ('custom' in bridgeSettings) {
actions.settings.updateBridgeSettings({
custom: bridgeSettings.custom,
});
}
actions.settings.updateBridgeSettings({
type: bridgeSettings.type,
normal: {
location: liftConstraint(bridgeSettings.normal.location),
},
custom: bridgeSettings.custom,
});
}

private onDaemonConnected() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import log from '../../../shared/logging';
import { useAppContext } from '../../context';
import { useRelaySettingsModifier } from '../../lib/constraint-updater';
import { useHistory } from '../../lib/history';
import { useSelector } from '../../redux/store';
import { LocationType, SpecialBridgeLocationType } from './select-location-types';
import { useSelectLocationContext } from './SelectLocationContainer';

Expand Down Expand Up @@ -88,6 +89,7 @@ function useOnSelectLocation() {
export function useOnSelectBridgeLocation() {
const { updateBridgeSettings } = useAppContext();
const { setLocationType } = useSelectLocationContext();
const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);

const setLocation = useCallback(async (bridgeUpdate: BridgeSettings) => {
if (bridgeUpdate) {
Expand All @@ -101,10 +103,14 @@ export function useOnSelectBridgeLocation() {
}
}, []);

const onSelectRelay = useCallback((location: RelayLocation) => {
const bridgeUpdate = new BridgeSettingsBuilder().location.fromRaw(location).build();
return setLocation(bridgeUpdate);
}, []);
const onSelectRelay = useCallback(
(location: RelayLocation) => {
const bridgeUpdate = new BridgeSettingsBuilder().location.fromRaw(location).build();
bridgeUpdate.custom = bridgeSettings.custom;
return setLocation(bridgeUpdate);
},
[bridgeSettings],
);

const onSelectSpecial = useCallback((location: SpecialBridgeLocationType) => {
switch (location) {
Expand Down
2 changes: 1 addition & 1 deletion gui/src/renderer/lib/utilityHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,5 @@ export function useNormalRelaySettings() {

export function useNormalBridgeSettings() {
const bridgeSettings = useSelector((state) => state.settings.bridgeSettings);
return 'normal' in bridgeSettings ? bridgeSettings.normal : undefined;
return bridgeSettings.normal;
}
15 changes: 8 additions & 7 deletions gui/src/renderer/redux/settings/reducers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IWindowsApplication } from '../../../shared/application-types';
import {
BridgeState,
BridgeType,
CustomLists,
IDnsOptions,
IpVersion,
Expand Down Expand Up @@ -51,13 +52,11 @@ export type RelaySettingsRedux =
};
};

export type BridgeSettingsRedux =
| {
normal: NormalBridgeSettingsRedux;
}
| {
custom: ProxySettings;
};
export type BridgeSettingsRedux = {
type: BridgeType;
normal: NormalBridgeSettingsRedux;
custom?: ProxySettings;
};

export interface IRelayLocationRelayRedux {
hostname: string;
Expand Down Expand Up @@ -140,9 +139,11 @@ const initialState: ISettingsReduxState = {
allowLan: false,
enableIpv6: true,
bridgeSettings: {
type: 'normal',
normal: {
location: 'any',
},
custom: undefined,
},
bridgeState: 'auto',
blockWhenDisconnected: false,
Expand Down
2 changes: 2 additions & 0 deletions gui/src/shared/bridge-settings-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ export default class BridgeSettingsBuilder {
public build(): BridgeSettings {
if (this.payload.location) {
return {
type: 'normal',
normal: {
location: this.payload.location,
providers: this.payload.providers ?? [],
ownership: this.payload.ownership ?? Ownership.any,
},
custom: undefined,
};
} else {
throw new Error('Unsupported configuration');
Expand Down
24 changes: 18 additions & 6 deletions gui/src/shared/daemon-rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,20 @@ export interface IDnsOptions {
};
}

export type ProxySettings = ILocalProxySettings | IRemoteProxySettings | IShadowsocksProxySettings;
export type ProxySettings =
| { local: ILocalProxySettings }
| { remote: IRemoteProxySettings }
| { shadowsocks: IShadowsocksProxySettings };

export interface ILocalProxySettings {
port: number;
peer: string;
localPort: number;
remoteIp: string;
remotePort: number;
}

export interface IRemoteProxySettings {
address: string;
ip: string;
port: number;
auth?: IRemoteProxyAuth;
}

Expand All @@ -363,7 +368,8 @@ export interface IRemoteProxyAuth {
}

export interface IShadowsocksProxySettings {
peer: string;
ip: string;
port: number;
password: string;
cipher: string;
}
Expand Down Expand Up @@ -451,7 +457,13 @@ export interface IBridgeConstraints {
ownership: Ownership;
}

export type BridgeSettings = { normal: IBridgeConstraints } | { custom: ProxySettings };
export type BridgeType = 'normal' | 'custom';

export interface BridgeSettings {
type: BridgeType;
normal: IBridgeConstraints;
custom?: ProxySettings;
}

export interface ISocketAddress {
host: string;
Expand Down
Loading
Loading