Skip to content

Commit 3454ded

Browse files
committed
Merge branch 'add-wireguard-over-shadowsocks-to-gui-des-967'
2 parents 9dbb580 + 93dd4c5 commit 3454ded

21 files changed

+634
-132
lines changed

CHANGELOG.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ Line wrap the file at 100 chars. Th
2323

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

3029
#### Windows
3130
- Add experimental support for Windows ARM64.

gui/locales/messages.pot

+39-2
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,20 @@ msgctxt "accessibility"
357357
msgid "Select location. Current location is %(location)s"
358358
msgstr ""
359359

360+
msgctxt "accessibility"
361+
msgid "Shadowsocks settings"
362+
msgstr ""
363+
360364
#. Provided to accessibility tools such as screenreaders to describe
361365
#. the button which unobscures the account number.
362366
msgctxt "accessibility"
363367
msgid "Show account number"
364368
msgstr ""
365369

370+
msgctxt "accessibility"
371+
msgid "UDP-over-TCP settings"
372+
msgstr ""
373+
366374
#. Title label in navigation bar
367375
msgctxt "account-view"
368376
msgid "Account"
@@ -2036,6 +2044,16 @@ msgctxt "wireguard-settings-nav"
20362044
msgid "%(wireguard)s settings"
20372045
msgstr ""
20382046

2047+
#. Title label in navigation bar
2048+
msgctxt "wireguard-settings-nav"
2049+
msgid "Shadowsocks"
2050+
msgstr ""
2051+
2052+
#. Title label in navigation bar
2053+
msgctxt "wireguard-settings-nav"
2054+
msgid "UDP-over-TCP"
2055+
msgstr ""
2056+
20392057
msgctxt "wireguard-settings-view"
20402058
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."
20412059
msgstr ""
@@ -2071,11 +2089,14 @@ msgid "Obfuscation hides the WireGuard traffic inside another protocol. It can b
20712089
msgstr ""
20722090

20732091
msgctxt "wireguard-settings-view"
2074-
msgid "On (UDP-over-TCP)"
2092+
msgid "Port"
20752093
msgstr ""
20762094

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

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

2118+
msgctxt "wireguard-settings-view"
2119+
msgid "Shadowsocks"
2120+
msgstr ""
2121+
20972122
msgctxt "wireguard-settings-view"
20982123
msgid "The automatic setting will randomly choose from the valid port ranges shown below."
20992124
msgstr ""
@@ -2118,10 +2143,19 @@ msgctxt "wireguard-settings-view"
21182143
msgid "This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers."
21192144
msgstr ""
21202145

2146+
msgctxt "wireguard-settings-view"
2147+
msgid "UDP-over-TCP"
2148+
msgstr ""
2149+
21212150
msgctxt "wireguard-settings-view"
21222151
msgid "UDP-over-TCP port"
21232152
msgstr ""
21242153

2154+
#. Text describing the valid port range for a port selector.
2155+
msgctxt "wireguard-settings-view"
2156+
msgid "Valid range: %(min)s - %(max)s"
2157+
msgstr ""
2158+
21252159
msgctxt "wireguard-settings-view"
21262160
msgid "Which TCP port the UDP-over-TCP obfuscation protocol should connect to on the VPN server."
21272161
msgstr ""
@@ -2387,6 +2421,9 @@ msgstr ""
23872421
msgid "Not found"
23882422
msgstr ""
23892423

2424+
msgid "On (UDP-over-TCP)"
2425+
msgstr ""
2426+
23902427
msgid "Overrides active"
23912428
msgstr ""
23922429

gui/src/main/daemon-rpc.ts

