Skip to content

Commit

Permalink
Allow app to use custom socks5 and shadwosocks proxies
Browse files Browse the repository at this point in the history
This PR has a couple of different purposes
    - Allow users to use socks5 local proxies with the CLI without
      having to be root nor use split-tunneling. This only works for
      OpenVPN.
    - Unify the types used by different proxy parts of the codebase,
      such as the Access Methods as well as some already existing
      OpenVPN proxy code.

This PR changes the firewall on all desktop platforms as well as changes
the routing table slightly on MacOS and Windows.
On Linux the firewall code is modified to apply the appropriate firewall
marks to all packages that go to a remote endpoint corresponding to the
remote part of a local socks5 proxy. The firewall marks will allow the
routing to be done without having to modify the routing table.
On MacOS and Windows the routing table is modified to allow packages to
go to that same endpoint to pass outside the VPN tunnel, it will
additionally punch a hole in the firewall.

The PR also migrates the settings file from version 7 to version 8 in order
to properly and neatly unify Proxy related types.

Finally it provides some slight extensions to the gRPC interface in
order to allow for control over the custom proxy settings.
Jontified committed Jan 3, 2024
1 parent 7378aa2 commit b96fb43
Showing 53 changed files with 2,513 additions and 1,273 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
140 changes: 97 additions & 43 deletions gui/src/main/daemon-rpc.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import {
AuthFailedError,
BridgeSettings,
BridgeState,
BridgeType,
ConnectionConfig,
Constraint,
CustomListError,
@@ -51,7 +52,6 @@ import {
ObfuscationSettings,
ObfuscationType,
Ownership,
ProxySettings,
ProxyType,
RelayEndpointType,
RelayLocation,
@@ -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>(
@@ -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(
2 changes: 2 additions & 0 deletions gui/src/main/default-settings.ts
Original file line number Diff line number Diff line change
@@ -29,11 +29,13 @@ export function getDefaultSettings(): ISettings {
},
},
bridgeSettings: {
type: 'normal',
normal: {
location: 'any',
providers: [],
ownership: Ownership.any,
},
custom: undefined,
},
bridgeState: 'auto',
tunnelOptions: {
18 changes: 7 additions & 11 deletions gui/src/renderer/app.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Original file line number Diff line number Diff line change
@@ -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';

@@ -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) {
@@ -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) {
2 changes: 1 addition & 1 deletion gui/src/renderer/lib/utilityHooks.ts
Original file line number Diff line number Diff line change
@@ -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,
@@ -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;
@@ -140,9 +139,11 @@ const initialState: ISettingsReduxState = {
allowLan: false,
enableIpv6: true,
bridgeSettings: {
type: 'normal',
normal: {
location: 'any',
},
custom: undefined,
},
bridgeState: 'auto',
blockWhenDisconnected: false,
2 changes: 2 additions & 0 deletions gui/src/shared/bridge-settings-builder.ts
Original file line number Diff line number Diff line change
@@ -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');
24 changes: 18 additions & 6 deletions gui/src/shared/daemon-rpc-types.ts
Original file line number Diff line number Diff line change
@@ -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;
}

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

export interface IShadowsocksProxySettings {
peer: string;
ip: string;
port: number;
password: string;
cipher: string;
}
@@ -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;
Loading

0 comments on commit b96fb43

Please sign in to comment.