diff --git a/.github/workflows/daemon.yml b/.github/workflows/daemon.yml index 3293e028791e..a5107fbcf8c1 100644 --- a/.github/workflows/daemon.yml +++ b/.github/workflows/daemon.yml @@ -114,6 +114,10 @@ jobs: - name: Checkout submodules run: git submodule update --init --depth=1 + - name: Checkout wireguard-nt + working-directory: dist-assets/binaries + run: git submodule update --init -- wireguard-nt + - name: Install Protoc uses: arduino/setup-protoc@v1 with: diff --git a/Cargo.lock b/Cargo.lock index cc54dcb168c7..eedb5dd48029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "aead" version = "0.5.2" @@ -52,6 +58,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -582,6 +600,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.9" @@ -591,6 +618,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -714,6 +750,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dary_heap" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7762d17f1241643615821a8455a0b2c3e803784b058693d990b11f2dce25a0ca" + [[package]] name = "dashmap" version = "5.5.3" @@ -1217,6 +1259,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -1756,6 +1807,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libflate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7d5654ae1795afc7ff76f4365c2c8791b0feb18e8996a96adad8ffd7c3b2bf" +dependencies = [ + "adler32", + "core2", + "crc32fast", + "dary_heap", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5f52fb8c451576ec6b79d3f4deb327398bc05bbdbd99021a6e77a4c855d524" +dependencies = [ + "core2", + "hashbrown 0.13.2", + "rle-decode-fast", +] + [[package]] name = "libm" version = "0.2.8" @@ -1835,6 +1910,22 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +[[package]] +name = "maybenot" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc2e64fe3f5fb1e247110a9a408449eff2259cc272cf57bad6f161e801ac962" +dependencies = [ + "byteorder", + "hex", + "libflate", + "rand 0.8.5", + "rand_distr", + "ring", + "serde", + "simple-error", +] + [[package]] name = "md-5" version = "0.10.5" @@ -2973,6 +3064,16 @@ dependencies = [ "getrandom 0.2.10", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3099,6 +3200,12 @@ dependencies = [ "signature", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "rs-release" version = "0.1.9" @@ -3467,6 +3574,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple-error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8542b68b8800c3cda649d2c72d688b6907b30f1580043135d61669d4aad1c175" + [[package]] name = "simple-signal" version = "1.1.1" @@ -3870,6 +3983,7 @@ dependencies = [ name = "talpid-wireguard" version = "0.0.0" dependencies = [ + "base64 0.13.1", "bitflags 1.3.2", "byteorder", "chrono", @@ -3880,6 +3994,7 @@ dependencies = [ "ipnetwork", "libc", "log", + "maybenot", "netlink-packet-core", "netlink-packet-route", "netlink-packet-utils", @@ -4821,6 +4936,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index b0c28f016aa8..65391bcf3755 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "ios/MullvadREST/Transport/Shadowsocks/shadowsocks-proxy", "ios/TunnelObfuscation/tunnel-obfuscator-proxy", "mullvad-api", + "mullvad-daemon", "mullvad-cli", "mullvad-daemon", "mullvad-exclude", diff --git a/dist-assets/binaries b/dist-assets/binaries index d5772339cee9..d9edc48af33d 160000 --- a/dist-assets/binaries +++ b/dist-assets/binaries @@ -1 +1 @@ -Subproject commit d5772339cee9c1a0d7671968746f02499b78e245 +Subproject commit d9edc48af33db4afbcb8d6c7754e1dfe963d5172 diff --git a/dist-assets/maybenot_machines b/dist-assets/maybenot_machines new file mode 100644 index 000000000000..87e72c4562a2 --- /dev/null +++ b/dist-assets/maybenot_machines @@ -0,0 +1,4 @@ +789cd5cfbb0900200c04d08b833886adb889389f5bb9801be811acb58ae2837ce02010c158b070555c9538b6377a64dbb0ceff242c20b79038507dd169fbede9f629bf6f021efa1b66 +789cedd0510a401110856153f665ffbbb1030e991735717890f8eabae22f35e252e31a6c2b6c2bf158ce88010b61b69fed14db7fabd8498ffad17d8fedbf5decc4adde3ab7b0fddb045fa153d3ff09ecdb6c7faf0cd04322e8 +789cedd1510a80200c80617de95addff36dda096e84b209bfe0f1bb40f32297f1056cbdd954eb68d6c9b7ac8e2e33a650176fbdd6ea07df24227b7daaf9effa27df2462768edade766689fa2a093d47aedbf86f6291a3ad1593ffb6e45fb4454795e630ae31d11bd1bedffeb0151b22c62 +789cedd2bd4b82411c07f07b2a48846a089a326889861a9c2452ee12221e0ca2a5201ca5a122fc0bda042544370771f3159c9c0405e51914e45c5414df7d441075f00515c4c553f0117906075f40f0331c77c7ddef8efb1e05a6de8e6f3d96661392ee1c6a9f349b72e6316a6e542c4c980b5dbb24cfbbcccefa1d00d57df145ff44079506a677adacc29623a4780f30288a312e99440f809a7d9b457c4711a1b8cf7249ea2adf75cb698e1b4fb50f19dc4d67e0c95d12d354186ab3b1e74f5b9ab7aecc4e70f332e918bfdef6db03ce9042a2bba491e88324629022ef85a6f1aa95a3ece0efcb5f5223012039997e3b726455cb84a9f82322db96f6ff531dbada6558bb120bdce79915bf6c6bc5f51659febc1141265ee0 diff --git a/docs/architecture.md b/docs/architecture.md index 1deba30706d1..78305a8c6a99 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -200,7 +200,7 @@ WireGuard tunnel to the relay and deriving the PSK within the tunnel. The PSK is stored in memory on the relay and the client, along with a new client generated ephemeral WireGuard key. Subsequently, a new tunnel is created using the new WireGuard key and the PSK, ensuring that the tunnel is quantum-resistant. -See the [protocol definition file](../talpid-tunnel-config-client/proto/tunnel_config.proto) for +See the [protocol definition file](../talpid-tunnel-config-client/proto/ephemeralpeer.proto) for more details on the protocol. #### Quantum-resistant tunnels & Multihop diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 1873be203877..5eb26192b29a 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -630,6 +630,12 @@ msgctxt "connect-view" msgid "You’re all set!" msgstr "" +#. %(hostname)s - The current server the app is connected to, e.g. "se-got-wg-001 using DAITA" +#. %(daita)s - Will be replaced with "DAITA" +msgctxt "connection-info" +msgid "%(hostname)s using %(daita)s" +msgstr "" + #. The hostname line displayed below the country on the main screen #. Available placeholders: #. %(relay)s - the relay hostname @@ -1368,6 +1374,10 @@ msgctxt "select-location-view" msgid "Relay" msgstr "" +msgctxt "select-location-view" +msgid "Setting: %(settingName)s" +msgstr "" + msgctxt "select-location-view" msgid "The app selects a random bridge server, but servers have a higher probability the closer they are to you." msgstr "" @@ -1980,12 +1990,20 @@ msgctxt "wireguard-settings-nav" msgid "%(wireguard)s settings" 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 "" + #. Available placeholders: #. %(wireguard)s - Will be replaced with the string "WireGuard" msgctxt "wireguard-settings-view" msgid "%(wireguard)s settings" msgstr "" +msgctxt "wireguard-settings-view" +msgid "Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s." +msgstr "" + msgctxt "wireguard-settings-view" msgid "IP version" msgstr "" @@ -2045,6 +2063,11 @@ msgctxt "wireguard-settings-view" msgid "This allows access to %(wireguard)s for devices that only support IPv6." msgstr "" +#. Warning text in a dialog that is displayed after a setting is toggled. +msgctxt "wireguard-settings-view" +msgid "This feature isn't available on all servers. You might need to change location after enabling." +msgstr "" + msgctxt "wireguard-settings-view" msgid "This feature makes the WireGuard tunnel resistant to potential attacks from quantum computers." msgstr "" diff --git a/gui/src/config.json b/gui/src/config.json index c1ddcd6cac17..a77c47de4501 100644 --- a/gui/src/config.json +++ b/gui/src/config.json @@ -41,6 +41,8 @@ "strings": { "wireguard": "WireGuard", "openvpn": "OpenVPN", - "splitTunneling": "Split tunneling" + "splitTunneling": "Split tunneling", + "daita": "DAITA", + "daitaFull": "Defence against AI-guided Traffic Analysis" } } diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index 531ec80ced55..ba913fea8762 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -37,6 +37,7 @@ import { IAppVersionInfo, IBridgeConstraints, ICustomList, + IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -561,6 +562,15 @@ export class DaemonRpc { await this.callEmpty(this.client.updateDevice); } + public async setDaitaSettings(daitaSettings: IDaitaSettings): Promise { + const grpcDaitaSettings = new grpcTypes.DaitaSettings(); + grpcDaitaSettings.setEnabled(daitaSettings.enabled); + await this.call( + this.client.setDaitaSettings, + grpcDaitaSettings, + ); + } + public async listDevices(accountToken: AccountToken): Promise> { try { const response = await this.callString( @@ -847,9 +857,7 @@ function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayListWithEnd relayList: { countries: relayList .getCountriesList() - .map((country: grpcTypes.RelayListCountry) => - convertFromRelayListCountry(country.toObject()), - ), + .map((country: grpcTypes.RelayListCountry) => convertFromRelayListCountry(country)), }, wireguardEndpointData: convertWireguardEndpointData(relayList.getWireguard()!), }; @@ -864,26 +872,37 @@ function convertWireguardEndpointData( }; } -function convertFromRelayListCountry( - country: grpcTypes.RelayListCountry.AsObject, -): IRelayListCountry { +function convertFromRelayListCountry(country: grpcTypes.RelayListCountry): IRelayListCountry { + const countryObject = country.toObject(); return { - ...country, - cities: country.citiesList.map(convertFromRelayListCity), + ...countryObject, + cities: country.getCitiesList().map(convertFromRelayListCity), }; } -function convertFromRelayListCity(city: grpcTypes.RelayListCity.AsObject): IRelayListCity { +function convertFromRelayListCity(city: grpcTypes.RelayListCity): IRelayListCity { + const cityObject = city.toObject(); return { - ...city, - relays: city.relaysList.map(convertFromRelayListRelay), + ...cityObject, + relays: city.getRelaysList().map(convertFromRelayListRelay), }; } -function convertFromRelayListRelay(relay: grpcTypes.Relay.AsObject): IRelayListHostname { +function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname { + const relayObject = relay.toObject(); + + let daita = false; + if (relayObject.endpointType === grpcTypes.Relay.RelayType.WIREGUARD) { + const endpointDataU8 = relay.getEndpointData()?.getValue_asU8(); + if (endpointDataU8) { + daita = grpcTypes.WireguardRelayEndpointData.deserializeBinary(endpointDataU8).getDaita(); + } + } + return { - ...relay, - endpointType: convertFromRelayType(relay.endpointType), + ...relayObject, + endpointType: convertFromRelayType(relayObject.endpointType), + daita, }; } @@ -1333,6 +1352,7 @@ function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObjec quantumResistant: convertFromQuantumResistantState( tunnelOptions.wireguard?.quantumResistant?.state, ), + daita: tunnelOptions.wireguard!.daita, }, generic: { enableIpv6: tunnelOptions.generic!.enableIpv6, diff --git a/gui/src/main/settings.ts b/gui/src/main/settings.ts index 8bf60d9108e2..22238c72c459 100644 --- a/gui/src/main/settings.ts +++ b/gui/src/main/settings.ts @@ -107,6 +107,9 @@ export default class Settings implements Readonly { const settings = await fs.readFile(path); return this.daemonRpc.applyJsonSettings(settings.toString()); }); + IpcMainEventChannel.settings.handleSetDaitaSettings((daitaSettings) => { + return this.daemonRpc.setDaitaSettings(daitaSettings); + }); IpcMainEventChannel.guiSettings.handleSetEnableSystemNotifications((flag: boolean) => { this.guiSettings.enableSystemNotifications = flag; diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index a8c1901ae85a..92c4cb56989f 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -16,6 +16,7 @@ import { IAccountData, IAppVersionInfo, ICustomList, + IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -340,6 +341,8 @@ export default class AppRenderer { IpcRendererEventChannel.windowsSplitTunneling.forgetManuallyAddedApplication(application); public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) => IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings); + public setDaitaSettings = (daitaSettings: IDaitaSettings) => + IpcRendererEventChannel.settings.setDaitaSettings(daitaSettings); public collectProblemReport = (toRedact: string | undefined) => IpcRendererEventChannel.problemReport.collectLogs(toRedact); public viewLog = (path: string) => IpcRendererEventChannel.problemReport.viewLog(path); @@ -812,6 +815,7 @@ export default class AppRenderer { reduxSettings.updateWireguardQuantumResistant( newSettings.tunnelOptions.wireguard.quantumResistant, ); + reduxSettings.updateWireguardDaita(newSettings.tunnelOptions.wireguard.daita); reduxSettings.updateBridgeState(newSettings.bridgeState); reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions); diff --git a/gui/src/renderer/components/BetaLabel.tsx b/gui/src/renderer/components/BetaLabel.tsx deleted file mode 100644 index ca19a9ef4b7c..000000000000 --- a/gui/src/renderer/components/BetaLabel.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import styled from 'styled-components'; - -import { colors } from '../../config.json'; -import { messages } from '../../shared/gettext'; - -const StyledBetaLabel = styled.span({ - display: 'inline-block', - fontFamily: 'Open Sans', - color: colors.blue, - fontSize: '13px', - fontWeight: 800, - lineHeight: '20px', - padding: '2px 0', - background: colors.yellow, - borderRadius: '5px', - width: '50px', - textAlign: 'center', -}); - -interface IBetaLabelProps { - className?: string; -} - -export default function BetaLabel(props: IBetaLabelProps) { - return {messages.gettext('BETA')}; -} diff --git a/gui/src/renderer/components/ConnectionPanel.tsx b/gui/src/renderer/components/ConnectionPanel.tsx index a99140bb431d..4c709b7204ed 100644 --- a/gui/src/renderer/components/ConnectionPanel.tsx +++ b/gui/src/renderer/components/ConnectionPanel.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; -import { colors } from '../../config.json'; +import { colors, strings } from '../../config.json'; import { EndpointObfuscationType, ProxyType, @@ -48,6 +48,7 @@ interface IProps { bridgeInfo?: IBridgeData; outAddress?: IOutAddress; obfuscationEndpoint?: IObfuscationData; + daita: boolean; onToggle: () => void; className?: string; } @@ -146,13 +147,15 @@ export default class ConnectionPanel extends React.Component { } private hostnameLine() { + let hostname = ''; + if (this.props.hostname && this.props.bridgeHostname) { - return sprintf(messages.pgettext('connection-info', '%(relay)s via %(entry)s'), { + hostname = sprintf(messages.pgettext('connection-info', '%(relay)s via %(entry)s'), { relay: this.props.hostname, entry: this.props.bridgeHostname, }); } else if (this.props.hostname && this.props.entryHostname) { - return sprintf( + hostname = sprintf( // TRANSLATORS: The hostname line displayed below the country on the main screen // TRANSLATORS: Available placeholders: // TRANSLATORS: %(relay)s - the relay hostname @@ -163,13 +166,31 @@ export default class ConnectionPanel extends React.Component { entry: this.props.entryHostname, }, ); + } else if (this.props.bridgeInfo?.ip) { + hostname = sprintf(messages.pgettext('connection-info', '%(relay)s via %(entry)s'), { + relay: this.props.hostname, + }); } else if (this.props.bridgeInfo !== undefined) { - return sprintf(messages.pgettext('connection-info', '%(relay)s via Custom bridge'), { + hostname = sprintf(messages.pgettext('connection-info', '%(relay)s via Custom bridge'), { relay: this.props.hostname, }); - } else { - return this.props.hostname || ''; + } else if (this.props.hostname) { + hostname = this.props.hostname; + } + + if (hostname !== '' && this.props.daita) { + hostname = sprintf( + // TRANSLATORS: %(hostname)s - The current server the app is connected to, e.g. "se-got-wg-001 using DAITA" + // TRANSLATORS: %(daita)s - Will be replaced with "DAITA" + messages.pgettext('connection-info', '%(hostname)s using %(daita)s'), + { + hostname, + daita: strings.daita, + }, + ); } + + return hostname; } private transportLine() { diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 7cd93b98bd2e..d315485659ad 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -22,7 +22,7 @@ import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; import Selector, { SelectorItem, SelectorWithCustomItem } from './cell/Selector'; -import { InfoIcon } from './InfoButton'; +import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; @@ -34,6 +34,7 @@ import { TitleBarItem, } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; +import YellowLabel from './YellowLabel'; const MIN_WIREGUARD_MTU_VALUE = 1280; const MAX_WIREGUARD_MTU_VALUE = 1420; @@ -44,22 +45,14 @@ function mapPortToSelectorItem(value: number): SelectorItem { return { label: value.toString(), value }; } -export const StyledContent = styled.div({ +const StyledContent = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, marginBottom: '2px', }); -export const StyledCellIcon = styled(Cell.UntintedIcon)({ - marginRight: '8px', -}); - -export const StyledInfoIcon = styled(InfoIcon)({ - marginRight: '16px', -}); - -export const StyledSelectorContainer = styled.div({ +const StyledSelectorContainer = styled.div({ flex: 0, }); @@ -107,6 +100,12 @@ export default function WireguardSettings() { + {window.env.platform === 'win32' && ( + + + + )} + @@ -558,6 +557,95 @@ function MtuSetting() { ); } +function DaitaSettings() { + const { setDaitaSettings } = useAppContext(); + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + + const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); + + const setDaita = useCallback((value: boolean) => { + if (value) { + showConfirmationDialog(); + } else { + void setDaitaSettings({ enabled: value }); + } + }, []); + + const confirmDaita = useCallback(() => { + void setDaitaSettings({ enabled: true }); + hideConfirmationDialog(); + }, []); + + return ( + <> + + + + + {strings.daita} + {messages.gettext('BETA')} + + + + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + '%(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.', + ), + { daita: strings.daita, daitaFull: strings.daitaFull }, + )} + + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + 'Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s.', + ), + { daita: strings.daita }, + )} + + + + + + + + + {messages.gettext('Enable anyway')} + , + + {messages.gettext('Back')} + , + ]} + close={hideConfirmationDialog}> + + { + // TRANSLATORS: Warning text in a dialog that is displayed after a setting is toggled. + messages.pgettext( + 'wireguard-settings-view', + "This feature isn't available on all servers. You might need to change location after enabling.", + ) + } + + + {sprintf( + messages.pgettext( + 'wireguard-settings-view', + 'Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %(daita)s.', + ), + { daita: strings.daita }, + )} + + + + ); +} + function QuantumResistantSetting() { const { setWireguardQuantumResistant } = useAppContext(); const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); diff --git a/gui/src/renderer/components/YellowLabel.tsx b/gui/src/renderer/components/YellowLabel.tsx new file mode 100644 index 000000000000..55059e223f31 --- /dev/null +++ b/gui/src/renderer/components/YellowLabel.tsx @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +import { colors } from '../../config.json'; + +export default styled.span({ + display: 'inline-block', + fontFamily: 'Open Sans', + color: colors.blue, + fontSize: '12px', + fontWeight: 800, + lineHeight: '20px', + padding: '1px 8px', + marginLeft: '8px', + background: colors.yellow, + borderRadius: '5px', + textAlign: 'center', + verticalAlign: 'middle', +}); diff --git a/gui/src/renderer/components/cell/Label.tsx b/gui/src/renderer/components/cell/Label.tsx index 729b7dc31d74..f74e57b39c19 100644 --- a/gui/src/renderer/components/cell/Label.tsx +++ b/gui/src/renderer/components/cell/Label.tsx @@ -8,6 +8,8 @@ import { CellButton } from './CellButton'; import { CellDisabledContext } from './Container'; const StyledLabel = styled.div<{ disabled: boolean }>(buttonText, (props) => ({ + display: 'flex', + alignItems: 'center', margin: '10px 0', flex: 1, color: props.disabled ? colors.white40 : colors.white, diff --git a/gui/src/renderer/components/select-location/RelayListContext.tsx b/gui/src/renderer/components/select-location/RelayListContext.tsx index 86dd7e54676a..13e2dc28e8d1 100644 --- a/gui/src/renderer/components/select-location/RelayListContext.tsx +++ b/gui/src/renderer/components/select-location/RelayListContext.tsx @@ -4,6 +4,7 @@ import { compareRelayLocation, RelayLocation } from '../../../shared/daemon-rpc- import { EndpointType, filterLocations, + filterLocationsByDaita, filterLocationsByEndPointType, getLocationsExpandedBySearch, searchForLocations, @@ -60,6 +61,7 @@ interface RelayListContextProviderProps { export function RelayListContextProvider(props: RelayListContextProviderProps) { const { locationType, searchTerm } = useSelectLocationContext(); + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); const fullRelayList = useSelector((state) => state.settings.relayLocations); const relaySettings = useNormalRelaySettings(); @@ -71,15 +73,21 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { return filterLocationsByEndPointType(fullRelayList, endpointType, relaySettings); }, [fullRelayList, locationType, relaySettings?.tunnelProtocol]); + const relayListForDaita = useMemo(() => { + return filterLocationsByDaita( + relayListForEndpointType, + daita, + locationType, + relaySettings?.tunnelProtocol ?? 'any', + relaySettings?.wireguard.useMultihop ?? false, + ); + }, [daita, relayListForEndpointType]); + // Filters the relays to only keep the relays matching the currently selected filters, e.g. // ownership and providers const relayListForFilters = useMemo(() => { - return filterLocations( - relayListForEndpointType, - relaySettings?.ownership, - relaySettings?.providers, - ); - }, [relaySettings?.ownership, relaySettings?.providers, relayListForEndpointType]); + return filterLocations(relayListForDaita, relaySettings?.ownership, relaySettings?.providers); + }, [relaySettings?.ownership, relaySettings?.providers, relayListForDaita]); // Filters the relays based on the provided search term const relayListForSearch = useMemo(() => { diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx index 5ba02039395f..3f3535225ed2 100644 --- a/gui/src/renderer/components/select-location/SelectLocation.tsx +++ b/gui/src/renderer/components/select-location/SelectLocation.tsx @@ -5,7 +5,7 @@ import { colors } from '../../../config.json'; import { Ownership } from '../../../shared/daemon-rpc-types'; import { messages } from '../../../shared/gettext'; import { useRelaySettingsUpdater } from '../../lib/constraint-updater'; -import { filterSpecialLocations } from '../../lib/filter-locations'; +import { daitaFilterActive, filterSpecialLocations } from '../../lib/filter-locations'; import { useHistory } from '../../lib/history'; import { formatHtml } from '../../lib/html-formatter'; import { RoutePath } from '../../lib/routes'; @@ -71,6 +71,13 @@ export default function SelectLocation() { const ownership = relaySettings?.ownership ?? Ownership.any; const providers = relaySettings?.providers ?? []; const filteredProviders = useFilteredProviders(providers, ownership); + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const showDaitaFilter = daitaFilterActive( + daita, + locationType, + relaySettings?.tunnelProtocol ?? 'any', + relaySettings?.wireguard.useMultihop ?? false, + ); const [searchValue, setSearchValue] = useState(''); @@ -122,7 +129,7 @@ export default function SelectLocation() { const showOwnershipFilter = ownership !== Ownership.any; const showProvidersFilter = providers.length > 0; - const showFilters = showOwnershipFilter || showProvidersFilter; + const showFilters = showOwnershipFilter || showProvidersFilter || showDaitaFilter; return ( @@ -220,6 +227,15 @@ export default function SelectLocation() { )} + + {showDaitaFilter && ( + + {sprintf( + messages.pgettext('select-location-view', 'Setting: %(settingName)s'), + { settingName: 'DAITA' }, + )} + + )} )} diff --git a/gui/src/renderer/containers/ConnectionPanelContainer.tsx b/gui/src/renderer/containers/ConnectionPanelContainer.tsx index 5311f1a8e424..b12902bc5d80 100644 --- a/gui/src/renderer/containers/ConnectionPanelContainer.tsx +++ b/gui/src/renderer/containers/ConnectionPanelContainer.tsx @@ -94,6 +94,11 @@ const mapStateToProps = (state: IReduxState) => { ? tunnelEndpointToObfuscationEndpoint(status.details.endpoint) : undefined; + const daita = + ((status.state === 'connected' || status.state === 'connecting') && + status.details?.endpoint.daita) ?? + false; + return { isOpen: state.userInterface.connectionPanelVisible, hostname: state.connection.hostname, @@ -104,6 +109,7 @@ const mapStateToProps = (state: IReduxState) => { bridgeInfo, outAddress, obfuscationEndpoint, + daita, }; }; diff --git a/gui/src/renderer/lib/filter-locations.ts b/gui/src/renderer/lib/filter-locations.ts index 63ca56cbf253..78126f1fb417 100644 --- a/gui/src/renderer/lib/filter-locations.ts +++ b/gui/src/renderer/lib/filter-locations.ts @@ -1,6 +1,13 @@ -import { Ownership, RelayEndpointType, RelayLocation } from '../../shared/daemon-rpc-types'; +import { + LiftedConstraint, + Ownership, + RelayEndpointType, + RelayLocation, + TunnelProtocol, +} from '../../shared/daemon-rpc-types'; import { relayLocations } from '../../shared/gettext'; import { + LocationType, RelayLocationCityWithVisibility, RelayLocationCountryWithVisibility, RelayLocationRelayWithVisibility, @@ -27,6 +34,30 @@ export function filterLocationsByEndPointType( return filterLocationsImpl(locations, getTunnelProtocolFilter(endpointType, relaySettings)); } +export function filterLocationsByDaita( + locations: IRelayLocationCountryRedux[], + daita: boolean, + locationType: LocationType, + tunnelProtocol: LiftedConstraint, + multihop: boolean, +): IRelayLocationCountryRedux[] { + return daitaFilterActive(daita, locationType, tunnelProtocol, multihop) + ? filterLocationsImpl(locations, (relay: IRelayLocationRelayRedux) => relay.daita) + : locations; +} + +export function daitaFilterActive( + daita: boolean, + locationType: LocationType, + tunnelProtocol: LiftedConstraint, + multihop: boolean, +) { + const isEntry = multihop + ? locationType === LocationType.entry + : locationType === LocationType.exit; + return daita && isEntry && tunnelProtocol !== 'openvpn'; +} + export function filterLocations( locations: IRelayLocationCountryRedux[], ownership?: Ownership, diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index aa4e460e6fa0..ba6e4ce5a8e0 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -4,6 +4,7 @@ import { ApiAccessMethodSettings, BridgeState, CustomLists, + IDaitaSettings, IDnsOptions, IWireguardEndpointData, ObfuscationSettings, @@ -77,6 +78,11 @@ export interface IUpdateWireguardQuantumResistantAction { quantumResistant?: boolean; } +export interface IUpdateWireguardDaitaAction { + type: 'UPDATE_WIREGUARD_DAITA'; + daita?: IDaitaSettings; +} + export interface IUpdateAutoStartAction { type: 'UPDATE_AUTO_START'; autoStart: boolean; @@ -136,6 +142,7 @@ export type SettingsAction = | IUpdateOpenVpnMssfixAction | IUpdateWireguardMtuAction | IUpdateWireguardQuantumResistantAction + | IUpdateWireguardDaitaAction | IUpdateAutoStartAction | IUpdateDnsOptionsAction | IUpdateSplitTunnelingStateAction @@ -245,6 +252,13 @@ function updateWireguardQuantumResistant( }; } +function updateWireguardDaita(daita?: IDaitaSettings): IUpdateWireguardDaitaAction { + return { + type: 'UPDATE_WIREGUARD_DAITA', + daita, + }; +} + function updateAutoStart(autoStart: boolean): IUpdateAutoStartAction { return { type: 'UPDATE_AUTO_START', @@ -326,6 +340,7 @@ export default { updateOpenVpnMssfix, updateWireguardMtu, updateWireguardQuantumResistant, + updateWireguardDaita, updateAutoStart, updateDnsOptions, updateSplitTunnelingState, diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 3c502ec103cb..18873eea2002 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -7,6 +7,7 @@ import { BridgeType, CustomLists, CustomProxy, + IDaitaSettings, IDnsOptions, IpVersion, IWireguardEndpointData, @@ -75,6 +76,7 @@ export interface IRelayLocationRelayRedux { owned: boolean; weight: number; endpointType: RelayEndpointType; + daita: boolean; } export interface IRelayLocationCityRedux { @@ -109,6 +111,7 @@ export interface ISettingsReduxState { wireguard: { mtu?: number; quantumResistant?: boolean; + daita?: IDaitaSettings; }; dns: IDnsOptions; splitTunneling: boolean; @@ -271,6 +274,14 @@ export default function ( quantumResistant: action.quantumResistant, }, }; + case 'UPDATE_WIREGUARD_DAITA': + return { + ...state, + wireguard: { + ...state.wireguard, + daita: action.daita, + }, + }; case 'UPDATE_AUTO_START': return { diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 738eef5e9561..761dd01a4cac 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -143,6 +143,7 @@ export interface ITunnelEndpoint { proxy?: IProxyEndpoint; obfuscationEndpoint?: IObfuscationEndpoint; entryEndpoint?: IEndpoint; + daita: boolean; } export interface IEndpoint { @@ -309,6 +310,7 @@ export interface IRelayListHostname { weight: number; owned: boolean; endpointType: RelayEndpointType; + daita: boolean; } export type RelayEndpointType = 'wireguard' | 'openvpn' | 'bridge'; @@ -320,6 +322,7 @@ export interface ITunnelOptions { wireguard: { mtu?: number; quantumResistant?: boolean; + daita?: IDaitaSettings; }; generic: { enableIpv6: boolean; @@ -503,6 +506,10 @@ export interface RelayOverride { ipv6AddrIn?: string; } +export interface IDaitaSettings { + enabled: boolean; +} + export function parseSocketAddress(socketAddrStr: string): ISocketAddress { const re = new RegExp(/(.+):(\d+)$/); const matches = socketAddrStr.match(re); diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index d90957764e00..561ec924a094 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -14,6 +14,7 @@ import { IAccountData, IAppVersionInfo, ICustomList, + IDaitaSettings, IDevice, IDeviceRemoval, IDnsOptions, @@ -192,6 +193,7 @@ export const ipcSchema = { testApiAccessMethodById: invoke(), testCustomApiAccessMethod: invoke(), clearAllRelayOverrides: invoke(), + setDaitaSettings: invoke(), }, guiSettings: { '': notifyRenderer(), diff --git a/gui/tasks/distribution.js b/gui/tasks/distribution.js index 99c8fb87efb1..7087d71301ed 100644 --- a/gui/tasks/distribution.js +++ b/gui/tasks/distribution.js @@ -152,6 +152,7 @@ const config = { from: distAssets('binaries/x86_64-pc-windows-msvc/wireguard-nt/mullvad-wireguard.dll'), to: '.', }, + { from: distAssets('maybenot_machines'), to: '.' }, ], }, diff --git a/gui/test/e2e/mocked/tunnel-state.spec.ts b/gui/test/e2e/mocked/tunnel-state.spec.ts index db919e09d31a..b4de4058410b 100644 --- a/gui/test/e2e/mocked/tunnel-state.spec.ts +++ b/gui/test/e2e/mocked/tunnel-state.spec.ts @@ -69,6 +69,7 @@ test('App should show connected tunnel state', async () => { protocol: 'tcp', quantumResistant: false, tunnelType: 'wireguard', + daita: false, }; await util.sendMockIpcResponse({ channel: 'tunnel-', diff --git a/gui/test/e2e/setup/main.ts b/gui/test/e2e/setup/main.ts index c0114494d9be..78da7f59b5a7 100644 --- a/gui/test/e2e/setup/main.ts +++ b/gui/test/e2e/setup/main.ts @@ -94,6 +94,7 @@ class ApplicationMain { weight: 0, owned: true, endpointType: 'wireguard', + daita: false, }, ], }, diff --git a/mullvad-api/src/relay_list.rs b/mullvad-api/src/relay_list.rs index deaf29ef10d6..73b387a8d8c0 100644 --- a/mullvad-api/src/relay_list.rs +++ b/mullvad-api/src/relay_list.rs @@ -303,6 +303,8 @@ struct WireGuardRelay { #[serde(flatten)] relay: Relay, public_key: wireguard::PublicKey, + #[serde(default)] + daita: bool, } impl WireGuardRelay { @@ -312,6 +314,7 @@ impl WireGuardRelay { location, relay_list::RelayEndpointData::Wireguard(relay_list::WireguardRelayEndpointData { public_key: self.public_key, + daita: self.daita, }), ) } diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 7ef60d758cc9..f022402a8316 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -542,6 +542,8 @@ impl Relay { allowed_ips: all_of_the_internet(), endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), psk: None, + #[cfg(target_os = "windows")] + constant_packet_size: false, }, exit_peer: None, ipv4_gateway, diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 19d5c1a3c956..77338ee336b8 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -1,6 +1,8 @@ use anyhow::Result; use clap::Subcommand; use mullvad_management_interface::MullvadProxyClient; +#[cfg(target_os = "windows")] +use mullvad_types::wireguard::DaitaSettings; use mullvad_types::{ constraints::Constraint, wireguard::{QuantumResistantState, RotationInterval, DEFAULT_ROTATION_INTERVAL}, @@ -38,6 +40,10 @@ pub enum TunnelOptions { /// Configure quantum-resistant key exchange #[arg(long)] quantum_resistant: Option, + /// Configure whether to enable DAITA + #[cfg(target_os = "windows")] + #[arg(long)] + daita: Option, /// The key rotation interval. Number of hours, or 'any' #[arg(long)] rotation_interval: Option>, @@ -95,6 +101,9 @@ impl Tunnel { tunnel_options.wireguard.quantum_resistant, ); + #[cfg(target_os = "windows")] + print_option!("DAITA", tunnel_options.wireguard.daita.enabled); + let key = rpc.get_wireguard_key().await?; print_option!("Public key", key.key,); print_option!(format_args!( @@ -129,10 +138,20 @@ impl Tunnel { TunnelOptions::Wireguard { mtu, quantum_resistant, + #[cfg(target_os = "windows")] + daita, rotation_interval, rotate_key, } => { - Self::handle_wireguard(mtu, quantum_resistant, rotation_interval, rotate_key).await + Self::handle_wireguard( + mtu, + quantum_resistant, + #[cfg(target_os = "windows")] + daita, + rotation_interval, + rotate_key, + ) + .await } TunnelOptions::Ipv6 { state } => Self::handle_ipv6(state).await, } @@ -159,6 +178,7 @@ impl Tunnel { async fn handle_wireguard( mtu: Option>, quantum_resistant: Option, + #[cfg(target_os = "windows")] daita: Option, rotation_interval: Option>, rotate_key: Option, ) -> Result<()> { @@ -174,6 +194,13 @@ impl Tunnel { println!("Quantum resistant setting has been updated"); } + #[cfg(target_os = "windows")] + if let Some(daita) = daita { + rpc.set_daita_settings(DaitaSettings { enabled: *daita }) + .await?; + println!("DAITA setting has been updated"); + } + if let Some(interval) = rotation_interval { match interval { Constraint::Only(interval) => { diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs index e605efbe3bc5..0d6ea0b0c034 100644 --- a/mullvad-cli/src/format.rs +++ b/mullvad-cli/src/format.rs @@ -174,6 +174,17 @@ fn format_relay_connection( "\nQuantum resistant tunnel: no" }; + #[cfg(target_os = "windows")] + let daita = if !verbose { + "" + } else if endpoint.daita { + "\nDAITA: yes" + } else { + "\nDAITA: no" + }; + #[cfg(not(target_os = "windows"))] + let daita = ""; + let mut bridge_type = String::new(); let mut obfuscator_type = String::new(); if verbose { @@ -186,7 +197,7 @@ fn format_relay_connection( } format!( - "{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{quantum_resistant}{bridge_type}{obfuscator_type}", + "{exit_endpoint}{first_hop}{bridge}{obfuscator}{tunnel_type}{quantum_resistant}{daita}{bridge_type}{obfuscator_type}", first_hop = first_hop.unwrap_or_default(), bridge = bridge.unwrap_or_default(), obfuscator = obfuscator.unwrap_or_default(), diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 365e82efeba1..d0fad0493d76 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -37,9 +37,13 @@ use futures::{ StreamExt, }; use geoip::GeoIpHandler; -use mullvad_relay_selector::{RelaySelector, SelectorConfig}; +use mullvad_relay_selector::{ + AdditionalRelayConstraints, AdditionalWireguardConstraints, RelaySelector, SelectorConfig, +}; #[cfg(target_os = "android")] use mullvad_types::account::{PlayPurchase, PlayPurchasePaymentToken}; +#[cfg(target_os = "windows")] +use mullvad_types::wireguard::DaitaSettings; use mullvad_types::{ access_method::{AccessMethod, AccessMethodSetting}, account::{AccountData, AccountToken, VoucherSubmission}, @@ -256,6 +260,9 @@ pub enum DaemonCommand { SetEnableIpv6(ResponseTx<(), settings::Error>, bool), /// Set whether to enable PQ PSK exchange in the tunnel SetQuantumResistantTunnel(ResponseTx<(), settings::Error>, QuantumResistantState), + /// Set DAITA settings for the tunnel + #[cfg(target_os = "windows")] + SetDaitaSettings(ResponseTx<(), settings::Error>, DaitaSettings), /// Set DNS options or servers to use SetDnsOptions(ResponseTx<(), settings::Error>, DnsOptions), /// Set override options to use for a given relay @@ -1242,6 +1249,10 @@ where self.on_set_quantum_resistant_tunnel(tx, quantum_resistant_state) .await } + #[cfg(target_os = "windows")] + SetDaitaSettings(tx, daita_settings) => { + self.on_set_daita_settings(tx, daita_settings).await + } SetDnsOptions(tx, dns_servers) => self.on_set_dns_options(tx, dns_servers).await, SetRelayOverride(tx, relay_override) => { self.on_set_relay_override(tx, relay_override).await @@ -2259,6 +2270,40 @@ where } } + #[cfg(target_os = "windows")] + async fn on_set_daita_settings( + &mut self, + tx: ResponseTx<(), settings::Error>, + daita_settings: DaitaSettings, + ) { + match self + .settings + .update(|settings| settings.tunnel_options.wireguard.daita = daita_settings) + .await + { + Ok(settings_changed) => { + Self::oneshot_send(tx, Ok(()), "set_daita_settings response"); + if settings_changed { + self.parameters_generator + .set_tunnel_options(&self.settings.tunnel_options) + .await; + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector + .set_config(new_selector_config(&self.settings)); + if self.get_target_tunnel_type() == Some(TunnelType::Wireguard) { + log::info!("Reconnecting because DAITA settings changed"); + self.reconnect_tunnel(); + } + } + } + Err(e) => { + log::error!("{}", e.display_chain_with_msg("Unable to save settings")); + Self::oneshot_send(tx, Err(e), "set_daita_settings response"); + } + } + } + async fn on_set_dns_options( &mut self, tx: ResponseTx<(), settings::Error>, @@ -2780,8 +2825,18 @@ impl DaemonShutdownHandle { } fn new_selector_config(settings: &Settings) -> SelectorConfig { + let additional_constraints = AdditionalRelayConstraints { + wireguard: AdditionalWireguardConstraints { + #[cfg(target_os = "windows")] + daita: settings.tunnel_options.wireguard.daita.enabled, + #[cfg(not(target_os = "windows"))] + daita: false, + }, + }; + SelectorConfig { relay_settings: settings.relay_settings.clone(), + additional_constraints, bridge_state: settings.bridge_state, bridge_settings: settings.bridge_settings.clone(), obfuscation_settings: settings.obfuscation_settings.clone(), diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 55573f87a7d1..ce203e7ba8d6 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -325,6 +325,25 @@ impl ManagementService for ManagementServiceImpl { Ok(Response::new(())) } + #[cfg(target_os = "windows")] + async fn set_daita_settings( + &self, + request: Request, + ) -> ServiceResult<()> { + let state = mullvad_types::wireguard::DaitaSettings::from(request.into_inner()); + + log::debug!("set_daita_settings({state:?})"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetDaitaSettings(tx, state))?; + self.wait_for_result(rx).await?.map(Response::new)?; + Ok(Response::new(())) + } + + #[cfg(not(target_os = "windows"))] + async fn set_daita_settings(&self, _: Request) -> ServiceResult<()> { + Ok(Response::new(())) + } + #[cfg(not(target_os = "android"))] async fn set_dns_options(&self, request: Request) -> ServiceResult<()> { let options = DnsOptions::try_from(request.into_inner()).map_err(map_protobuf_type_err)?; diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 8af34aa8afe3..31d39db3069b 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -43,6 +43,7 @@ service ManagementService { rpc SetWireguardMtu(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} rpc SetEnableIpv6(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} rpc SetQuantumResistantTunnel(QuantumResistantState) returns (google.protobuf.Empty) {} + rpc SetDaitaSettings(DaitaSettings) returns (google.protobuf.Empty) {} rpc SetDnsOptions(DnsOptions) returns (google.protobuf.Empty) {} rpc SetRelayOverride(RelayOverride) returns (google.protobuf.Empty) {} rpc ClearAllRelayOverrides(google.protobuf.Empty) returns (google.protobuf.Empty) {} @@ -220,6 +221,7 @@ message TunnelEndpoint { ObfuscationEndpoint obfuscation = 6; Endpoint entry_endpoint = 7; TunnelMetadata tunnel_metadata = 8; + bool daita = 9; } enum ObfuscationType { @@ -494,12 +496,15 @@ message QuantumResistantState { State state = 1; } +message DaitaSettings { bool enabled = 1; } + message TunnelOptions { message OpenvpnOptions { optional uint32 mssfix = 1; } message WireguardOptions { optional uint32 mtu = 1; google.protobuf.Duration rotation_interval = 2; QuantumResistantState quantum_resistant = 4; + DaitaSettings daita = 5; } message GenericOptions { bool enable_ipv6 = 1; } @@ -584,7 +589,10 @@ message Relay { Location location = 11; } -message WireguardRelayEndpointData { bytes public_key = 1; } +message WireguardRelayEndpointData { + bytes public_key = 1; + bool daita = 2; +} message Location { string country = 1; diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index 8cf3ac495d2e..04304a19ec4d 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -2,6 +2,8 @@ use crate::types; use futures::{Stream, StreamExt}; +#[cfg(target_os = "windows")] +use mullvad_types::wireguard::DaitaSettings; use mullvad_types::{ access_method::{self, AccessMethod, AccessMethodSetting}, account::{AccountData, AccountToken, VoucherSubmission}, @@ -344,6 +346,16 @@ impl MullvadProxyClient { Ok(()) } + #[cfg(target_os = "windows")] + pub async fn set_daita_settings(&mut self, settings: DaitaSettings) -> Result<()> { + let settings = types::DaitaSettings::from(settings); + self.0 + .set_daita_settings(settings) + .await + .map_err(Error::Rpc)?; + Ok(()) + } + pub async fn set_dns_options(&mut self, options: DnsOptions) -> Result<()> { let options = types::DnsOptions::from(&options); self.0.set_dns_options(options).await.map_err(Error::Rpc)?; diff --git a/mullvad-management-interface/src/types/conversions/custom_tunnel.rs b/mullvad-management-interface/src/types/conversions/custom_tunnel.rs index 8a4408ec83d3..2445ec3292d1 100644 --- a/mullvad-management-interface/src/types/conversions/custom_tunnel.rs +++ b/mullvad-management-interface/src/types/conversions/custom_tunnel.rs @@ -91,6 +91,8 @@ impl TryFrom for mullvad_types::ConnectionConfig { allowed_ips, endpoint, psk: None, + #[cfg(target_os = "windows")] + constant_packet_size: false, }, exit_peer: None, ipv4_gateway, diff --git a/mullvad-management-interface/src/types/conversions/net.rs b/mullvad-management-interface/src/types/conversions/net.rs index 80648cbf8dcb..3557a6a636f9 100644 --- a/mullvad-management-interface/src/types/conversions/net.rs +++ b/mullvad-management-interface/src/types/conversions/net.rs @@ -40,6 +40,10 @@ impl From for proto::TunnelEndpoint { tunnel_metadata: endpoint .tunnel_interface .map(|tunnel_interface| proto::TunnelMetadata { tunnel_interface }), + #[cfg(target_os = "windows")] + daita: endpoint.daita, + #[cfg(not(target_os = "windows"))] + daita: false, } } } @@ -123,6 +127,8 @@ impl TryFrom for talpid_types::net::TunnelEndpoint { tunnel_interface: endpoint .tunnel_metadata .map(|tunnel_metadata| tunnel_metadata.tunnel_interface), + #[cfg(target_os = "windows")] + daita: endpoint.daita, }) } } diff --git a/mullvad-management-interface/src/types/conversions/relay_list.rs b/mullvad-management-interface/src/types/conversions/relay_list.rs index 32aee834afe1..4e0a3637022a 100644 --- a/mullvad-management-interface/src/types/conversions/relay_list.rs +++ b/mullvad-management-interface/src/types/conversions/relay_list.rs @@ -122,6 +122,7 @@ impl From for proto::Relay { "mullvad_daemon.management_interface/WireguardRelayEndpointData", proto::WireguardRelayEndpointData { public_key: data.public_key.as_bytes().to_vec(), + daita: data.daita, }, )), _ => None, @@ -236,6 +237,7 @@ impl TryFrom for mullvad_types::relay_list::Relay { MullvadEndpointData::Wireguard( mullvad_types::relay_list::WireguardRelayEndpointData { public_key: bytes_to_pubkey(&data.public_key)?, + daita: data.daita, }, ) } diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index a4d63131584c..857f32d99102 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -97,6 +97,10 @@ impl From<&mullvad_types::settings::TunnelOptions> for proto::TunnelOptions { .expect("Failed to convert std::time::Duration to prost_types::Duration for tunnel_options.wireguard.rotation_interval") }), quantum_resistant: Some(proto::QuantumResistantState::from(options.wireguard.quantum_resistant)), + #[cfg(target_os = "windows")] + daita: Some(proto::DaitaSettings::from(options.wireguard.daita.clone())), + #[cfg(not(target_os = "windows"))] + daita: None, }), generic: Some(proto::tunnel_options::GenericOptions { enable_ipv6: options.generic.enable_ipv6, @@ -282,6 +286,13 @@ impl TryFrom for mullvad_types::settings::TunnelOptions { .ok_or(FromProtobufTypeError::InvalidArgument( "missing quantum resistant state", ))??, + #[cfg(target_os = "windows")] + daita: wireguard_options + .daita + .map(mullvad_types::wireguard::DaitaSettings::from) + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing daita settings", + ))?, }, generic: net::GenericTunnelOptions { enable_ipv6: generic_options.enable_ipv6, diff --git a/mullvad-management-interface/src/types/conversions/wireguard.rs b/mullvad-management-interface/src/types/conversions/wireguard.rs index f35a8c72160e..4a4341339cfc 100644 --- a/mullvad-management-interface/src/types/conversions/wireguard.rs +++ b/mullvad-management-interface/src/types/conversions/wireguard.rs @@ -71,3 +71,21 @@ impl TryFrom for mullvad_types::wireguard::Quantum } } } + +#[cfg(target_os = "windows")] +impl From for proto::DaitaSettings { + fn from(settings: mullvad_types::wireguard::DaitaSettings) -> Self { + proto::DaitaSettings { + enabled: settings.enabled, + } + } +} + +#[cfg(target_os = "windows")] +impl From for mullvad_types::wireguard::DaitaSettings { + fn from(settings: proto::DaitaSettings) -> Self { + mullvad_types::wireguard::DaitaSettings { + enabled: settings.enabled, + } + } +} diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index 9ac49d0b1f63..73c709a844a7 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -10,6 +10,7 @@ mod relay_selector; pub use error::Error; pub use relay_selector::detailer; pub use relay_selector::{ - query, GetRelay, RelaySelector, RuntimeParameters, SelectedBridge, SelectedObfuscator, - SelectorConfig, WireguardConfig, RETRY_ORDER, + query, AdditionalRelayConstraints, AdditionalWireguardConstraints, GetRelay, RelaySelector, + RuntimeParameters, SelectedBridge, SelectedObfuscator, SelectorConfig, WireguardConfig, + RETRY_ORDER, }; diff --git a/mullvad-relay-selector/src/relay_selector/detailer.rs b/mullvad-relay-selector/src/relay_selector/detailer.rs index 807903eb39ba..3dc1cc903e7d 100644 --- a/mullvad-relay-selector/src/relay_selector/detailer.rs +++ b/mullvad-relay-selector/src/relay_selector/detailer.rs @@ -84,6 +84,9 @@ fn wireguard_singlehop_endpoint( allowed_ips: all_of_the_internet(), // This will be filled in later, not the relay selector's problem psk: None, + // This will be filled in later + #[cfg(target_os = "windows")] + constant_packet_size: false, }; Ok(MullvadWireguardEndpoint { peer: peer_config, @@ -122,6 +125,9 @@ fn wireguard_multihop_endpoint( allowed_ips: all_of_the_internet(), // This will be filled in later, not the relay selector's problem psk: None, + // This will be filled in later + #[cfg(target_os = "windows")] + constant_packet_size: false, }; let entry_endpoint = { @@ -137,6 +143,9 @@ fn wireguard_multihop_endpoint( allowed_ips: vec![IpNetwork::from(exit.endpoint.ip())], // This will be filled in later psk: None, + // This will be filled in later + #[cfg(target_os = "windows")] + constant_packet_size: false, }; Ok(MullvadWireguardEndpoint { diff --git a/mullvad-relay-selector/src/relay_selector/matcher.rs b/mullvad-relay-selector/src/relay_selector/matcher.rs index 2987d8965abf..f6c244d3f255 100644 --- a/mullvad-relay-selector/src/relay_selector/matcher.rs +++ b/mullvad-relay-selector/src/relay_selector/matcher.rs @@ -8,7 +8,7 @@ use mullvad_types::{ GeographicLocationConstraint, InternalBridgeConstraints, LocationConstraint, Ownership, Providers, }, - relay_list::{Relay, RelayEndpointData}, + relay_list::{Relay, RelayEndpointData, WireguardRelayEndpointData}, }; use talpid_types::net::TunnelType; @@ -32,7 +32,9 @@ pub fn filter_matching_relay_list<'a, R: Iterator + Clone>( // Filter by ownership .filter(|relay| filter_on_ownership(&query.ownership, relay)) // Filter by providers - .filter(|relay| filter_on_providers(&query.providers, relay)); + .filter(|relay| filter_on_providers(&query.providers, relay)) + // Filter by DAITA support + .filter(|relay| filter_on_daita(&query.wireguard_constraints.daita, relay)); // The last filtering to be done is on the `include_in_country` attribute found on each // relay. When the location constraint is based on country, a relay which has @@ -76,7 +78,7 @@ pub fn filter_matching_bridges<'a, R: Iterator + Clone>( .filter(|relay| filter_on_location(&locations, relay)) // Filter by ownership .filter(|relay| filter_on_ownership(&constraints.ownership, relay)) - // Filter by constraints + // Filter by providers .filter(|relay| filter_on_providers(&constraints.providers, relay)) .cloned() .collect() @@ -108,6 +110,19 @@ pub fn filter_on_providers(filter: &Constraint, relay: &Relay) -> boo filter.matches(relay) } +/// Returns whether `relay` satisfy the daita constraint posed by `filter`. +pub fn filter_on_daita(filter: &Constraint, relay: &Relay) -> bool { + match (filter, &relay.endpoint_data) { + // Only a subset of relays support DAITA, so filter out ones that don't. + ( + Constraint::Only(true), + RelayEndpointData::Wireguard(WireguardRelayEndpointData { daita, .. }), + ) => *daita, + // If we don't require DAITA, any relay works. + _ => true, + } +} + /// Returns whether the relay is an OpenVPN relay. pub const fn filter_openvpn(relay: &Relay) -> bool { matches!(relay.endpoint_data, RelayEndpointData::Openvpn) diff --git a/mullvad-relay-selector/src/relay_selector/mod.rs b/mullvad-relay-selector/src/relay_selector/mod.rs index fbf3ac1c4bde..3f92bd344bf8 100644 --- a/mullvad-relay-selector/src/relay_selector/mod.rs +++ b/mullvad-relay-selector/src/relay_selector/mod.rs @@ -99,6 +99,7 @@ pub struct RelaySelector { pub struct SelectorConfig { // Normal relay settings pub relay_settings: RelaySettings, + pub additional_constraints: AdditionalRelayConstraints, pub custom_lists: CustomListsSettings, pub relay_overrides: Vec, // Wireguard specific data @@ -108,6 +109,20 @@ pub struct SelectorConfig { pub bridge_settings: BridgeSettings, } +/// Extra relay constraints not specified in `relay_settings`. +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct AdditionalRelayConstraints { + pub wireguard: AdditionalWireguardConstraints, +} + +/// Constraints to use when selecting WireGuard servers +#[derive(Default, Debug, Clone, Eq, PartialEq)] +pub struct AdditionalWireguardConstraints { + /// If true, select WireGuard relays that support DAITA. If false, select any + /// server. + pub daita: bool, +} + /// Values which affect the choice of relay but are only known at runtime. #[derive(Clone, Debug)] pub struct RuntimeParameters { @@ -169,6 +184,7 @@ enum SpecializedSelectorConfig<'a> { #[derive(Debug, Clone)] struct NormalSelectorConfig<'a> { user_preferences: &'a RelayConstraints, + additional_preferences: &'a AdditionalRelayConstraints, custom_lists: &'a CustomListsSettings, // Wireguard specific data obfuscation_settings: &'a ObfuscationSettings, @@ -273,6 +289,7 @@ impl Default for SelectorConfig { let default_settings = Settings::default(); SelectorConfig { relay_settings: default_settings.relay_settings, + additional_constraints: AdditionalRelayConstraints::default(), bridge_settings: default_settings.bridge_settings, obfuscation_settings: default_settings.obfuscation_settings, bridge_state: default_settings.bridge_state, @@ -291,6 +308,7 @@ impl<'a> From<&'a SelectorConfig> for SpecializedSelectorConfig<'a> { RelaySettings::Normal(user_preferences) => { SpecializedSelectorConfig::Normal(NormalSelectorConfig { user_preferences, + additional_preferences: &value.additional_constraints, obfuscation_settings: &value.obfuscation_settings, bridge_state: &value.bridge_state, bridge_settings: &value.bridge_settings, @@ -307,6 +325,7 @@ impl<'a> From> for RelayQuery { /// Map the Wireguard-specific bits of `value` to [`WireguradRelayQuery`] fn wireguard_constraints( wireguard_constraints: WireguardConstraints, + additional_constraints: AdditionalWireguardConstraints, obfuscation_settings: ObfuscationSettings, ) -> WireguardRelayQuery { let WireguardConstraints { @@ -315,6 +334,7 @@ impl<'a> From> for RelayQuery { use_multihop, entry_location, } = wireguard_constraints; + let AdditionalWireguardConstraints { daita } = additional_constraints; WireguardRelayQuery { port, ip_version, @@ -322,6 +342,7 @@ impl<'a> From> for RelayQuery { entry_location, obfuscation: obfuscation_settings.selected_obfuscation, udp2tcp_port: Constraint::Only(obfuscation_settings.udp2tcp.clone()), + daita: Constraint::Only(daita), } } @@ -350,6 +371,7 @@ impl<'a> From> for RelayQuery { let wireguard_constraints = wireguard_constraints( value.user_preferences.wireguard_constraints.clone(), + value.additional_preferences.wireguard.clone(), value.obfuscation_settings.clone(), ); let openvpn_constraints = openvpn_constraints( @@ -724,8 +746,14 @@ impl RelaySelector { // After we have our two queries (one for the exit relay & one for the entry relay), // we can query for all exit & entry candidates! All candidates are needed for the next // step. - let exit_candidates = - filter_matching_relay_list(query, parsed_relays.relays(), config.custom_lists); + let mut exit_relay_query = query.clone(); + // DAITA should only be enabled for the entry relay + exit_relay_query.wireguard_constraints.daita = Constraint::Only(false); + let exit_candidates = filter_matching_relay_list( + &exit_relay_query, + parsed_relays.relays(), + config.custom_lists, + ); let entry_candidates = filter_matching_relay_list( &entry_relay_query, parsed_relays.relays(), diff --git a/mullvad-relay-selector/src/relay_selector/query.rs b/mullvad-relay-selector/src/relay_selector/query.rs index f112e83968a4..ff6840705121 100644 --- a/mullvad-relay-selector/src/relay_selector/query.rs +++ b/mullvad-relay-selector/src/relay_selector/query.rs @@ -28,6 +28,7 @@ //! queries and ensure that queries are built in a type-safe manner, reducing the risk //! of runtime errors and improving code readability. +use crate::AdditionalWireguardConstraints; use mullvad_types::{ constraints::Constraint, relay_constraints::{ @@ -139,6 +140,7 @@ pub struct WireguardRelayQuery { pub entry_location: Constraint, pub obfuscation: SelectedObfuscation, pub udp2tcp_port: Constraint, + pub daita: Constraint, } impl WireguardRelayQuery { @@ -156,6 +158,7 @@ impl WireguardRelayQuery { entry_location: Constraint::Any, obfuscation: SelectedObfuscation::Auto, udp2tcp_port: Constraint::Any, + daita: Constraint::Any, } } } @@ -172,6 +175,17 @@ impl From for WireguardConstraints { } } +impl From for AdditionalWireguardConstraints { + /// The mapping from [`WireguardRelayQuery`] to [`AdditionalWireguardConstraints`]. + fn from(value: WireguardRelayQuery) -> Self { + AdditionalWireguardConstraints { + daita: value + .daita + .unwrap_or(AdditionalWireguardConstraints::default().daita), + } + } +} + /// A query for a relay with OpenVPN-specific properties, such as `bridge_settings`. /// /// This struct may look a lot like [`OpenVpnConstraints`], and that is the point! @@ -344,10 +358,11 @@ pub mod builder { } } /// Set the VPN protocol for this [`RelayQueryBuilder`] to Wireguard. - pub fn wireguard(mut self) -> RelayQueryBuilder> { + pub fn wireguard(mut self) -> RelayQueryBuilder> { let protocol = Wireguard { multihop: Any, obfuscation: Any, + daita: Any, }; self.query.tunnel_protocol = Constraint::Only(TunnelType::Wireguard); // Update the type state @@ -381,13 +396,14 @@ pub mod builder { /// select entry point. /// /// [`WireguardRelayQuery`]: super::WireguardRelayQuery - pub struct Wireguard { + pub struct Wireguard { multihop: Multihop, obfuscation: Obfuscation, + daita: Daita, } // This impl-block is quantified over all configurations - impl RelayQueryBuilder> { + impl RelayQueryBuilder> { /// Specify the port to ues when connecting to the selected /// Wireguard relay. pub const fn port(mut self, port: u16) -> Self { @@ -403,11 +419,27 @@ pub mod builder { } } - impl RelayQueryBuilder> { + impl RelayQueryBuilder> { + /// Enable DAITA support. + pub fn daita(mut self) -> RelayQueryBuilder> { + self.query.wireguard_constraints.daita = Constraint::Only(true); + // Update the type state + RelayQueryBuilder { + query: self.query, + protocol: Wireguard { + multihop: self.protocol.multihop, + obfuscation: self.protocol.obfuscation, + daita: true, + }, + } + } + } + + impl RelayQueryBuilder> { /// Enable multihop. /// /// To configure the entry relay, see [`RelayQueryBuilder::entry`]. - pub fn multihop(mut self) -> RelayQueryBuilder> { + pub fn multihop(mut self) -> RelayQueryBuilder> { self.query.wireguard_constraints.use_multihop = Constraint::Only(true); // Update the type state RelayQueryBuilder { @@ -415,12 +447,13 @@ pub mod builder { protocol: Wireguard { multihop: true, obfuscation: self.protocol.obfuscation, + daita: self.protocol.daita, }, } } } - impl RelayQueryBuilder> { + impl RelayQueryBuilder> { /// Set the entry location in a multihop configuration. This requires /// multihop to be enabled. pub fn entry(mut self, location: GeographicLocationConstraint) -> Self { @@ -430,18 +463,19 @@ pub mod builder { } } - impl RelayQueryBuilder> { + impl RelayQueryBuilder> { /// Enable `UDP2TCP` obufscation. This will in turn enable the option to configure the /// `UDP2TCP` port. pub fn udp2tcp( mut self, - ) -> RelayQueryBuilder> { + ) -> RelayQueryBuilder> { let obfuscation = Udp2TcpObfuscationSettings { port: Constraint::Any, }; let protocol = Wireguard { multihop: self.protocol.multihop, obfuscation: obfuscation.clone(), + daita: self.protocol.daita, }; self.query.wireguard_constraints.udp2tcp_port = Constraint::Only(obfuscation); self.query.wireguard_constraints.obfuscation = SelectedObfuscation::Udp2Tcp; @@ -452,7 +486,7 @@ pub mod builder { } } - impl RelayQueryBuilder> { + impl RelayQueryBuilder> { /// Set the `UDP2TCP` port. This is the TCP port which the `UDP2TCP` obfuscation /// protocol should use to connect to a relay. pub fn udp2tcp_port(mut self, port: u16) -> Self { diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index ed3546c62ecf..d92e7a72a9cb 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -54,6 +54,7 @@ static RELAYS: Lazy = Lazy::new(|| RelayList { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -71,6 +72,7 @@ static RELAYS: Lazy = Lazy::new(|| RelayList { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -179,6 +181,19 @@ fn unwrap_relay(get_result: GetRelay) -> Relay { } } +fn unwrap_entry_relay(get_result: GetRelay) -> Relay { + match get_result { + GetRelay::Wireguard { inner, .. } => match inner { + crate::WireguardConfig::Singlehop { exit } => exit, + crate::WireguardConfig::Multihop { entry, .. } => entry, + }, + GetRelay::OpenVpn { exit, .. } => exit, + GetRelay::Custom(custom) => { + panic!("Can not extract regular relay from custom relay: {custom}") + } + } +} + fn unwrap_endpoint(get_result: GetRelay) -> MullvadEndpoint { match get_result { GetRelay::Wireguard { endpoint, .. } => MullvadEndpoint::Wireguard(endpoint), @@ -200,6 +215,13 @@ fn default_relay_selector() -> RelaySelector { RelaySelector::from_list(SelectorConfig::default(), RELAYS.clone()) } +fn supports_daita(relay: &Relay) -> bool { + match relay.endpoint_data { + RelayEndpointData::Wireguard(WireguardRelayEndpointData { daita, .. }) => daita, + _ => false, + } +} + /// This is not an actual test. Rather, it serves as a reminder that if [`RETRY_ORDER`] is modified, /// the programmer should be made aware to update all external documents which rely on the retry /// order to be correct. @@ -414,6 +436,7 @@ fn test_wireguard_entry() { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -431,6 +454,7 @@ fn test_wireguard_entry() { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -932,6 +956,7 @@ fn test_include_in_country() { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -949,6 +974,7 @@ fn test_include_in_country() { "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", ) .unwrap(), + daita: false, }), location: None, }, @@ -1117,3 +1143,156 @@ fn openvpn_bridge_with_automatic_transport_protocol() { assert!(relay.is_err()) } } + +/// Return only entry relays that support DAITA when DAITA filtering is enabled. All relays that +/// support DAITA also support NOT DAITA. Thus, disabling it should not cause any WireGuard relays +/// to be filtered out. +#[test] +fn test_daita() { + let relay_list = RelayList { + etag: None, + countries: vec![RelayListCountry { + name: "Sweden".to_string(), + code: "se".to_string(), + cities: vec![RelayListCity { + name: "Gothenburg".to_string(), + code: "got".to_string(), + latitude: 57.70887, + longitude: 11.97456, + relays: vec![ + Relay { + hostname: "se9-wireguard".to_string(), + ipv4_addr_in: "185.213.154.68".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a09f".parse().unwrap()), + include_in_country: true, + active: true, + owned: true, + provider: "31173".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + daita: false, + }), + location: None, + }, + Relay { + hostname: "se10-wireguard".to_string(), + ipv4_addr_in: "185.213.154.69".parse().unwrap(), + ipv6_addr_in: Some("2a03:1b20:5:f011::a10f".parse().unwrap()), + include_in_country: true, + active: true, + owned: false, + provider: "31173".to_string(), + weight: 1, + endpoint_data: RelayEndpointData::Wireguard(WireguardRelayEndpointData { + public_key: PublicKey::from_base64( + "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", + ) + .unwrap(), + daita: true, + }), + location: None, + }, + ], + }], + }], + openvpn: OpenVpnEndpointData { ports: vec![] }, + bridge: BridgeEndpointData { + shadowsocks: vec![], + }, + wireguard: WireguardEndpointData { + port_ranges: vec![(53, 53), (4000, 33433), (33565, 51820), (52000, 60000)], + ipv4_gateway: "10.64.0.1".parse().unwrap(), + ipv6_gateway: "fc00:bbbb:bbbb:bb01::1".parse().unwrap(), + udp2tcp_ports: vec![], + }, + }; + + let daita_supporting_relay = + GeographicLocationConstraint::hostname("se", "got", "se10-wireguard"); + let nondaita_supporting_relay = + GeographicLocationConstraint::hostname("se", "got", "se9-wireguard"); + + let relay_selector = RelaySelector::from_list(SelectorConfig::default(), relay_list); + + // Only pick relays that support DAITA + let query = RelayQueryBuilder::new().wireguard().daita().build(); + let relay = unwrap_entry_relay(relay_selector.get_relay_by_query(query).unwrap()); + assert!( + supports_daita(&relay), + "Selector supported relay that does not support DAITA: {relay:?}" + ); + + // Fail when only non-DAITA relays match constraints + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .location(nondaita_supporting_relay.clone()) + .build(); + relay_selector + .get_relay_by_query(query) + .expect_err("Expected to find no matching relay"); + + // DAITA-supporting relays can be picked even when it is disabled + let query = RelayQueryBuilder::new() + .wireguard() + .location(daita_supporting_relay.clone()) + .build(); + relay_selector + .get_relay_by_query(query) + .expect("Expected DAITA-supporting relay to work without DAITA"); + + // Non DAITA-supporting relays can be picked when it is disabled + let query = RelayQueryBuilder::new() + .wireguard() + .location(nondaita_supporting_relay.clone()) + .build(); + relay_selector + .get_relay_by_query(query) + .expect("Expected DAITA-supporting relay to work without DAITA"); + + // Entry relay must support daita + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .multihop() + .build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { exit: _, entry }, + .. + } => { + assert!(supports_daita(&entry), "entry relay must support DAITA"); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } + + // Exit relay does not have to support daita + let query = RelayQueryBuilder::new() + .wireguard() + .daita() + .multihop() + .location(nondaita_supporting_relay) + .build(); + let relay = relay_selector.get_relay_by_query(query).unwrap(); + match relay { + GetRelay::Wireguard { + inner: WireguardConfig::Multihop { exit, entry: _ }, + .. + } => { + assert!( + !supports_daita(&exit), + "expected non DAITA-supporting exit relay, got {exit:?}" + ); + } + wrong_relay => panic!( + "Relay selector should have picked a Wireguard relay, instead chose {wrong_relay:?}" + ), + } +} diff --git a/mullvad-types/src/relay_list.rs b/mullvad-types/src/relay_list.rs index 77cc621728bb..09ab312a5f64 100644 --- a/mullvad-types/src/relay_list.rs +++ b/mullvad-types/src/relay_list.rs @@ -129,6 +129,7 @@ impl PartialEq for Relay { /// # "BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=", /// # ) /// # .unwrap(), + /// # daita: false, /// # }), /// # location: None, /// }; @@ -232,6 +233,9 @@ struct PortRange { pub struct WireguardRelayEndpointData { /// Public key used by the relay peer pub public_key: wireguard::PublicKey, + /// Whether the server supports DAITA + #[serde(default)] + pub daita: bool, } #[derive(Debug, Default, Clone, Deserialize, Serialize)] diff --git a/mullvad-types/src/wireguard.rs b/mullvad-types/src/wireguard.rs index c85860a776aa..d8e9bd403b71 100644 --- a/mullvad-types/src/wireguard.rs +++ b/mullvad-types/src/wireguard.rs @@ -55,6 +55,12 @@ impl FromStr for QuantumResistantState { #[error("Not a valid state")] pub struct QuantumResistantStateParseError; +#[cfg(target_os = "windows")] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +pub struct DaitaSettings { + pub enabled: bool, +} + /// Contains account specific wireguard data #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct WireguardData { @@ -201,6 +207,9 @@ pub struct TunnelOptions { pub mtu: Option, /// Obtain a PSK using the relay config client. pub quantum_resistant: QuantumResistantState, + /// Configure DAITA + #[cfg(target_os = "windows")] + pub daita: DaitaSettings, /// Interval used for automatic key rotation #[cfg_attr(target_os = "android", jnix(skip))] pub rotation_interval: Option, @@ -212,6 +221,8 @@ impl Default for TunnelOptions { TunnelOptions { mtu: None, quantum_resistant: QuantumResistantState::Auto, + #[cfg(target_os = "windows")] + daita: DaitaSettings::default(), rotation_interval: None, } } @@ -226,6 +237,8 @@ impl TunnelOptions { QuantumResistantState::On => true, QuantumResistantState::Off => false, }, + #[cfg(target_os = "windows")] + daita: self.daita.enabled, } } } diff --git a/talpid-core/src/tunnel/mod.rs b/talpid-core/src/tunnel/mod.rs index 0daa8b996c73..ce860d0ed635 100644 --- a/talpid-core/src/tunnel/mod.rs +++ b/talpid-core/src/tunnel/mod.rs @@ -177,7 +177,6 @@ impl TunnelMonitor { let config = talpid_wireguard::config::Config::from_parameters(params, default_mtu)?; let monitor = talpid_wireguard::WireguardMonitor::start( config, - params.options.quantum_resistant, #[cfg(not(target_os = "android"))] detect_mtu, log.as_deref(), diff --git a/talpid-tunnel-config-client/build.rs b/talpid-tunnel-config-client/build.rs index 129cfd71546b..aeb21fe009f2 100644 --- a/talpid-tunnel-config-client/build.rs +++ b/talpid-tunnel-config-client/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/tunnel_config.proto").unwrap(); + tonic_build::compile_protos("proto/ephemeralpeer.proto").unwrap(); } diff --git a/talpid-tunnel-config-client/examples/psk-exchange.rs b/talpid-tunnel-config-client/examples/psk-exchange.rs index 87200d033660..e7b34ab851a5 100644 --- a/talpid-tunnel-config-client/examples/psk-exchange.rs +++ b/talpid-tunnel-config-client/examples/psk-exchange.rs @@ -18,14 +18,16 @@ async fn main() { let pubkey = PublicKey::from_base64(pubkey_string.trim()).expect("Invalid public key"); let private_key = PrivateKey::new_from_random(); - let psk = talpid_tunnel_config_client::push_pq_key( + let ephemeral_peer = talpid_tunnel_config_client::request_ephemeral_peer( tuncfg_server_ip, pubkey, private_key.public_key(), + true, + false, ) .await .unwrap(); println!("private key: {private_key:?}"); - println!("psk: {psk:?}"); + println!("psk: {:?}", ephemeral_peer.psk); } diff --git a/talpid-tunnel-config-client/examples/tuncfg-server.rs b/talpid-tunnel-config-client/examples/tuncfg-server.rs index 4d29d764a268..928587e96813 100644 --- a/talpid-tunnel-config-client/examples/tuncfg-server.rs +++ b/talpid-tunnel-config-client/examples/tuncfg-server.rs @@ -1,80 +1,93 @@ -//! A server implementation of the tuncfg PskExchangeV1 RPC to test -//! the client side implementation. +//! A server implementation of the tuncfg RegisterPeerV1 RPC to test +//! the client side implementation of PQ. #[allow(clippy::derive_partial_eq_without_eq)] mod proto { - tonic::include_proto!("tunnel_config"); + tonic::include_proto!("ephemeralpeer"); } use classic_mceliece_rust::{PublicKey, CRYPTO_PUBLICKEYBYTES}; use proto::{ - post_quantum_secure_server::{PostQuantumSecure, PostQuantumSecureServer}, - PskRequestV1, PskResponseV1, + ephemeral_peer_server::{EphemeralPeer, EphemeralPeerServer}, + EphemeralPeerRequestV1, EphemeralPeerResponseV1, PostQuantumResponseV1, }; use talpid_types::net::wireguard::PresharedKey; use tonic::{transport::Server, Request, Response, Status}; #[derive(Debug, Default)] -pub struct PostQuantumSecureImpl {} +pub struct EphemeralPeerImpl {} #[tonic::async_trait] -impl PostQuantumSecure for PostQuantumSecureImpl { - async fn psk_exchange_v1( +impl EphemeralPeer for EphemeralPeerImpl { + async fn register_peer_v1( &self, - request: Request, - ) -> Result, Status> { + request: Request, + ) -> Result, Status> { let mut rng = rand::thread_rng(); let request = request.into_inner(); - println!("wg_pubkey: {:?}", request.wg_pubkey); - println!("wg_psk_pubkey: {:?}", request.wg_psk_pubkey); + println!("wg_parent_pubkey: {:?}", request.wg_parent_pubkey); + println!( + "wg_ephemeral_peer_pubkey: {:?}", + request.wg_ephemeral_peer_pubkey + ); + println!("daita (no-op): {:?}", request.daita); - // The ciphertexts that will be returned to the client - let mut ciphertexts = Vec::new(); - // The final PSK that is computed by XORing together all the KEM outputs. - let mut psk_data = Box::new([0u8; 32]); + let post_quantum = if let Some(post_quantum) = request.post_quantum { + // The ciphertexts that will be returned to the client + let mut ciphertexts = Vec::new(); - for kem_pubkey in request.kem_pubkeys { - println!("\tKEM algorithm: {}", kem_pubkey.algorithm_name); - let (ciphertext, shared_secret) = match kem_pubkey.algorithm_name.as_str() { - "Classic-McEliece-460896f-round3" => { - let key_data: [u8; CRYPTO_PUBLICKEYBYTES] = - kem_pubkey.key_data.as_slice().try_into().unwrap(); - let public_key = PublicKey::from(&key_data); - let (ciphertext, shared_secret) = - classic_mceliece_rust::encapsulate_boxed(&public_key, &mut rng); - (ciphertext.as_array().to_vec(), *shared_secret.as_array()) - } - "Kyber1024" => { - let public_key = kem_pubkey.key_data.as_slice(); - let (ciphertext, shared_secret) = - pqc_kyber::encapsulate(public_key, &mut rng).unwrap(); - (ciphertext.to_vec(), shared_secret) - } - name => panic!("Unsupported KEM algorithm: {name}"), - }; + // The final PSK that is computed by XORing together all the KEM outputs. + let mut psk_data = Box::new([0u8; 32]); + + for kem_pubkey in post_quantum.kem_pubkeys { + println!("\tKEM algorithm: {}", kem_pubkey.algorithm_name); + let (ciphertext, shared_secret) = match kem_pubkey.algorithm_name.as_str() { + "Classic-McEliece-460896f-round3" => { + let key_data: [u8; CRYPTO_PUBLICKEYBYTES] = + kem_pubkey.key_data.as_slice().try_into().unwrap(); + let public_key = PublicKey::from(&key_data); + let (ciphertext, shared_secret) = + classic_mceliece_rust::encapsulate_boxed(&public_key, &mut rng); + (ciphertext.as_array().to_vec(), *shared_secret.as_array()) + } + "Kyber1024" => { + let public_key = kem_pubkey.key_data.as_slice(); + let (ciphertext, shared_secret) = + pqc_kyber::encapsulate(public_key, &mut rng).unwrap(); + (ciphertext.to_vec(), shared_secret) + } + name => panic!("Unsupported KEM algorithm: {name}"), + }; - ciphertexts.push(ciphertext); - println!("\tshared secret: {shared_secret:?}"); - for (psk_byte, shared_secret_byte) in psk_data.iter_mut().zip(shared_secret.iter()) { - *psk_byte ^= shared_secret_byte; + ciphertexts.push(ciphertext); + println!("\tshared secret: {shared_secret:?}"); + for (psk_byte, shared_secret_byte) in psk_data.iter_mut().zip(shared_secret.iter()) + { + *psk_byte ^= shared_secret_byte; + } } - } - let psk = PresharedKey::from(psk_data); - println!("psk: {psk:?}"); - println!("=============================================="); - Ok(Response::new(PskResponseV1 { ciphertexts })) + let psk = PresharedKey::from(psk_data); + println!("psk: {psk:?}"); + println!("=============================================="); + + Some(PostQuantumResponseV1 { ciphertexts }) + } else { + None + }; + + Ok(Response::new(EphemeralPeerResponseV1 { post_quantum })) } } #[tokio::main] async fn main() -> Result<(), Box> { let addr = "127.0.0.1:1337".parse()?; - let server = PostQuantumSecureImpl::default(); + let server = EphemeralPeerImpl::default(); Server::builder() - .add_service(PostQuantumSecureServer::new(server)) + .add_service(EphemeralPeerServer::new(server)) .serve(addr) .await?; diff --git a/talpid-tunnel-config-client/proto/ephemeralpeer.proto b/talpid-tunnel-config-client/proto/ephemeralpeer.proto new file mode 100644 index 000000000000..bb49eb5598b1 --- /dev/null +++ b/talpid-tunnel-config-client/proto/ephemeralpeer.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +option go_package = "github.com/mullvad/wg-manager/tuncfg/api/ephemeralpeer"; + +package ephemeralpeer; + +service EphemeralPeer { + // Derive an ephemeral peer with one or several options enabled, such as PQ or DAITA. + // + // The VPN server associates the ephemeral peer with the peer who performed the exchange. Any + // already existing ephemeral peer for the normal peer is replaced. Each normal peer can have + // at most one ephemeral peer. + // + // The ephemeral peer is mutually exclusive to the normal peer. The server keeps both peers in + // memory, but only one of them is loaded into WireGuard at any point in time. A handshake from + // the normal peer unloads the corresponding ephemeral peer from WireGuard and vice versa. + // + // A new peer is negotiated to avoid a premature break of the tunnel used for negotiation. + // A tunnel would break prematurely if configuration such as preshared key were applied before the + // normal peer received the server's response. This cannot occur now because the client decides + // when to switch to the ephemeral tunnel. This design also allows the client to switch back to + // using a non-ephemeral tunnel at any point. + // + // The server gives no guarantees how long the ephemeral peer will be valid and working when it's + // no longer in use. The client should negotiate a new ephemeral peer every time it establishes a + // new tunnel to the server. + // + // The request from the VPN client should contain: + // * `wg_parent_pubkey` - The public key used by the current tunnel (that the request travels + // inside). + // * `wg_ephemeral_peer_pubkey` - A newly generated ephemeral WireGuard public key for the + // ephemeral peer. The server will associate the new configuration with this key. + // * One or more requests for different types of options. See the individual messages for more + // information. If a request is provided, a corresponding response may be returned in the + // server's response. + rpc RegisterPeerV1(EphemeralPeerRequestV1) returns (EphemeralPeerResponseV1) {} +} + +message EphemeralPeerRequestV1 { + bytes wg_parent_pubkey = 1; + bytes wg_ephemeral_peer_pubkey = 2; + PostQuantumRequestV1 post_quantum = 3; + DaitaRequestV1 daita = 4; +} + +// The v1 request supports exactly two algorithms. +// The algorithms can appear soletary or in mixed order: +// - "Classic-McEliece-460896f", but explicitly identified as "Classic-McEliece-460896f-round3" +// - "Kyber1024" +message PostQuantumRequestV1 { repeated KemPubkeyV1 kem_pubkeys = 1; } + +message KemPubkeyV1 { + string algorithm_name = 1; + bytes key_data = 2; +} + +message DaitaRequestV1 { bool activate_daita = 1; } + +message EphemeralPeerResponseV1 { + // The response from the VPN server contains: + // * `ciphertexts` - A list of the ciphertexts (the encapsulated shared secrets) for all + // public keys in `kem_pubkeys` in the request, in the same order as in the request. + // + // # Deriving the WireGuard PSK + // + // The PSK to be used in WireGuard's preshared-key field is computed by XORing the resulting + // shared secrets of all the KEM algorithms. All currently supported and planned to be + // supported algorithms output 32 bytes, so this is trivial. + // + // Since the PSK provided to WireGuard is directly fed into a HKDF, it is not important that + // the entropy in the PSK is uniformly distributed. The actual keys used for encrypting the + // data channel will have uniformly distributed entropy anyway, thanks to the HKDF. + // But even if that was not true, since both CME and Kyber run SHAKE256 as the last step + // of their internal key derivation, the output they produce are uniformly distributed. + // + // If we later want to support another type of KEM that produce longer or shorter output, + // we can hash that secret into a 32 byte hash before proceeding to the XOR step. + // + // Mixing with XOR (A = B ^ C) is fine since nothing about A is revealed even if one of B or C + // is known. Both B *and* C must be known to compute any bit in A. This means all involved + // KEM algorithms must be broken before the PSK can be computed by an attacker. + PostQuantumResponseV1 post_quantum = 1; +} + +message PostQuantumResponseV1 { repeated bytes ciphertexts = 1; } diff --git a/talpid-tunnel-config-client/proto/tunnel_config.proto b/talpid-tunnel-config-client/proto/tunnel_config.proto deleted file mode 100644 index e6e4b73d97ba..000000000000 --- a/talpid-tunnel-config-client/proto/tunnel_config.proto +++ /dev/null @@ -1,83 +0,0 @@ -syntax = "proto3"; - -option go_package = "github.com/mullvad/wg-manager/server/tuncfg"; - -package tunnel_config; - -service PostQuantumSecure { - // Allows deriving a preshared key (PSK) using one or multiple PQ-secure key-encapsulation - // mechanisms (KEM). The preshared key is added to WireGuard's preshared-key field in a new - // ephemeral peer (PQ-peer). This makes the tunnel resistant towards attacks using - // quantum computers. - // - // The VPN server associates the PQ-peer with the peer who performed the exchange. Any - // already existing PQ-peer for the normal peer is replaced. Each normal peer can have - // at most one PQ-peer. - // - // The PQ-peer is mutually exclusive to the normal peer. The server keeps both peers in memory, - // but only one of them is loaded into WireGuard at any point in time. A handshake from the - // normal peer unloads the corresponding PQ-peer from WireGuard and vice versa. - // - // A new peer is negotiated for PQ to avoid a premature break of the tunnel used for negotiation. - // A tunnel would break prematurely if the preshared key is applied before the normal peer - // received the server's contribution to the KEM exchange. This cannot occur now because - // the client decides when to switch to the PQ-secure tunnel. This design also allows - // the client to switch back to using a non-PQ-secure tunnel at any point. - // - // The negotiated PQ-peer is ephemeral. The server gives no guarantees how long it will be - // valid and working. The client should negotiate a new PQ-peer every time it establishes a new - // tunnel to the server. - // - // The full exchange requires just a single request-response round trip between the VPN client - // and the VPN server. - // - // # Request-response format - // - // The request from the VPN client contains: - // * `wg_pubkey` - The public key used by the current tunnel (that the request travels inside). - // * `wg_psk_pubkey` - A newly generated ephemeral WireGuard public key for the PQ-peer. - // The server will associate the derived PSK with this public key. - // * `kem_pubkeys` - A list describing the KEM algorithms. Must have at least one entry. - // The same KEM must not be listed more than once. Each list item contains: - // * `algorithm_name` - The name of the KEM, including which variant. Should be the same - // name/format that `liboqs` uses. - // * `key_data` - The client's public key for this KEM. Will be used by the server to - // encapsulate the shared secret for this KEM. - // - // The response from the VPN server contains: - // * `ciphertexts` - A list of the ciphertexts (the encapsulated shared secrets) for all - // public keys in `kem_pubkeys` in the request, in the same order as in the request. - // - // # Deriving the WireGuard PSK - // - // The PSK to be used in WireGuard's preshared-key field is computed by XORing the resulting - // shared secrets of all the KEM algorithms. All currently supported and planned to be - // supported algorithms output 32 bytes, so this is trivial. - // - // Since the PSK provided to WireGuard is directly fed into a HKDF, it is not important that - // the entropy in the PSK is uniformly distributed. The actual keys used for encrypting the - // data channel will have uniformly distributed entropy anyway, thanks to the HKDF. - // But even if that was not true, since both CME and Kyber run SHAKE256 as the last step - // of their internal key derivation, the output they produce are uniformly distributed. - // - // If we later want to support another type of KEM that produce longer or shorter output, - // we can hash that secret into a 32 byte hash before proceeding to the XOR step. - // - // Mixing with XOR (A = B ^ C) is fine since nothing about A is revealed even if one of B or C - // is known. Both B *and* C must be known to compute any bit in A. This means all involved - // KEM algorithms must be broken before the PSK can be computed by an attacker. - rpc PskExchangeV1(PskRequestV1) returns (PskResponseV1) {} -} - -message PskRequestV1 { - bytes wg_pubkey = 1; - bytes wg_psk_pubkey = 2; - repeated KemPubkeyV1 kem_pubkeys = 3; -} - -message KemPubkeyV1 { - string algorithm_name = 1; - bytes key_data = 2; -} - -message PskResponseV1 { repeated bytes ciphertexts = 1; } diff --git a/talpid-tunnel-config-client/src/classic_mceliece.rs b/talpid-tunnel-config-client/src/classic_mceliece.rs index a2d5dc0be20d..2036bc3fc7df 100644 --- a/talpid-tunnel-config-client/src/classic_mceliece.rs +++ b/talpid-tunnel-config-client/src/classic_mceliece.rs @@ -1,6 +1,5 @@ -use classic_mceliece_rust::{ - keypair_boxed, Ciphertext, PublicKey, SecretKey, SharedSecret, CRYPTO_CIPHERTEXTBYTES, -}; +use classic_mceliece_rust::{keypair_boxed, Ciphertext, CRYPTO_CIPHERTEXTBYTES}; +pub use classic_mceliece_rust::{PublicKey, SecretKey, SharedSecret}; /// The `keypair_boxed` function needs just under 1 MiB of stack in debug /// builds. Even though it probably works to run it directly on the main diff --git a/talpid-tunnel-config-client/src/kyber.rs b/talpid-tunnel-config-client/src/kyber.rs index 5654b2e10b71..003c88dc484d 100644 --- a/talpid-tunnel-config-client/src/kyber.rs +++ b/talpid-tunnel-config-client/src/kyber.rs @@ -1,6 +1,5 @@ -use pqc_kyber::{SecretKey, KYBER_CIPHERTEXTBYTES}; - -pub use pqc_kyber::{keypair, KyberError}; +use pqc_kyber::KYBER_CIPHERTEXTBYTES; +pub use pqc_kyber::{keypair, KyberError, SecretKey}; /// Use the strongest variant of Kyber. It is fast and the keys are small, so there is no practical /// benefit of going with anything lower. diff --git a/talpid-tunnel-config-client/src/lib.rs b/talpid-tunnel-config-client/src/lib.rs index 2c7b5e58f38c..e272e794c776 100644 --- a/talpid-tunnel-config-client/src/lib.rs +++ b/talpid-tunnel-config-client/src/lib.rs @@ -13,7 +13,7 @@ mod kyber; #[allow(clippy::derive_partial_eq_without_eq)] mod proto { - tonic::include_proto!("tunnel_config"); + tonic::include_proto!("ephemeralpeer"); } use libc::setsockopt; @@ -34,6 +34,7 @@ use sys::*; pub enum Error { GrpcConnectError(tonic::transport::Error), GrpcError(tonic::Status), + MissingCiphertexts, InvalidCiphertextLength { algorithm: &'static str, actual: usize, @@ -51,6 +52,7 @@ impl std::fmt::Display for Error { match self { GrpcConnectError(_) => "Failed to connect to config service".fmt(f), GrpcError(status) => write!(f, "RPC failed: {status}"), + MissingCiphertexts => write!(f, "Found no ciphertexts in response"), InvalidCiphertextLength { algorithm, actual, @@ -77,7 +79,7 @@ impl std::error::Error for Error { } } -type RelayConfigService = proto::post_quantum_secure_client::PostQuantumSecureClient; +type RelayConfigService = proto::ephemeral_peer_client::EphemeralPeerClient; /// Port used by the tunnel config service. pub const CONFIG_SERVICE_PORT: u16 = 1337; @@ -93,72 +95,101 @@ pub const CONFIG_SERVICE_PORT: u16 = 1337; /// handshake to work even if there is fragmentation. const CONFIG_CLIENT_MTU: u16 = 576; -/// Generates a new WireGuard key pair and negotiates a PSK with the relay in a PQ-safe -/// manner. This creates a peer on the relay with the new WireGuard pubkey and PSK, -/// which can then be used to establish a PQ-safe tunnel to the relay. -// TODO: consider binding to the tunnel interface here, on non-windows platforms -pub async fn push_pq_key( +pub struct EphemeralPeer { + pub psk: Option, +} + +/// Negotiate a short-lived peer with a PQ-safe PSK or with DAITA enabled. +pub async fn request_ephemeral_peer( service_address: IpAddr, - wg_pubkey: PublicKey, - wg_psk_pubkey: PublicKey, -) -> Result { - let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await; - let kyber_keypair = kyber::keypair(&mut rand::thread_rng()); + parent_pubkey: PublicKey, + ephemeral_pubkey: PublicKey, + enable_post_quantum: bool, + enable_daita: bool, +) -> Result { + let (pq_request, kem_secrets) = if enable_post_quantum { + let (cme_kem_pubkey, cme_kem_secret) = classic_mceliece::generate_keys().await; + let kyber_keypair = kyber::keypair(&mut rand::thread_rng()); + + ( + Some(proto::PostQuantumRequestV1 { + kem_pubkeys: vec![ + proto::KemPubkeyV1 { + algorithm_name: classic_mceliece::ALGORITHM_NAME.to_owned(), + key_data: cme_kem_pubkey.as_array().to_vec(), + }, + proto::KemPubkeyV1 { + algorithm_name: kyber::ALGORITHM_NAME.to_owned(), + key_data: kyber_keypair.public.to_vec(), + }, + ], + }), + Some((cme_kem_secret, kyber_keypair.secret)), + ) + } else { + (None, None) + }; + + let daita = Some(proto::DaitaRequestV1 { + activate_daita: enable_daita, + }); let mut client = new_client(service_address).await?; let response = client - .psk_exchange_v1(proto::PskRequestV1 { - wg_pubkey: wg_pubkey.as_bytes().to_vec(), - wg_psk_pubkey: wg_psk_pubkey.as_bytes().to_vec(), - kem_pubkeys: vec![ - proto::KemPubkeyV1 { - algorithm_name: classic_mceliece::ALGORITHM_NAME.to_owned(), - key_data: cme_kem_pubkey.as_array().to_vec(), - }, - proto::KemPubkeyV1 { - algorithm_name: kyber::ALGORITHM_NAME.to_owned(), - key_data: kyber_keypair.public.to_vec(), - }, - ], + .register_peer_v1(proto::EphemeralPeerRequestV1 { + wg_parent_pubkey: parent_pubkey.as_bytes().to_vec(), + wg_ephemeral_peer_pubkey: ephemeral_pubkey.as_bytes().to_vec(), + post_quantum: pq_request, + daita, }) .await .map_err(Error::GrpcError)?; - let ciphertexts = response.into_inner().ciphertexts; - - // Unpack the ciphertexts into one per KEM without needing to access them by index. - let [cme_ciphertext, kyber_ciphertext] = <&[Vec; 2]>::try_from(ciphertexts.as_slice()) - .map_err(|_| Error::InvalidCiphertextCount { - actual: ciphertexts.len(), - })?; - - // Store the PSK data on the heap. So it can be passed around and then zeroized on drop without - // being stored in a bunch of places on the stack. - let mut psk_data = Box::new([0u8; 32]); - - // Decapsulate Classic McEliece and mix into PSK - { - let mut shared_secret = classic_mceliece::decapsulate(&cme_kem_secret, cme_ciphertext)?; - xor_assign(&mut psk_data, shared_secret.as_array()); + let psk = if let Some((cme_kem_secret, kyber_secret)) = kem_secrets { + let ciphertexts = response + .into_inner() + .post_quantum + .ok_or(Error::MissingCiphertexts)? + .ciphertexts; + + // Unpack the ciphertexts into one per KEM without needing to access them by index. + let [cme_ciphertext, kyber_ciphertext] = <&[Vec; 2]>::try_from(ciphertexts.as_slice()) + .map_err(|_| Error::InvalidCiphertextCount { + actual: ciphertexts.len(), + })?; + + // Store the PSK data on the heap. So it can be passed around and then zeroized on drop without + // being stored in a bunch of places on the stack. + let mut psk_data = Box::new([0u8; 32]); + + // Decapsulate Classic McEliece and mix into PSK + { + let mut shared_secret = classic_mceliece::decapsulate(&cme_kem_secret, cme_ciphertext)?; + xor_assign(&mut psk_data, shared_secret.as_array()); + + // This should happen automatically due to `SharedSecret` implementing ZeroizeOnDrop. But + // doing it explicitly provides a stronger guarantee that it's not accidentally + // removed. + shared_secret.zeroize(); + } + // Decapsulate Kyber and mix into PSK + { + let mut shared_secret = kyber::decapsulate(kyber_secret, kyber_ciphertext)?; + xor_assign(&mut psk_data, &shared_secret); + + // The shared secret is sadly stored in an array on the stack. So we can't get any + // guarantees that it's not copied around on the stack. The best we can do here + // is to zero out the version we have and hope the compiler optimizes out copies. + // https://github.com/Argyle-Software/kyber/issues/59 + shared_secret.zeroize(); + } - // This should happen automatically due to `SharedSecret` implementing ZeroizeOnDrop. But - // doing it explicitly provides a stronger guarantee that it's not accidentally - // removed. - shared_secret.zeroize(); - } - // Decapsulate Kyber and mix into PSK - { - let mut shared_secret = kyber::decapsulate(kyber_keypair.secret, kyber_ciphertext)?; - xor_assign(&mut psk_data, &shared_secret); - - // The shared secret is sadly stored in an array on the stack. So we can't get any - // guarantees that it's not copied around on the stack. The best we can do here - // is to zero out the version we have and hope the compiler optimizes out copies. - // https://github.com/Argyle-Software/kyber/issues/59 - shared_secret.zeroize(); - } + Some(PresharedKey::from(psk_data)) + } else { + None + }; - Ok(PresharedKey::from(psk_data)) + Ok(EphemeralPeer { psk }) } /// Performs `dst = dst ^ src`. diff --git a/talpid-types/src/net/mod.rs b/talpid-types/src/net/mod.rs index 2b9f0a73664f..a17d8ceb5f25 100644 --- a/talpid-types/src/net/mod.rs +++ b/talpid-types/src/net/mod.rs @@ -40,6 +40,8 @@ impl TunnelParameters { obfuscation: None, entry_endpoint: None, tunnel_interface: None, + #[cfg(target_os = "windows")] + daita: false, }, TunnelParameters::Wireguard(params) => TunnelEndpoint { tunnel_type: TunnelType::Wireguard, @@ -55,6 +57,8 @@ impl TunnelParameters { .get_exit_endpoint() .map(|_| params.connection.get_endpoint()), tunnel_interface: None, + #[cfg(target_os = "windows")] + daita: params.options.daita, }, } } @@ -183,6 +187,8 @@ pub struct TunnelEndpoint { pub entry_endpoint: Option, #[cfg_attr(target_os = "android", jnix(skip))] pub tunnel_interface: Option, + #[cfg(target_os = "windows")] + pub daita: bool, } impl fmt::Display for TunnelEndpoint { diff --git a/talpid-types/src/net/wireguard.rs b/talpid-types/src/net/wireguard.rs index db7b2da3a919..f7212236e2f9 100644 --- a/talpid-types/src/net/wireguard.rs +++ b/talpid-types/src/net/wireguard.rs @@ -60,6 +60,10 @@ pub struct PeerConfig { /// ephemeral and living in memory only. #[serde(skip)] pub psk: Option, + /// Enable constant packet sizes for `entry_peer`` + #[cfg(target_os = "windows")] + #[serde(skip)] + pub constant_packet_size: bool, } #[derive(Clone, Eq, PartialEq, Deserialize, Serialize, Debug)] @@ -76,6 +80,9 @@ pub struct TunnelOptions { pub mtu: Option, /// Perform PQ-safe PSK exchange when connecting pub quantum_resistant: bool, + /// Enable DAITA during tunnel config + #[cfg(target_os = "windows")] + pub daita: bool, } /// Wireguard x25519 private key diff --git a/talpid-windows/src/sync.rs b/talpid-windows/src/sync.rs index 202c96524d28..7b4ed59be3d6 100644 --- a/talpid-windows/src/sync.rs +++ b/talpid-windows/src/sync.rs @@ -1,7 +1,7 @@ use std::{io, ptr}; use windows_sys::Win32::{ - Foundation::{CloseHandle, BOOL, HANDLE}, - System::Threading::{CreateEventW, SetEvent}, + Foundation::{CloseHandle, DuplicateHandle, BOOL, DUPLICATE_SAME_ACCESS, HANDLE}, + System::Threading::{CreateEventW, GetCurrentProcess, SetEvent}, }; /// Windows event object @@ -39,6 +39,26 @@ impl Event { pub fn as_raw(&self) -> HANDLE { self.0 } + + /// Duplicate the event object with `DuplicateHandle()` + pub fn duplicate(&self) -> io::Result { + let mut new_event = 0; + let status = unsafe { + DuplicateHandle( + GetCurrentProcess(), + self.0, + GetCurrentProcess(), + &mut new_event, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ) + }; + if status == 0 { + return Err(io::Error::last_os_error()); + } + Ok(Event(new_event)) + } } impl Drop for Event { diff --git a/talpid-wireguard/Cargo.toml b/talpid-wireguard/Cargo.toml index 1fc6e13b3a40..c2562d212c30 100644 --- a/talpid-wireguard/Cargo.toml +++ b/talpid-wireguard/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] thiserror = { workspace = true } +base64 = "0.13" futures = "0.3.15" hex = "0.4" ipnetwork = "0.16" @@ -54,6 +55,7 @@ talpid-dbus = { path = "../talpid-dbus" } bitflags = "1.2" talpid-windows = { path = "../talpid-windows" } widestring = "1.0" +maybenot = "1.0" # TODO: Figure out which features are needed and which are not [target.'cfg(windows)'.dependencies.windows-sys] diff --git a/talpid-wireguard/src/config.rs b/talpid-wireguard/src/config.rs index 29328eb68152..f10a0e485991 100644 --- a/talpid-wireguard/src/config.rs +++ b/talpid-wireguard/src/config.rs @@ -28,6 +28,10 @@ pub struct Config { pub enable_ipv6: bool, /// Obfuscator config to be used for reaching the relay. pub obfuscator_config: Option, + /// Enable quantum-resistant PSK exchange + pub quantum_resistant: bool, + /// Enable DAITA + pub daita: bool, } /// Configuration errors @@ -92,6 +96,11 @@ impl Config { #[cfg(target_os = "linux")] enable_ipv6: generic_options.enable_ipv6, obfuscator_config: obfuscator_config.to_owned(), + quantum_resistant: wg_options.quantum_resistant, + #[cfg(target_os = "windows")] + daita: wg_options.daita, + #[cfg(not(target_os = "windows"))] + daita: false, }; for peer in config.peers_mut() { diff --git a/talpid-wireguard/src/connectivity_check.rs b/talpid-wireguard/src/connectivity_check.rs index e820a3eb73a1..70f88e687215 100644 --- a/talpid-wireguard/src/connectivity_check.rs +++ b/talpid-wireguard/src/connectivity_check.rs @@ -612,6 +612,11 @@ mod test { ) -> Pin> + Send>> { Box::pin(async { Ok(()) }) } + + #[cfg(target_os = "windows")] + fn start_daita(&mut self) -> std::result::Result<(), TunnelError> { + Ok(()) + } } fn mock_monitor( diff --git a/talpid-wireguard/src/lib.rs b/talpid-wireguard/src/lib.rs index 72d6a31566df..7c01538b60ac 100644 --- a/talpid-wireguard/src/lib.rs +++ b/talpid-wireguard/src/lib.rs @@ -260,7 +260,6 @@ impl WireguardMonitor { + 'static, >( mut config: Config, - psk_negotiation: bool, #[cfg(not(target_os = "android"))] detect_mtu: bool, log_path: Option<&Path>, args: TunnelArgs<'_, F>, @@ -283,12 +282,12 @@ impl WireguardMonitor { log_path, args.resource_dir, args.tun_provider.clone(), + #[cfg(target_os = "android")] + config.quantum_resistant, #[cfg(target_os = "windows")] args.route_manager.clone(), #[cfg(target_os = "windows")] setup_done_tx, - #[cfg(target_os = "android")] - psk_negotiation, )?; let iface_name = tunnel.get_interface_name(); @@ -336,7 +335,7 @@ impl WireguardMonitor { .await?; let metadata = Self::tunnel_metadata(&iface_name, &config); - let allowed_traffic = if psk_negotiation { + let allowed_traffic = if config.quantum_resistant || config.daita { AllowedTunnelTraffic::One(Endpoint::new( config.ipv4_gateway, talpid_tunnel_config_client::CONFIG_SERVICE_PORT, @@ -365,16 +364,16 @@ impl WireguardMonitor { .map_err(Error::SetupRoutingError) .map_err(CloseMsg::SetupError)?; - let psk_obfs_sender = close_obfs_sender.clone(); - if psk_negotiation { - Self::psk_negotiation( + let ephemeral_obfs_sender = close_obfs_sender.clone(); + if config.quantum_resistant || config.daita { + Self::config_ephemeral_peers( &tunnel, &mut config, args.retry_attempt, args.on_event.clone(), &iface_name, obfuscator.clone(), - psk_obfs_sender, + ephemeral_obfs_sender, #[cfg(target_os = "android")] args.tun_provider, ) @@ -386,6 +385,14 @@ impl WireguardMonitor { let config = config.clone(); let iface_name = iface_name.clone(); tokio::task::spawn(async move { + #[cfg(target_os = "windows")] + if config.daita { + // TODO: For now, we assume the MTU during the tunnel lifetime. + // We could instead poke maybenot whenever we detect changes to it. + log::warn!("MTU detection is not supported with DAITA. Skipping"); + return; + } + if let Err(e) = mtu_detection::automatic_mtu_correction( gateway, iface_name, @@ -465,7 +472,7 @@ impl WireguardMonitor { } #[allow(clippy::too_many_arguments)] - async fn psk_negotiation( + async fn config_ephemeral_peers( tunnel: &Arc>>>, config: &mut Config, retry_attempt: u32, @@ -482,7 +489,7 @@ impl WireguardMonitor { + Clone + 'static, { - let wg_psk_privkey = PrivateKey::new_from_random(); + let ephemeral_private_key = PrivateKey::new_from_random(); let close_obfs_sender = close_obfs_sender.clone(); let allowed_traffic = Endpoint::new( @@ -507,11 +514,17 @@ impl WireguardMonitor { let metadata = Self::tunnel_metadata(iface_name, config); (on_event)(TunnelEvent::InterfaceUp(metadata, allowed_traffic.clone())).await; - let exit_psk = - Self::perform_psk_negotiation(retry_attempt, config, wg_psk_privkey.public_key()) - .await?; + let exit_should_have_daita = config.daita && !config.is_multihop(); + let exit_psk = Self::request_ephemeral_peer( + retry_attempt, + config, + ephemeral_private_key.public_key(), + config.quantum_resistant, + exit_should_have_daita, + ) + .await?; - log::debug!("Successfully exchanged PSK with exit peer"); + log::debug!("Retrieved ephemeral peer"); if config.is_multihop() { // Set up tunnel to lead to entry @@ -531,22 +544,27 @@ impl WireguardMonitor { &tun_provider, ) .await?; - let entry_psk = Some( - Self::perform_psk_negotiation( - retry_attempt, - &entry_config, - wg_psk_privkey.public_key(), - ) - .await?, - ); + let entry_psk = Self::request_ephemeral_peer( + retry_attempt, + &entry_config, + ephemeral_private_key.public_key(), + config.quantum_resistant, + config.daita, + ) + .await?; log::debug!("Successfully exchanged PSK with entry peer"); config.entry_peer.psk = entry_psk; } - config.exit_peer_mut().psk = Some(exit_psk); + config.exit_peer_mut().psk = exit_psk; + #[cfg(target_os = "windows")] + if config.daita { + log::trace!("Enabling constant packet size for entry peer"); + config.entry_peer.constant_packet_size = true; + } - config.tunnel.private_key = wg_psk_privkey; + config.tunnel.private_key = ephemeral_private_key; *config = Self::reconfigure_tunnel( tunnel, @@ -557,6 +575,19 @@ impl WireguardMonitor { &tun_provider, ) .await?; + + #[cfg(target_os = "windows")] + if config.daita { + // Start local DAITA machines + let mut tunnel = tunnel.lock().unwrap(); + if let Some(tunnel) = tunnel.as_mut() { + tunnel + .start_daita() + .map_err(Error::TunnelError) + .map_err(CloseMsg::SetupError)?; + } + } + let metadata = Self::tunnel_metadata(iface_name, config); (on_event)(TunnelEvent::InterfaceUp( metadata, @@ -678,12 +709,14 @@ impl WireguardMonitor { Ok(()) } - async fn perform_psk_negotiation( + async fn request_ephemeral_peer( retry_attempt: u32, config: &Config, wg_psk_pubkey: PublicKey, - ) -> std::result::Result { - log::debug!("Performing PQ-safe PSK exchange"); + enable_pq: bool, + enable_daita: bool, + ) -> std::result::Result, CloseMsg> { + log::debug!("Requesting ephemeral peer"); let timeout = std::cmp::min( MAX_PSK_EXCHANGE_TIMEOUT, @@ -691,23 +724,25 @@ impl WireguardMonitor { .saturating_mul(PSK_EXCHANGE_TIMEOUT_MULTIPLIER.saturating_pow(retry_attempt)), ); - let psk = tokio::time::timeout( + let ephemeral = tokio::time::timeout( timeout, - talpid_tunnel_config_client::push_pq_key( + talpid_tunnel_config_client::request_ephemeral_peer( IpAddr::from(config.ipv4_gateway), config.tunnel.private_key.public_key(), wg_psk_pubkey, + enable_pq, + enable_daita, ), ) .await .map_err(|_timeout_err| { - log::warn!("Timeout while negotiating PSK"); + log::warn!("Timeout while negotiating ephemeral peer"); CloseMsg::PskNegotiationTimeout })? .map_err(Error::PskNegotiationError) .map_err(CloseMsg::SetupError)?; - Ok(psk) + Ok(ephemeral.psk) } #[allow(unused_variables)] @@ -717,7 +752,7 @@ impl WireguardMonitor { log_path: Option<&Path>, resource_dir: &Path, tun_provider: Arc>, - #[cfg(target_os = "android")] psk_negotiation: bool, + #[cfg(target_os = "android")] gateway_only: bool, #[cfg(windows)] route_manager: crate::routing::RouteManagerHandle, #[cfg(windows)] setup_done_tx: mpsc::Sender>, ) -> Result> { @@ -771,7 +806,7 @@ impl WireguardMonitor { Self::get_tunnel_destinations(config).flat_map(Self::replace_default_prefixes); #[cfg(target_os = "android")] - let config = Self::patch_allowed_ips(config, psk_negotiation); + let config = Self::patch_allowed_ips(config, gateway_only); #[cfg(target_os = "linux")] log::debug!("Using userspace WireGuard implementation"); @@ -994,6 +1029,8 @@ pub(crate) trait Tunnel: Send { &self, _config: Config, ) -> Pin> + Send>>; + #[cfg(target_os = "windows")] + fn start_daita(&mut self) -> std::result::Result<(), TunnelError>; } /// Errors to be returned from WireGuard implementations, namely implementers of the Tunnel trait diff --git a/talpid-wireguard/src/wireguard_nt/daita.rs b/talpid-wireguard/src/wireguard_nt/daita.rs new file mode 100644 index 000000000000..75d6ebaa4d58 --- /dev/null +++ b/talpid-wireguard/src/wireguard_nt/daita.rs @@ -0,0 +1,450 @@ +use super::WIREGUARD_KEY_LENGTH; +use maybenot::framework::MachineId; +use once_cell::sync::OnceCell; +use std::{collections::HashMap, fs, io, path::Path, time::Duration}; +use std::{os::windows::prelude::RawHandle, sync::Arc}; +use talpid_types::net::wireguard::PublicKey; +use tokio::task::JoinHandle; +use windows_sys::Win32::Foundation::BOOLEAN; +use windows_sys::Win32::{ + Foundation::ERROR_NO_MORE_ITEMS, + System::Threading::{WaitForMultipleObjects, WaitForSingleObject, INFINITE}, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Failed to find maybenot machines + #[error("Failed to enumerate maybenot machines")] + EnumerateMachines(#[source] io::Error), + /// Failed to parse maybenot machine + #[error("Failed to parse maybenot machine \"{0}\"")] + InvalidMachine(String), + /// Failed to initialize quit event + #[error("Failed to initialize quit event")] + InitializeQuitEvent(#[source] io::Error), + /// Failed to initialize machinist handle + #[error("Failed to initialize machinist handle")] + InitializeHandle(#[source] io::Error), + /// Failed to initialize maybenot framework + #[error("Failed to initialize maybenot framework: {0}")] + InitializeMaybenot(String), +} + +// See DAITA_EVENT_TYPE: +// https://github.com/mullvad/wireguard-nt-priv/blob/mullvad-patches/driver/daita.h +#[repr(C)] +#[derive(Debug)] +#[allow(dead_code)] +pub enum EventType { + NonpaddingSent, + NonpaddingReceived, + PaddingSent, + PaddingReceived, +} + +// See DAITA_EVENT: +// https://github.com/mullvad/wireguard-nt-priv/blob/mullvad-patches/driver/daita.h +#[repr(C)] +#[derive(Debug)] +pub struct Event { + pub peer: [u8; WIREGUARD_KEY_LENGTH], + pub event_type: EventType, + pub xmit_bytes: u16, + pub user_context: usize, +} + +// See DAITA_ACTION_TYPE: +// https://github.com/mullvad/wireguard-nt-priv/blob/mullvad-patches/driver/daita.h +#[repr(C)] +pub enum ActionType { + InjectPadding, +} + +// See DAITA_PADDING_ACTION: +// https://github.com/mullvad/wireguard-nt-priv/blob/mullvad-patches/driver/daita.h +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct PaddingAction { + pub byte_count: u16, + pub replace: BOOLEAN, +} + +// See DAITA_ACTION: +// https://github.com/mullvad/wireguard-nt-priv/blob/mullvad-patches/driver/daita.h +#[repr(C)] +pub struct Action { + pub peer: [u8; WIREGUARD_KEY_LENGTH], + pub action_type: ActionType, + pub payload: ActionPayload, + pub user_context: usize, +} + +#[repr(C)] +pub union ActionPayload { + pub padding: PaddingAction, +} + +/// Maximum number of events that can be stored in the underlying buffer +const EVENTS_CAPACITY: usize = 1000; +/// Maximum number of actions that can be stored in the underlying buffer +const ACTIONS_CAPACITY: usize = 1000; + +pub mod bindings { + use super::*; + use windows_sys::Win32::Foundation::BOOL; + + pub type WireGuardDaitaActivateFn = unsafe extern "stdcall" fn( + adapter: RawHandle, + events_capacity: usize, + actions_capacity: usize, + ) -> BOOL; + pub type WireGuardDaitaEventDataAvailableEventFn = + unsafe extern "stdcall" fn(adapter: RawHandle) -> RawHandle; + pub type WireGuardDaitaReceiveEventsFn = + unsafe extern "stdcall" fn(adapter: RawHandle, events: *mut Event) -> usize; + pub type WireGuardDaitaSendActionFn = + unsafe extern "stdcall" fn(adapter: RawHandle, action: *const Action) -> BOOL; +} + +#[derive(Debug)] +pub struct Session { + adapter: Arc, +} + +impl Session { + /// Call `WireGuardDaitaActivate` for an existing WireGuard interface + pub(super) fn from_adapter(adapter: Arc) -> io::Result { + // SAFETY: `WgNtAdapter` has a valid adapter handle + unsafe { + adapter + .dll_handle + .daita_activate(adapter.handle, EVENTS_CAPACITY, ACTIONS_CAPACITY) + }?; + Ok(Self { adapter }) + } + + pub fn receive_events<'a>( + &self, + buffer: &'a mut [Event; EVENTS_CAPACITY], + ) -> io::Result<&'a [Event]> { + let num_events = unsafe { + // SAFETY: The adapter is valid, and the buffer is large enough to accommodate all + // events. + self.adapter + .dll_handle + .daita_receive_events(self.adapter.handle, buffer.as_mut_ptr())? + }; + Ok(unsafe { std::slice::from_raw_parts(buffer.as_ptr(), num_events) }) + } + + pub fn send_action(&self, action: &Action) -> io::Result<()> { + // SAFETY: The adapter is valid + unsafe { + self.adapter + .dll_handle + .daita_send_action(self.adapter.handle, action) + } + } + + pub fn event_data_available_event(&self) -> RawHandle { + // SAFETY: The adapter is valid + // This never fails when there's a DAITA session + unsafe { + self.adapter + .dll_handle + .daita_event_data_available_event(self.adapter.handle) + .unwrap() + } + } +} + +fn maybenot_event_from_event( + event: &Event, + machine_ids: &MachineMap, + override_size: Option, +) -> Option { + let xmit_bytes = override_size.unwrap_or(event.xmit_bytes); + match event.event_type { + EventType::PaddingReceived => Some(maybenot::framework::TriggerEvent::PaddingRecv { + bytes_recv: xmit_bytes, + }), + EventType::NonpaddingSent => Some(maybenot::framework::TriggerEvent::NonPaddingSent { + bytes_sent: xmit_bytes, + }), + EventType::NonpaddingReceived => Some(maybenot::framework::TriggerEvent::NonPaddingRecv { + bytes_recv: xmit_bytes, + }), + EventType::PaddingSent => Some(maybenot::framework::TriggerEvent::PaddingSent { + bytes_sent: xmit_bytes, + machine: machine_ids.get_machine_id(event.user_context)?.to_owned(), + }), + } +} + +/// Handle for a set of DAITA machines. +/// Note: `close` is NOT called implicitly when this is dropped. +pub struct MachinistHandle { + quit_event: talpid_windows::sync::Event, +} + +impl MachinistHandle { + fn new(quit_event: &talpid_windows::sync::Event) -> io::Result { + Ok(MachinistHandle { + quit_event: quit_event.duplicate()?, + }) + } + + /// Signal quit event + pub fn close(&self) -> io::Result<()> { + self.quit_event.set() + } +} + +pub struct Machinist { + daita: Arc, + machine_ids: MachineMap, + machine_tasks: HashMap>, + tokio_handle: tokio::runtime::Handle, + quit_event: talpid_windows::sync::Event, + peer: PublicKey, + override_size: Option, +} + +// TODO: This is silly. Let me use the raw ID of MachineId, please. +struct MachineMap { + id_to_num: HashMap, + num_to_id: HashMap, +} + +impl MachineMap { + fn new() -> Self { + Self { + id_to_num: HashMap::new(), + num_to_id: HashMap::new(), + } + } + + fn get_or_create_raw_id(&mut self, machine_id: MachineId) -> usize { + *self.id_to_num.entry(machine_id).or_insert_with(|| { + let raw_id = self.num_to_id.len(); + self.num_to_id.insert(raw_id, machine_id); + raw_id + }) + } + + fn get_machine_id(&self, raw_id: usize) -> Option<&MachineId> { + self.num_to_id.get(&raw_id) + } +} + +impl Machinist { + /// Spawn an actor that handles scheduling of Maybenot actions and forwards DAITA events to the framework. + pub fn spawn( + resource_dir: &Path, + daita: Session, + peer: PublicKey, + mtu: u16, + ) -> std::result::Result { + const MAX_PADDING_BYTES: f64 = 0.0; + const MAX_BLOCKING_BYTES: f64 = 0.0; + + static MAYBENOT_MACHINES: OnceCell> = OnceCell::new(); + + let machines = MAYBENOT_MACHINES.get_or_try_init(|| { + let path = resource_dir.join("maybenot_machines"); + log::debug!("Reading maybenot machines from {}", path.display()); + + let mut machines = vec![]; + let machines_str = fs::read_to_string(path).map_err(Error::EnumerateMachines)?; + for machine_str in machines_str.lines() { + let machine_str = machine_str.trim(); + if matches!(machine_str.chars().next(), None | Some('#')) { + continue; + } + log::debug!("Adding maybenot machine: {machine_str}"); + machines.push( + machine_str + .parse::() + .map_err(|_error| Error::InvalidMachine(machine_str.to_owned()))?, + ); + } + Ok(machines) + })?; + + let quit_event = + talpid_windows::sync::Event::new(true, false).map_err(Error::InitializeQuitEvent)?; + let handle = MachinistHandle::new(&quit_event).map_err(Error::InitializeHandle)?; + + let framework = maybenot::framework::Framework::new( + machines.clone(), + MAX_PADDING_BYTES, + MAX_BLOCKING_BYTES, + mtu, + std::time::Instant::now(), + ) + .map_err(|error| Error::InitializeMaybenot(error.to_string()))?; + + let daita = Arc::new(daita); + let tokio_handle = tokio::runtime::Handle::current(); + + std::thread::spawn(move || { + Self { + daita, + machine_ids: MachineMap::new(), + machine_tasks: HashMap::new(), + tokio_handle, + quit_event, + peer, + // TODO: We're assuming that constant packet size is always enabled here + override_size: Some(mtu), + } + .event_loop(framework); + }); + + Ok(handle) + } + + fn event_loop( + mut self, + mut framework: maybenot::framework::Framework>, + ) { + use windows_sys::Win32::Foundation::WAIT_OBJECT_0; + + loop { + if unsafe { WaitForSingleObject(self.quit_event.as_raw(), 0) } == WAIT_OBJECT_0 { + break; + } + + let events = match self.wait_for_events() { + Ok(events) => { + if events.is_empty() { + break; + } + events + } + Err(error) => { + log::error!("Error while waiting for DAITA events: {error}"); + break; + } + }; + + for action in framework.trigger_events(&events, std::time::Instant::now()) { + self.handle_action(action); + } + } + + log::debug!("Stopped DAITA event loop"); + } + + fn handle_action(&mut self, action: &maybenot::framework::Action) { + match *action { + maybenot::framework::Action::Cancel { machine } => { + let raw_id = self.machine_ids.get_or_create_raw_id(machine); + + // Drop all scheduled actions for a given machine + if let Some(task) = self.machine_tasks.get_mut(&raw_id) { + task.abort(); + } + } + maybenot::framework::Action::InjectPadding { + timeout, + size, + machine, + replace, + .. + } => { + let peer = self.peer.clone(); + + let raw_id = self.machine_ids.get_or_create_raw_id(machine); + self.machine_tasks.entry(raw_id).and_modify(|f| f.abort()); + + let action = Action { + peer: *peer.as_bytes(), + action_type: ActionType::InjectPadding, + user_context: raw_id, + payload: ActionPayload { + padding: PaddingAction { + byte_count: size, + replace: if replace { 1 } else { 0 }, + }, + }, + }; + + if timeout == Duration::ZERO { + if let Err(error) = self.daita.send_action(&action) { + log::error!("Failed to send DAITA action: {error}"); + } + } else { + // Schedule action on the tokio runtime + let daita = Arc::downgrade(&self.daita); + let task = self.tokio_handle.spawn(async move { + tokio::time::sleep(timeout).await; + + let Some(daita) = daita.upgrade() else { return }; + + if let Err(error) = daita.send_action(&action) { + log::error!("Failed to send DAITA action: {error}"); + } + }); + self.machine_tasks.insert(raw_id, task); + } + } + maybenot::framework::Action::BlockOutgoing { .. } => {} + } + } + + /// Take all events from the ring buffer while there are any left. + /// If there are no events available, wait for events to arrive. + /// Otherwise, break and return a non-zero number of events to be processed. + /// If the quit event was signaled, this returns an empty vector. + fn wait_for_events(&mut self) -> io::Result> { + use windows_sys::Win32::Foundation::WAIT_OBJECT_0; + + let wait_events = [ + self.quit_event.as_raw(), + self.daita.event_data_available_event() as isize, + ]; + + let mut event_buffer: [Event; EVENTS_CAPACITY] = unsafe { std::mem::zeroed() }; + + loop { + match self.daita.receive_events(&mut event_buffer) { + Ok(events) => { + let converted_events: Vec<_> = events + .iter() + .filter(|event| &event.peer == self.peer.as_bytes()) + .filter_map(|event| { + maybenot_event_from_event(event, &self.machine_ids, self.override_size) + }) + .collect(); + if !converted_events.is_empty() { + return Ok(converted_events); + } + // Try again if we only received events for irrelevant peers + } + Err(error) => { + if error.raw_os_error() == Some(ERROR_NO_MORE_ITEMS as i32) { + let wait_result = unsafe { + WaitForMultipleObjects( + u32::try_from(wait_events.len()).unwrap(), + wait_events.as_ptr(), + 0, + INFINITE, + ) + }; + + if wait_result == WAIT_OBJECT_0 { + // Quit event signaled + break Ok(vec![]); + } + if wait_result == WAIT_OBJECT_0 + 1 { + // Event object signaled -- try to receive more events + continue; + } + } + break Err(std::io::Error::last_os_error()); + } + } + } + } +} diff --git a/talpid-wireguard/src/wireguard_nt.rs b/talpid-wireguard/src/wireguard_nt/mod.rs similarity index 86% rename from talpid-wireguard/src/wireguard_nt.rs rename to talpid-wireguard/src/wireguard_nt/mod.rs index 10cc45b38418..6acd6dc71070 100644 --- a/talpid-wireguard/src/wireguard_nt.rs +++ b/talpid-wireguard/src/wireguard_nt/mod.rs @@ -9,14 +9,14 @@ use futures::SinkExt; use ipnetwork::IpNetwork; use once_cell::sync::{Lazy, OnceCell}; use std::{ - ffi::CStr, + ffi::{c_uchar, CStr}, fmt, future::Future, - io, mem, - mem::MaybeUninit, + io, + mem::{self, MaybeUninit}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, os::windows::io::RawHandle, - path::Path, + path::{Path, PathBuf}, pin::Pin, ptr, sync::{Arc, Mutex}, @@ -38,6 +38,8 @@ use windows_sys::{ }, }; +mod daita; + static WG_NT_DLL: OnceCell = OnceCell::new(); static ADAPTER_TYPE: Lazy = Lazy::new(|| U16CString::from_str("Mullvad").unwrap()); static ADAPTER_ALIAS: Lazy = Lazy::new(|| U16CString::from_str("Mullvad").unwrap()); @@ -159,12 +161,23 @@ pub enum Error { /// Failed to parse data returned by the driver #[error("Failed to parse data returned by wireguard-nt")] InvalidConfigData, + + /// DAITA machinist failed + #[error("Failed to enable DAITA on tunnel device")] + EnableTunnelDaita(#[source] io::Error), + + /// DAITA machinist failed + #[error("Failed to initialize DAITA machinist")] + InitializeMachinist(#[source] daita::Error), } pub struct WgNtTunnel { + resource_dir: PathBuf, + config: Arc>, device: Option>, interface_name: String, setup_handle: tokio::task::JoinHandle<()>, + daita_handle: Option, _logger_handle: LoggerHandle, } @@ -305,6 +318,7 @@ bitflags! { const REPLACE_ALLOWED_IPS = 0b00100000; const REMOVE = 0b01000000; const UPDATE = 0b10000000; + const HAS_CONSTANT_PACKET_SIZE = 0b100000000; } } @@ -322,6 +336,7 @@ struct WgPeer { rx_bytes: u64, last_handshake: u64, allowed_ips_count: u32, + constant_packet_size: c_uchar, } #[derive(Clone, Copy)] @@ -446,7 +461,7 @@ impl WgNtTunnel { let device = Some(device2.clone()); let setup_future = setup_ip_listener( - device2, + device2.clone(), u32::from(config.mtu), config.tunnel.addresses.iter().any(|addr| addr.is_ipv6()), ); @@ -457,17 +472,50 @@ impl WgNtTunnel { }); Ok(WgNtTunnel { + resource_dir: resource_dir.to_owned(), + config: Arc::new(Mutex::new(config.clone())), device, interface_name, setup_handle, + daita_handle: None, _logger_handle: logger_handle, }) } fn stop_tunnel(&mut self) { self.setup_handle.abort(); + if let Some(daita_handle) = self.daita_handle.take() { + let _ = daita_handle.close(); + } let _ = self.device.take(); } + + fn spawn_machinist(&mut self) -> Result<()> { + if let Some(handle) = self.daita_handle.take() { + log::info!("Stopping previous DAITA machines"); + let _ = handle.close(); + } + + let Some(device) = self.device.clone() else { + log::debug!("Tunnel is stopped; not starting machines"); + return Ok(()); + }; + + let config = self.config.lock().unwrap(); + + log::info!("Initializing DAITA for wireguard device"); + let session = daita::Session::from_adapter(device).map_err(Error::EnableTunnelDaita)?; + self.daita_handle = Some( + daita::Machinist::spawn( + &self.resource_dir, + session, + config.entry_peer.public_key.clone(), + config.mtu, + ) + .map_err(Error::InitializeMachinist)?, + ); + Ok(()) + } } async fn setup_ip_listener(device: Arc, mtu: u32, has_ipv6: bool) -> Result<()> { @@ -622,6 +670,11 @@ struct WgNtDll { func_set_adapter_state: WireGuardSetStateFn, func_set_logger: WireGuardSetLoggerFn, func_set_adapter_logging: WireGuardSetAdapterLoggingFn, + + func_daita_activate: daita::bindings::WireGuardDaitaActivateFn, + func_daita_event_data_available_event: daita::bindings::WireGuardDaitaEventDataAvailableEventFn, + func_daita_receive_events: daita::bindings::WireGuardDaitaReceiveEventsFn, + func_daita_send_action: daita::bindings::WireGuardDaitaSendActionFn, } unsafe impl Send for WgNtDll {} @@ -694,6 +747,30 @@ impl WgNtDll { CStr::from_bytes_with_nul(b"WireGuardSetAdapterLogging\0").unwrap(), )?) as *const _ as *const _) }, + func_daita_activate: unsafe { + *((&get_proc_fn( + handle, + CStr::from_bytes_with_nul(b"WireGuardDaitaActivate\0").unwrap(), + )?) as *const _ as *const _) + }, + func_daita_event_data_available_event: unsafe { + *((&get_proc_fn( + handle, + CStr::from_bytes_with_nul(b"WireGuardDaitaEventDataAvailableEvent\0").unwrap(), + )?) as *const _ as *const _) + }, + func_daita_receive_events: unsafe { + *((&get_proc_fn( + handle, + CStr::from_bytes_with_nul(b"WireGuardDaitaReceiveEvents\0").unwrap(), + )?) as *const _ as *const _) + }, + func_daita_send_action: unsafe { + *((&get_proc_fn( + handle, + CStr::from_bytes_with_nul(b"WireGuardDaitaSendAction\0").unwrap(), + )?) as *const _ as *const _) + }, }) } @@ -790,6 +867,52 @@ impl WgNtDll { } Ok(()) } + + pub unsafe fn daita_activate( + &self, + adapter: RawHandle, + events_capacity: usize, + actions_capacity: usize, + ) -> io::Result<()> { + if (self.func_daita_activate)(adapter, events_capacity, actions_capacity) == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } + + pub unsafe fn daita_event_data_available_event( + &self, + adapter: RawHandle, + ) -> io::Result { + let ready_event = (self.func_daita_event_data_available_event)(adapter); + if ready_event.is_null() { + return Err(io::Error::last_os_error()); + } + Ok(ready_event) + } + + pub unsafe fn daita_receive_events( + &self, + adapter: RawHandle, + events: *mut daita::Event, + ) -> io::Result { + let num_events = (self.func_daita_receive_events)(adapter, events); + if num_events == 0 { + return Err(io::Error::last_os_error()); + } + Ok(num_events) + } + + pub unsafe fn daita_send_action( + &self, + adapter: RawHandle, + action: *const daita::Action, + ) -> io::Result<()> { + if (self.func_daita_send_action)(adapter, action) == 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + } } impl Drop for WgNtDll { @@ -816,11 +939,13 @@ fn serialize_config(config: &Config) -> Result>> { buffer.extend(as_uninit_byte_slice(&header)); for peer in config.peers() { - let flags = if peer.psk.is_some() { - WgPeerFlag::HAS_PRESHARED_KEY | WgPeerFlag::HAS_PUBLIC_KEY | WgPeerFlag::HAS_ENDPOINT - } else { - WgPeerFlag::HAS_PUBLIC_KEY | WgPeerFlag::HAS_ENDPOINT - }; + let mut flags = WgPeerFlag::HAS_PUBLIC_KEY + | WgPeerFlag::HAS_ENDPOINT + | WgPeerFlag::HAS_CONSTANT_PACKET_SIZE; + if peer.psk.is_some() { + flags |= WgPeerFlag::HAS_PRESHARED_KEY; + } + let constant_packet_size = if peer.constant_packet_size { 1 } else { 0 }; let wg_peer = WgPeer { flags, reserved: 0, @@ -836,6 +961,7 @@ fn serialize_config(config: &Config) -> Result>> { rx_bytes: 0, last_handshake: 0, allowed_ips_count: u32::try_from(peer.allowed_ips.len()).unwrap(), + constant_packet_size, }; buffer.extend(as_uninit_byte_slice(&wg_peer)); @@ -960,13 +1086,16 @@ impl Tunnel for WgNtTunnel { config: Config, ) -> Pin> + Send>> { let device = self.device.clone(); + let current_config = self.config.clone(); Box::pin(async move { let Some(device) = device else { log::error!("Failed to set config: No tunnel device"); return Err(super::TunnelError::SetConfigError); }; - device.set_config(&config).map_err(|error| { + let mut current_config = current_config.lock().unwrap(); + *current_config = config; + device.set_config(¤t_config).map_err(|error| { log::error!( "{}", error.display_chain_with_msg("Failed to set wg-nt tunnel config") @@ -975,6 +1104,16 @@ impl Tunnel for WgNtTunnel { }) }) } + + fn start_daita(&mut self) -> std::result::Result<(), crate::TunnelError> { + self.spawn_machinist().map_err(|error| { + log::error!( + "{}", + error.display_chain_with_msg("Failed to start DAITA for wg-nt tunnel") + ); + super::TunnelError::SetConfigError + }) + } } pub fn as_uninit_byte_slice(value: &T) -> &[mem::MaybeUninit] { @@ -984,7 +1123,6 @@ pub fn as_uninit_byte_slice(value: &T) -> &[mem::MaybeUninit = Lazy::new(|| Interface { @@ -1026,7 +1168,9 @@ mod tests { peers_count: 1, }, p0: WgPeer { - flags: WgPeerFlag::HAS_PUBLIC_KEY | WgPeerFlag::HAS_ENDPOINT, + flags: WgPeerFlag::HAS_PUBLIC_KEY + | WgPeerFlag::HAS_ENDPOINT + | WgPeerFlag::HAS_CONSTANT_PACKET_SIZE, reserved: 0, public_key: *WG_PUBLIC_KEY.as_bytes(), preshared_key: [0; WIREGUARD_KEY_LENGTH], @@ -1039,6 +1183,8 @@ mod tests { rx_bytes: 0, last_handshake: 0, allowed_ips_count: 1, + #[cfg(target_os = "windows")] + constant_packet_size: 0, }, p0_allowed_ip_0: WgAllowedIp { address: WgIpAddr::from("1.3.3.0".parse::().unwrap()), diff --git a/windows/driverlogic/driverlogic.vcxproj b/windows/driverlogic/driverlogic.vcxproj index c68d0568ab82..ab9e6c19b5f5 100644 --- a/windows/driverlogic/driverlogic.vcxproj +++ b/windows/driverlogic/driverlogic.vcxproj @@ -61,7 +61,7 @@ true _DEBUG;_CONSOLE;%(PreprocessorDefinitions) true - $(ProjectDir)../../dist-assets/binaries/x86_64-pc-windows-msvc/;$(ProjectDir)../windows-libraries/src/;$(ProjectDir)../ + $(ProjectDir)../../dist-assets/binaries/x86_64-pc-windows-msvc/;$(ProjectDir)../../dist-assets/binaries/wireguard-nt/api/;$(ProjectDir)../windows-libraries/src/;$(ProjectDir)../ stdcpp20 MultiThreadedDebug @@ -83,7 +83,7 @@ NDEBUG;_CONSOLE;%(PreprocessorDefinitions) true stdcpp20 - $(ProjectDir)../../dist-assets/binaries/x86_64-pc-windows-msvc/;$(ProjectDir)../windows-libraries/src/;$(ProjectDir)../ + $(ProjectDir)../../dist-assets/binaries/x86_64-pc-windows-msvc/;$(ProjectDir)../../dist-assets/binaries/wireguard-nt/api/;$(ProjectDir)../windows-libraries/src/;$(ProjectDir)../ MultiThreaded @@ -115,7 +115,7 @@ - + diff --git a/windows/driverlogic/driverlogic.vcxproj.filters b/windows/driverlogic/driverlogic.vcxproj.filters index bd73d300299b..fae74cfdee3e 100644 --- a/windows/driverlogic/driverlogic.vcxproj.filters +++ b/windows/driverlogic/driverlogic.vcxproj.filters @@ -26,6 +26,6 @@ - + \ No newline at end of file diff --git a/windows/driverlogic/src/driverlogic.cpp b/windows/driverlogic/src/driverlogic.cpp index 7cd8cf5f51fc..795960b6d2f9 100644 --- a/windows/driverlogic/src/driverlogic.cpp +++ b/windows/driverlogic/src/driverlogic.cpp @@ -4,7 +4,7 @@ #include "service.h" #include "log.h" #include "wintun.h" -#include "wireguard.h" +#include "wireguard_dll.h" #include "devenum.h" #include #include diff --git a/windows/driverlogic/src/wireguard.h b/windows/driverlogic/src/wireguard_dll.h similarity index 96% rename from windows/driverlogic/src/wireguard.h rename to windows/driverlogic/src/wireguard_dll.h index e99da56c05c5..7260ce5b4dfa 100644 --- a/windows/driverlogic/src/wireguard.h +++ b/windows/driverlogic/src/wireguard_dll.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include "util.h"