+30-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
DeviceEvent,
3030
DeviceState,
3131
DirectMethod,
32+
EndpointObfuscationType,
3233
ErrorStateCause,
3334
ErrorStateDetails,
3435
FeatureIndicator,
@@ -388,6 +389,11 @@ export class DaemonRpc {
388389
grpcTypes.ObfuscationSettings.SelectedObfuscation.OFF,
389390
);
390391
break;
392+
case ObfuscationType.shadowsocks:
393+
grpcObfuscationSettings.setSelectedObfuscation(
394+
grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS,
395+
);
396+
break;
391397
case ObfuscationType.udp2tcp:
392398
grpcObfuscationSettings.setSelectedObfuscation(
393399
grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP,
@@ -403,7 +409,13 @@ export class DaemonRpc {
403409
grpcObfuscationSettings.setUdp2tcp(grpcUdp2tcpSettings);
404410
}
405411

406-
grpcObfuscationSettings.setShadowsocks(new grpcTypes.ShadowsocksSettings());
412+
if (obfuscationSettings.shadowsocksSettings) {
413+
const shadowsocksSettings = new grpcTypes.ShadowsocksSettings();
414+
if (obfuscationSettings.shadowsocksSettings.port !== 'any') {
415+
shadowsocksSettings.setPort(obfuscationSettings.shadowsocksSettings.port.only);
416+
}
417+
grpcObfuscationSettings.setShadowsocks(shadowsocksSettings);
418+
}
407419

408420
await this.call<grpcTypes.ObfuscationSettings, Empty>(
409421
this.client.setObfuscationSettings,
@@ -1200,17 +1212,22 @@ function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObjec
12001212
function convertFromObfuscationEndpoint(
12011213
obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject,
12021214
): IObfuscationEndpoint {
1203-
// TODO: Handle Shadowsocks (and other implemented protocols)
1204-
if (
1205-
obfuscationEndpoint.obfuscationType !== grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP
1206-
) {
1207-
throw new Error('unsupported obfuscation protocol');
1215+
let obfuscationType: EndpointObfuscationType;
1216+
switch (obfuscationEndpoint.obfuscationType) {
1217+
case grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP:
1218+
obfuscationType = 'udp2tcp';
1219+
break;
1220+
case grpcTypes.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS:
1221+
obfuscationType = 'shadowsocks';
1222+
break;
1223+
default:
1224+
throw new Error('unsupported obfuscation protocol');
12081225
}
12091226

12101227
return {
12111228
...obfuscationEndpoint,
12121229
protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol),
1213-
obfuscationType: 'udp2tcp',
1230+
obfuscationType: obfuscationType,
12141231
};
12151232
}
12161233

@@ -1470,13 +1487,19 @@ function convertFromObfuscationSettings(
14701487
case grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP:
14711488
selectedObfuscationType = ObfuscationType.udp2tcp;
14721489
break;
1490+
case grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS:
1491+
selectedObfuscationType = ObfuscationType.shadowsocks;
1492+
break;
14731493
}
14741494

14751495
return {
14761496
selectedObfuscation: selectedObfuscationType,
14771497
udp2tcpSettings: obfuscationSettings?.udp2tcp
14781498
? { port: convertFromConstraint(obfuscationSettings.udp2tcp.port) }
14791499
: { port: 'any' },
1500+
shadowsocksSettings: obfuscationSettings?.shadowsocks
1501+
? { port: convertFromConstraint(obfuscationSettings.shadowsocks.port) }
1502+
: { port: 'any' },
14801503
};
14811504
}
14821505

gui/src/main/default-settings.ts

+3
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export function getDefaultSettings(): ISettings {
7474
udp2tcpSettings: {
7575
port: 'any',
7676
},
77+
shadowsocksSettings: {
78+
port: 'any',
79+
},
7780
},
7881
customLists: [],
7982
apiAccessMethods: getDefaultApiAccessMethods(),

gui/src/renderer/components/AppRouter.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ import SelectLanguage from './SelectLanguage';
2929
import Settings from './Settings';
3030
import SettingsImport from './SettingsImport';
3131
import SettingsTextImport from './SettingsTextImport';
32+
import Shadowsocks from './Shadowsocks';
3233
import SplitTunnelingSettings from './SplitTunnelingSettings';
3334
import Support from './Support';
3435
import TooManyDevices from './TooManyDevices';
3536
import TransitionContainer, { TransitionView } from './TransitionContainer';
37+
import UdpOverTcp from './UdpOverTcp';
3638
import UserInterfaceSettings from './UserInterfaceSettings';
3739
import VpnSettings from './VpnSettings';
3840
import WireguardSettings from './WireguardSettings';
@@ -83,6 +85,8 @@ export default function AppRouter() {
8385
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
8486
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
8587
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
88+
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
89+
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
8690
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
8791
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
8892
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useCallback } from 'react';
2+
import { sprintf } from 'sprintf-js';
3+
import styled from 'styled-components';
4+
5+
import { wrapConstraint } from '../../shared/daemon-rpc-types';
6+
import { messages } from '../../shared/gettext';
7+
import { removeNonNumericCharacters } from '../../shared/string-helpers';
8+
import { useAppContext } from '../context';
9+
import { useHistory } from '../lib/history';
10+
import { useSelector } from '../redux/store';
11+
import { AriaDescription, AriaInputGroup } from './AriaGroup';
12+
import * as Cell from './cell';
13+
import { SelectorItem, SelectorWithCustomItem } from './cell/Selector';
14+
import { BackAction } from './KeyboardNavigation';
15+
import { Layout, SettingsContainer } from './Layout';
16+
import {
17+
NavigationBar,
18+
NavigationContainer,
19+
NavigationItems,
20+
NavigationScrollbars,
21+
TitleBarItem,
22+
} from './NavigationBar';
23+
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
24+
25+
const PORTS: Array<SelectorItem<number>> = [];
26+
const ALLOWED_RANGE = [1, 65000];
27+
28+
const StyledContent = styled.div({
29+
display: 'flex',
30+
flexDirection: 'column',
31+
flex: 1,
32+
marginBottom: '2px',
33+
});
34+
35+
const StyledSelectorContainer = styled.div({
36+
flex: 0,
37+
});
38+
39+
export default function Shadowsocks() {
40+
const { pop } = useHistory();
41+
42+
return (
43+
<BackAction action={pop}>
44+
<Layout>
45+
<SettingsContainer>
46+
<NavigationContainer>
47+
<NavigationBar>
48+
<NavigationItems>
49+
<TitleBarItem>
50+
{
51+
// TRANSLATORS: Title label in navigation bar
52+
messages.pgettext('wireguard-settings-nav', 'Shadowsocks')
53+
}
54+
</TitleBarItem>
55+
</NavigationItems>
56+
</NavigationBar>
57+
58+
<NavigationScrollbars>
59+
<SettingsHeader>
60+
<HeaderTitle>
61+
{messages.pgettext('wireguard-settings-view', 'Shadowsocks')}
62+
</HeaderTitle>
63+
</SettingsHeader>
64+
65+
<StyledContent>
66+
<Cell.Group>
67+
<ShadowsocksPortSelector />
68+
</Cell.Group>
69+
</StyledContent>
70+
</NavigationScrollbars>
71+
</NavigationContainer>
72+
</SettingsContainer>
73+
</Layout>
74+
</BackAction>
75+
);
76+
}
77+
78+
function ShadowsocksPortSelector() {
79+
const { setObfuscationSettings } = useAppContext();
80+
const obfuscationSettings = useSelector((state) => state.settings.obfuscationSettings);
81+
82+
const port =
83+
obfuscationSettings.shadowsocksSettings.port === 'any'
84+
? null
85+
: obfuscationSettings.shadowsocksSettings.port.only;
86+
87+
const setShadowsocksPort = useCallback(
88+
async (port: number | null) => {
89+
await setObfuscationSettings({
90+
...obfuscationSettings,
91+
shadowsocksSettings: {
92+
...obfuscationSettings.shadowsocksSettings,
93+
port: wrapConstraint(port),
94+
},
95+
});
96+
},
97+
[setObfuscationSettings, obfuscationSettings],
98+
);
99+
100+
const parseValue = useCallback((port: string) => parseInt(port), []);
101+
102+
const validateValue = useCallback(
103+
(value: number) => value >= ALLOWED_RANGE[0] && value <= ALLOWED_RANGE[1],
104+
[],
105+
);
106+
107+
return (
108+
<AriaInputGroup>
109+
<StyledSelectorContainer>
110+
<SelectorWithCustomItem
111+
// TRANSLATORS: The title for the WireGuard port selector.
112+
title={messages.pgettext('wireguard-settings-view', 'Port')}
113+
items={PORTS}
114+
value={port}
115+
onSelect={setShadowsocksPort}
116+
inputPlaceholder={messages.pgettext('wireguard-settings-view', 'Port')}
117+
automaticValue={null}
118+
parseValue={parseValue}
119+
modifyValue={removeNonNumericCharacters}
120+
validateValue={validateValue}
121+
maxLength={`${ALLOWED_RANGE[1]}`.length}
122+
/>
123+
</StyledSelectorContainer>
124+
<Cell.CellFooter>
125+
<AriaDescription>
126+
<Cell.CellFooterText>
127+
{sprintf(
128+
// TRANSLATORS: Text describing the valid port range for a port selector.
129+
messages.pgettext('wireguard-settings-view', 'Valid range: %(min)s - %(max)s'),
130+
{ min: ALLOWED_RANGE[0], max: ALLOWED_RANGE[1] },
131+
)}
132+
</Cell.CellFooterText>
133+
</AriaDescription>
134+
</Cell.CellFooter>
135+
</AriaInputGroup>
136+
);
137+
}

0 commit comments

Comments
 (0)