Skip to content

Commit

Permalink
Merge branch 'add-wireguard-over-shadowsocks-to-gui-des-967'
Browse files Browse the repository at this point in the history
  • Loading branch information
raksooo committed Aug 30, 2024
2 parents 9dbb580 + 93dd4c5 commit 3454ded
Show file tree
Hide file tree
Showing 21 changed files with 634 additions and 132 deletions.
5 changes: 2 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ Line wrap the file at 100 chars. Th

## [Unreleased]
### Added
- Add WireGuard over Shadowsocks obfuscation to the CLI. It can be enabled with
`mullvad obfuscation set mode shadowsocks`. This will also be used automatically when connecting
fails with other methods.
- Add WireGuard over Shadowsocks obfuscation. It can be enabled in "WireGuard settings". This will
also be used automatically when connecting fails with other methods.

#### Windows
- Add experimental support for Windows ARM64.
Expand Down
41 changes: 39 additions & 2 deletions gui/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,20 @@ msgctxt "accessibility"
msgid "Select location. Current location is %(location)s"
msgstr ""

msgctxt "accessibility"
msgid "Shadowsocks settings"
msgstr ""

#. Provided to accessibility tools such as screenreaders to describe
#. the button which unobscures the account number.
msgctxt "accessibility"
msgid "Show account number"
msgstr ""

msgctxt "accessibility"
msgid "UDP-over-TCP settings"
msgstr ""

#. Title label in navigation bar
msgctxt "account-view"
msgid "Account"
Expand Down Expand Up @@ -2036,6 +2044,16 @@ msgctxt "wireguard-settings-nav"
msgid "%(wireguard)s settings"
msgstr ""

#. Title label in navigation bar
msgctxt "wireguard-settings-nav"
msgid "Shadowsocks"
msgstr ""

#. Title label in navigation bar
msgctxt "wireguard-settings-nav"
msgid "UDP-over-TCP"
msgstr ""

msgctxt "wireguard-settings-view"
msgid "%(daita)s (%(daitaFull)s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size."
msgstr ""
Expand Down Expand Up @@ -2071,11 +2089,14 @@ msgid "Obfuscation hides the WireGuard traffic inside another protocol. It can b
msgstr ""

msgctxt "wireguard-settings-view"
msgid "On (UDP-over-TCP)"
msgid "Port"
msgstr ""

#. Text showing currently selected port.
#. Available placeholders:
#. %(port)s - Can be either a number between 1 and 65000 or the text "Automatic".
msgctxt "wireguard-settings-view"
msgid "Port"
msgid "Port: %(port)s"
msgstr ""

#. The title for the WireGuard quantum resistance selector. This setting
Expand All @@ -2094,6 +2115,10 @@ msgctxt "wireguard-settings-view"
msgid "Set %(wireguard)s MTU value. Valid range: %(min)d - %(max)d."
msgstr ""

msgctxt "wireguard-settings-view"
msgid "Shadowsocks"
msgstr ""

msgctxt "wireguard-settings-view"
msgid "The automatic setting will randomly choose from the valid port ranges shown below."
msgstr ""
Expand All @@ -2118,10 +2143,19 @@ msgctxt "wireguard-settings-view"
msgid "This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers."
msgstr ""

msgctxt "wireguard-settings-view"
msgid "UDP-over-TCP"
msgstr ""

msgctxt "wireguard-settings-view"
msgid "UDP-over-TCP port"
msgstr ""

#. Text describing the valid port range for a port selector.
msgctxt "wireguard-settings-view"
msgid "Valid range: %(min)s - %(max)s"
msgstr ""

msgctxt "wireguard-settings-view"
msgid "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server."
msgstr ""
Expand Down Expand Up @@ -2387,6 +2421,9 @@ msgstr ""
msgid "Not found"
msgstr ""

msgid "On (UDP-over-TCP)"
msgstr ""

msgid "Overrides active"
msgstr ""

Expand Down
37 changes: 30 additions & 7 deletions gui/src/main/daemon-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
DeviceEvent,
DeviceState,
DirectMethod,
EndpointObfuscationType,
ErrorStateCause,
ErrorStateDetails,
FeatureIndicator,
Expand Down Expand Up @@ -388,6 +389,11 @@ export class DaemonRpc {
grpcTypes.ObfuscationSettings.SelectedObfuscation.OFF,
);
break;
case ObfuscationType.shadowsocks:
grpcObfuscationSettings.setSelectedObfuscation(
grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS,
);
break;
case ObfuscationType.udp2tcp:
grpcObfuscationSettings.setSelectedObfuscation(
grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP,
Expand All @@ -403,7 +409,13 @@ export class DaemonRpc {
grpcObfuscationSettings.setUdp2tcp(grpcUdp2tcpSettings);
}

grpcObfuscationSettings.setShadowsocks(new grpcTypes.ShadowsocksSettings());
if (obfuscationSettings.shadowsocksSettings) {
const shadowsocksSettings = new grpcTypes.ShadowsocksSettings();
if (obfuscationSettings.shadowsocksSettings.port !== 'any') {
shadowsocksSettings.setPort(obfuscationSettings.shadowsocksSettings.port.only);
}
grpcObfuscationSettings.setShadowsocks(shadowsocksSettings);
}

await this.call<grpcTypes.ObfuscationSettings, Empty>(
this.client.setObfuscationSettings,
Expand Down Expand Up @@ -1200,17 +1212,22 @@ function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObjec
function convertFromObfuscationEndpoint(
obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject,
): IObfuscationEndpoint {
// TODO: Handle Shadowsocks (and other implemented protocols)
if (
obfuscationEndpoint.obfuscationType !== grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP
) {
throw new Error('unsupported obfuscation protocol');
let obfuscationType: EndpointObfuscationType;
switch (obfuscationEndpoint.obfuscationType) {
case grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP:
obfuscationType = 'udp2tcp';
break;
case grpcTypes.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS:
obfuscationType = 'shadowsocks';
break;
default:
throw new Error('unsupported obfuscation protocol');
}

return {
...obfuscationEndpoint,
protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol),
obfuscationType: 'udp2tcp',
obfuscationType: obfuscationType,
};
}

Expand Down Expand Up @@ -1470,13 +1487,19 @@ function convertFromObfuscationSettings(
case grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP:
selectedObfuscationType = ObfuscationType.udp2tcp;
break;
case grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS:
selectedObfuscationType = ObfuscationType.shadowsocks;
break;
}

return {
selectedObfuscation: selectedObfuscationType,
udp2tcpSettings: obfuscationSettings?.udp2tcp
? { port: convertFromConstraint(obfuscationSettings.udp2tcp.port) }
: { port: 'any' },
shadowsocksSettings: obfuscationSettings?.shadowsocks
? { port: convertFromConstraint(obfuscationSettings.shadowsocks.port) }
: { port: 'any' },
};
}

Expand Down
3 changes: 3 additions & 0 deletions gui/src/main/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export function getDefaultSettings(): ISettings {
udp2tcpSettings: {
port: 'any',
},
shadowsocksSettings: {
port: 'any',
},
},
customLists: [],
apiAccessMethods: getDefaultApiAccessMethods(),
Expand Down
4 changes: 4 additions & 0 deletions gui/src/renderer/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ import SelectLanguage from './SelectLanguage';
import Settings from './Settings';
import SettingsImport from './SettingsImport';
import SettingsTextImport from './SettingsTextImport';
import Shadowsocks from './Shadowsocks';
import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import TransitionContainer, { TransitionView } from './TransitionContainer';
import UdpOverTcp from './UdpOverTcp';
import UserInterfaceSettings from './UserInterfaceSettings';
import VpnSettings from './VpnSettings';
import WireguardSettings from './WireguardSettings';
Expand Down Expand Up @@ -83,6 +85,8 @@ export default function AppRouter() {
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
Expand Down
137 changes: 137 additions & 0 deletions gui/src/renderer/components/Shadowsocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useCallback } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';

import { wrapConstraint } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { removeNonNumericCharacters } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
import { useSelector } from '../redux/store';
import { AriaDescription, AriaInputGroup } from './AriaGroup';
import * as Cell from './cell';
import { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import {
NavigationBar,
NavigationContainer,
NavigationItems,
NavigationScrollbars,
TitleBarItem,
} from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';

const PORTS: Array<SelectorItem<number>> = [];
const ALLOWED_RANGE = [1, 65000];

const StyledContent = styled.div({
display: 'flex',
flexDirection: 'column',
flex: 1,
marginBottom: '2px',
});

const StyledSelectorContainer = styled.div({
flex: 0,
});

export default function Shadowsocks() {
const { pop } = useHistory();

return (
<BackAction action={pop}>
<Layout>
<SettingsContainer>
<NavigationContainer>
<NavigationBar>
<NavigationItems>
<TitleBarItem>
{
// TRANSLATORS: Title label in navigation bar
messages.pgettext('wireguard-settings-nav', 'Shadowsocks')
}
</TitleBarItem>
</NavigationItems>
</NavigationBar>

<NavigationScrollbars>
<SettingsHeader>
<HeaderTitle>
{messages.pgettext('wireguard-settings-view', 'Shadowsocks')}
</HeaderTitle>
</SettingsHeader>

<StyledContent>
<Cell.Group>
<ShadowsocksPortSelector />
</Cell.Group>
</StyledContent>
</NavigationScrollbars>
</NavigationContainer>
</SettingsContainer>
</Layout>
</BackAction>
);
}

function ShadowsocksPortSelector() {
const { setObfuscationSettings } = useAppContext();
const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings);

const port =
obfuscationSettings.shadowsocksSettings.port === 'any'
? null
: obfuscationSettings.shadowsocksSettings.port.only;

const setShadowsocksPort = useCallback(
async (port: number | null) => {
await setObfuscationSettings({
...obfuscationSettings,
shadowsocksSettings: {
...obfuscationSettings.shadowsocksSettings,
port: wrapConstraint(port),
},
});
},
[setObfuscationSettings, obfuscationSettings],
);

const parseValue = useCallback((port: string) => parseInt(port), []);

const validateValue = useCallback(
(value: number) => value >= ALLOWED_RANGE[0] && value <= ALLOWED_RANGE[1],
[],
);

return (
<AriaInputGroup>
<StyledSelectorContainer>
<SelectorWithCustomItem
// TRANSLATORS: The title for the WireGuard port selector.
title={messages.pgettext('wireguard-settings-view', 'Port')}
items={PORTS}
value={port}
onSelect={setShadowsocksPort}
inputPlaceholder={messages.pgettext('wireguard-settings-view', 'Port')}
automaticValue={null}
parseValue={parseValue}
modifyValue={removeNonNumericCharacters}
validateValue={validateValue}
maxLength={`${ALLOWED_RANGE[1]}`.length}
/>
</StyledSelectorContainer>
<Cell.CellFooter>
<AriaDescription>
<Cell.CellFooterText>
{sprintf(
// TRANSLATORS: Text describing the valid port range for a port selector.
messages.pgettext('wireguard-settings-view', 'Valid range: %(min)s - %(max)s'),
{ min: ALLOWED_RANGE[0], max: ALLOWED_RANGE[1] },
)}
</Cell.CellFooterText>
</AriaDescription>
</Cell.CellFooter>
</AriaInputGroup>
);
}
Loading

0 comments on commit 3454ded

Please sign in to comment.