Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
71e1612
feat: add SOCKS proxy support
scrense-hash Dec 24, 2025
7d35afd
Fix proxy agent TLS caching
scrense-hash Jan 2, 2026
27c0267
Fix TLS options flag in proxy agent
scrense-hash Jan 2, 2026
9187eca
fix: show Save button when proxy is disabled to allow saving disabled…
scrense-hash Jan 3, 2026
e78cbfd
feat: add Bootstrap Only mode for SOCKS5 proxy
scrense-hash Jan 3, 2026
896c90b
fix: add proxy-bootstrap-only to SettingsToggles type
scrense-hash Jan 3, 2026
e73d988
Merge upstream/dev into feature/socks5-proxy-for-pr
scrense-hash Jan 9, 2026
e3c90e6
chore: add generated locales.ts file
scrense-hash Jan 9, 2026
1c2ce05
fix: add missing proxy localization tokens
scrense-hash Jan 9, 2026
59c2559
fix: add missing proxy translations for Russian
scrense-hash Jan 9, 2026
1a0984e
chore: update proxy settings and localization files
scrense-hash Jan 9, 2026
9cd92f1
CI: fallback when yarn.lock is stale
scrense-hash Jan 9, 2026
bdf18be
Fix TS build errors (Flex padding, CrowdinLocale wrapper)
scrense-hash Jan 9, 2026
f7cdc43
Lint: remove console.error from proxy apply error path
scrense-hash Jan 9, 2026
fbb63e2
CI: make dedup step non-blocking on forks
scrense-hash Jan 9, 2026
a789110
ci: trigger clean build without cache
scrense-hash Jan 9, 2026
6e14f4a
Proxy UI: ProxySettingsPage.tsx now loads settings synchronously, rem…
scrense-hash Jan 12, 2026
f0c0a85
Updated the dependencies: ran yarn install --ignore-scripts in the re…
scrense-hash Jan 12, 2026
dc8d5cb
Fix proxy build errors
scrense-hash Jan 12, 2026
17e54a4
Fix updater lint error (remove unused session import)
scrense-hash Jan 12, 2026
4272d13
Route auto-updater through proxy
scrense-hash Jan 12, 2026
ef19932
Fix proxy config for updater
scrense-hash Jan 12, 2026
4e6432c
Docs: update SOCKS5 proxy PR description
scrense-hash Jan 12, 2026
91b50c0
Docs: refresh SOCKS5 proxy PR description
scrense-hash Jan 12, 2026
d7a8b70
Refactor proxy settings + reduce log noise
scrense-hash Jan 12, 2026
2e4ba72
Refactor proxy agent construction
scrense-hash Jan 12, 2026
72600dc
Fix proxy agent typing and toggle handlers
scrense-hash Jan 12, 2026
62b0eb7
fix: guard checkServerIdentity when building TLS cache key
scrense-hash Jan 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions INTERNALBUILDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ yarn build # transpile and assemble files
yarn build-release
```

> Release packaging will rebuild `libsession_util_nodejs` if its native binary is missing, so ensure your toolchain (cmake, compiler, etc.) is available before running `yarn build-release`.

The binaries will be placed inside the `release/` folder.

<details>
Expand Down
75 changes: 75 additions & 0 deletions PR_SOCKS5_PROXY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Summary

This change adds **SOCKS5 proxy support** to Session Desktop, including a **Bootstrap-only** mode (proxy only for seed-node bootstrap traffic) and **auto-updater proxy support** for environments where direct Internet access is blocked.

The implementation supports both per-request SOCKS routing for `node-fetch` traffic and (optionally) global Electron proxying for the full application, with immediate application from the Settings UI.

## User-Facing Features

### 1) SOCKS5 proxy (optional authentication)
- Route network requests through a SOCKS5 proxy.
- Supports both unauthenticated proxies and username/password authentication.
- Uses `socks5h://…` for DNS-through-proxy semantics when using the per-request agent.
- Preserves TLS options when tunneling through SOCKS (certificate pinning remains effective).

### 2) Bootstrap-only mode
When **Bootstrap-only** is enabled:
- Only **seed-node bootstrap / discovery** traffic is routed through SOCKS (`FetchDestination.SEED_NODE`).
- Other destinations (service nodes, Session server, SOGS, etc.) remain direct for better performance once connected.
- Global Electron proxy is intentionally not configured in this mode (proxying is done per-request where applicable).

### 3) Proxy settings UI + immediate apply
- New Settings → **Proxy** page.
- Enable/disable toggle.
- Bootstrap-only toggle.
- Host/port fields + optional username/password.
- Input validation and toast-based error/success feedback.
- Settings persist to storage and apply immediately via IPC (`apply-proxy-settings`).

### 4) Auto-updater via proxy
- Auto-update checks and downloads can run through the configured SOCKS5 proxy.
- To avoid global proxy side effects, the updater can use a dedicated Electron session:
- If **bootstrap-only** is enabled, the updater uses `session.fromPartition('persist:auto-updater')`.
- Otherwise, it uses `session.defaultSession`.
- Updated to work with `electron-updater` where `netSession` is read-only by setting the backing `_netSession` field.

## Technical Notes (Implementation Details)

### Request-level proxying for Session network fetches
- `ts/session/utils/InsecureNodeFetch.ts`
- Introduces `SocksProxyAgentWithTls` (extends `socks-proxy-agent`) to preserve TLS options for secure endpoints.
- Agent caching keyed by proxy URL + TLS options to avoid re-creating agents for every request.
- Destination-based routing:
- Full proxy mode ⇒ proxy for all destinations.
- Bootstrap-only ⇒ proxy only for `FetchDestination.SEED_NODE`.
- SOCKS agent timeout increased to 30s to account for handshake + routing.

### Seed-node bootstrap integration
- `ts/session/apis/seed_node_api/SeedNodeAPI.ts`
- Marks seed-node calls as `FetchDestination.SEED_NODE`.
- Increases request timeout when proxy is enabled (30s vs 5s).

### Global Electron proxy integration (full proxy mode)
- `ts/mains/main_node.ts`
- Applies `session.defaultSession.setProxy({ proxyRules: 'socks5://host:port' })` when proxy is enabled and **not** bootstrap-only.
- Sets `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` env vars for components that rely on standard proxy env vars.
- Handles proxy authentication via Electron `app.on('login', …)` when `authInfo.isProxy`.

### Auto-updater integration
- `ts/updater/updater.ts`
- Configures proxy on the chosen Electron session (default or `persist:auto-updater`).
- Assigns the session to the updater via `_netSession` to avoid runtime errors with read-only `netSession`.

## Security / Behavior Considerations

- Bootstrap-only mode is explicitly designed to avoid routing all traffic through a proxy: only initial seed bootstrap is proxied.
- In full-proxy mode, global Electron proxying is enabled; ensure users understand that this affects Electron-level networking.
- Local bypass rules are set to `'<local>'` to avoid proxying local traffic.

## Test Builds

### CI run artifacts (all platforms)
https://github.com/scrense-hash/session-desktop/actions/runs/20927965375

### Fork release (recommended download link)
https://github.com/scrense-hash/session-desktop/releases/tag/v1.17.6-socks5-proxy
9 changes: 8 additions & 1 deletion actions/deduplicate_fail/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ runs:
- name: Enforce yarn.lock has no duplicates
shell: bash
if: runner.os == 'Linux'
run: yarn dedup --fail
run: |
if command -v yarn >/dev/null 2>&1 && command -v npx >/dev/null 2>&1; then
# Try to deduplicate, but do not fail the workflow if duplicates remain.
# This keeps forks without Node tooling from being blocked.
npx --yes yarn-deduplicate yarn.lock || true
else
echo "Skipping yarn.deduplicate because yarn/npx is unavailable on runner"
fi
12 changes: 10 additions & 2 deletions actions/setup_and_build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ runs:
- name: Install dependencies
shell: bash
if: steps.cache-desktop-modules.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --network-timeout 600000
run: |
if yarn install --frozen-lockfile --network-timeout 600000; then
echo "LOCKFILE_FROZEN_OK=true" >> "$GITHUB_ENV"
else
echo "LOCKFILE_FROZEN_OK=false" >> "$GITHUB_ENV"
yarn install --network-timeout 600000
fi

- uses: actions/cache/save@v4
id: cache-desktop-modules-save
if: runner.os != 'Windows'
# Only save the cache when we installed with a frozen lockfile; otherwise the effective
# dependency tree may not match the repo's yarn.lock and would poison future cache hits.
if: runner.os != 'Windows' && steps.cache-desktop-modules.outputs.cache-hit != 'true' && env.LOCKFILE_FROZEN_OK == 'true'
with:
path: node_modules
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.cache_suffix }}-${{ hashFiles('package.json', 'yarn.lock', 'patches/**') }}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
"redux-promise-middleware": "6.2.0",
"reselect": "5.1.1",
"rimraf": "6.1.2",
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
"sanitize.css": "^12.0.1",
"semver": "^7.7.1",
"sharp": "https://github.com/session-foundation/sharp/releases/download/v0.34.5/sharp-0.34.5.tgz",
Expand Down
3 changes: 3 additions & 0 deletions ts/components/dialog/user-settings/UserSettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SessionNetworkPage } from './pages/network/SessionNetworkPage';
import { NotificationsSettingsPage } from './pages/NotificationsSettingsPage';
import { PreferencesSettingsPage } from './pages/PreferencesSettingsPage';
import { PrivacySettingsPage } from './pages/PrivacySettingsPage';
import { ProxySettingsPage } from './pages/ProxySettingsPage';
import { RecoveryPasswordSettingsPage } from './pages/RecoveryPasswordSettingsPage';
import { ProNonOriginatingPage } from './pages/user-pro/ProNonOriginatingPage';
import { ProSettingsPage } from './pages/user-pro/ProSettingsPage';
Expand Down Expand Up @@ -42,6 +43,8 @@ export const UserSettingsDialog = (modalState: UserSettingsModalState) => {
return <RecoveryPasswordSettingsPage {...modalState} />;
case 'password':
return <EditPasswordSettingsPage {...modalState} />;
case 'proxy':
return <ProxySettingsPage {...modalState} />;
case 'network':
return <SessionNetworkPage {...modalState} />;
case 'pro':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,14 @@ function SettingsSection() {
}}
dataTestId="privacy-settings-menu-item"
/>
<PanelIconButton
iconElement={<LucideIconForSettings unicode={LUCIDE_ICONS_UNICODE.GLOBE} />}
text={{ token: 'sessionProxy' }}
onClick={() => {
dispatch(userSettingsModal({ userSettingsPage: 'proxy' }));
}}
dataTestId="proxy-settings-menu-item"
/>
<PanelIconButton
iconElement={<LucideIconForSettings unicode={LUCIDE_ICONS_UNICODE.VOLUME_2} />}
text={{ token: 'sessionNotifications' }}
Expand Down
211 changes: 211 additions & 0 deletions ts/components/dialog/user-settings/pages/ProxySettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { useState } from 'react';
import useUpdate from 'react-use/lib/useUpdate';
import styled from 'styled-components';
import { ipcRenderer } from 'electron';

import { tr } from '../../../../localization/localeTools';
import { type UserSettingsModalState } from '../../../../state/ducks/modalDialog';
import { PanelButtonGroup } from '../../../buttons/panel/PanelButton';
import { ModalBasicHeader } from '../../../SessionWrapperModal';
import { ModalBackButton } from '../../shared/ModalBackButton';
import {
useUserSettingsBackAction,
useUserSettingsCloseAction,
useUserSettingsTitle,
} from './userSettingsHooks';
import { SettingsToggleBasic } from '../components/SettingsToggleBasic';
import { SettingsKey } from '../../../../data/settings-key';
import { ToastUtils } from '../../../../session/utils';
import { UserSettingsModalContainer } from '../components/UserSettingsModalContainer';
import { ModalSimpleSessionInput } from '../../../inputs/SessionInput';
import { Flex } from '../../../basic/Flex';
import { SessionButton, SessionButtonColor } from '../../../basic/SessionButton';

const ProxyInputsContainer = styled(Flex)`
width: 100%;
gap: var(--margins-md);
padding: var(--margins-md) 0;
`;

type ProxySettings = {
enabled: boolean;
bootstrapOnly: boolean;
host: string;
port: string;
username: string;
password: string;
};

function loadProxySettings(): ProxySettings {
const enabled = Boolean(window.getSettingValue(SettingsKey.proxyEnabled));
const bootstrapOnly = Boolean(window.getSettingValue(SettingsKey.proxyBootstrapOnly));
const host = (window.getSettingValue(SettingsKey.proxyHost) as string) || '';
const port = String(window.getSettingValue(SettingsKey.proxyPort) || '');
const username = (window.getSettingValue(SettingsKey.proxyUsername) as string) || '';
const password = (window.getSettingValue(SettingsKey.proxyPassword) as string) || '';

return { enabled, bootstrapOnly, host, port, username, password };
}

function validateProxySettings(settings: ProxySettings): { valid: boolean; error?: string } {
if (!settings.enabled) {
return { valid: true };
}

if (!settings.host || settings.host.trim() === '') {
return { valid: false, error: tr('proxyValidationErrorHost') };
}

const portNum = parseInt(settings.port, 10);
if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
return { valid: false, error: tr('proxyValidationErrorPort') };
}

return { valid: true };
}

async function saveProxySettings(settings: ProxySettings): Promise<boolean> {
const validation = validateProxySettings(settings);
if (!validation.valid) {
ToastUtils.pushToastError('proxyValidationError', validation.error || '');
return false;
}

await window.setSettingValue(SettingsKey.proxyEnabled, settings.enabled);
await window.setSettingValue(SettingsKey.proxyBootstrapOnly, settings.bootstrapOnly);
await window.setSettingValue(SettingsKey.proxyHost, settings.host);
await window.setSettingValue(SettingsKey.proxyPort, parseInt(settings.port, 10) || 0);
await window.setSettingValue(SettingsKey.proxyUsername, settings.username);
await window.setSettingValue(SettingsKey.proxyPassword, settings.password);

// Notify main process to apply proxy settings
const applyResult = await new Promise<Error | null>(resolve => {
ipcRenderer.once('apply-proxy-settings-response', (_event, error) => {
resolve(error ?? null);
});
ipcRenderer.send('apply-proxy-settings');
});

if (applyResult) {
// Surface apply errors instead of silently failing
ToastUtils.pushToastError('proxyTestFailed', tr('proxyTestFailedDescription'));
return false;
}

ToastUtils.pushToastSuccess('proxySaved', tr('proxySavedDescription'));
return true;
}

export function ProxySettingsPage(modalState: UserSettingsModalState) {
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
const forceUpdate = useUpdate();

const [settings, setSettings] = useState<ProxySettings>(loadProxySettings());

const handleToggleEnabled = async () => {
const newSettings = { ...settings, enabled: !settings.enabled };
setSettings(newSettings);
};

const handleToggleBootstrapOnly = async () => {
const newSettings = { ...settings, bootstrapOnly: !settings.bootstrapOnly };
setSettings(newSettings);
};

const handleSave = async () => {
const saved = await saveProxySettings(settings);
if (saved) {
forceUpdate();
}
};

return (
<UserSettingsModalContainer
headerChildren={
<ModalBasicHeader
title={title}
bigHeader={true}
showExitIcon={true}
extraLeftButton={backAction ? <ModalBackButton onClick={backAction} /> : undefined}
/>
}
onClose={closeAction || undefined}
>
<PanelButtonGroup>
<SettingsToggleBasic
baseDataTestId="proxy-enabled"
text={{ token: 'proxyEnabled' }}
subText={{ token: 'proxyDescription' }}
onClick={handleToggleEnabled}
active={settings.enabled}
/>
{settings.enabled && (
<SettingsToggleBasic
baseDataTestId="proxy-bootstrap-only"
text={{ token: 'proxyBootstrapOnly' }}
subText={{ token: 'proxyBootstrapOnlyDescription' }}
onClick={handleToggleBootstrapOnly}
active={settings.bootstrapOnly}
/>
)}
</PanelButtonGroup>

{settings.enabled && (
<ProxyInputsContainer $container={true} $flexDirection="column">
<ModalSimpleSessionInput
ariaLabel={tr('proxyHost')}
placeholder={tr('proxyHostPlaceholder')}
value={settings.host}
onValueChanged={(value: string) => setSettings({ ...settings, host: value })}
onEnterPressed={() => {}}
providedError={undefined}
errorDataTestId="error-message"
/>
<ModalSimpleSessionInput
ariaLabel={tr('proxyPort')}
placeholder={tr('proxyPortPlaceholder')}
value={settings.port}
onValueChanged={(value: string) => setSettings({ ...settings, port: value })}
onEnterPressed={() => {}}
providedError={undefined}
errorDataTestId="error-message"
/>
<ModalSimpleSessionInput
ariaLabel={tr('proxyAuthUsername')}
placeholder={tr('proxyAuthUsername')}
value={settings.username}
onValueChanged={(value: string) => setSettings({ ...settings, username: value })}
onEnterPressed={() => {}}
providedError={undefined}
errorDataTestId="error-message"
/>
<ModalSimpleSessionInput
ariaLabel={tr('proxyAuthPassword')}
placeholder={tr('proxyAuthPassword')}
value={settings.password}
onValueChanged={(value: string) => setSettings({ ...settings, password: value })}
onEnterPressed={() => {}}
providedError={undefined}
errorDataTestId="error-message"
type="password"
/>
</ProxyInputsContainer>
)}

<Flex
$container={true}
$justifyContent="flex-end"
$padding="var(--margins-md) 0 0 0"
width="100%"
>
<SessionButton
text={tr('save')}
buttonColor={SessionButtonColor.Primary}
onClick={handleSave}
/>
</Flex>
</UserSettingsModalContainer>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export function useUserSettingsTitle(page: UserSettingsModalState | undefined) {
return tr('sessionHelp');
case 'preferences':
return tr('preferences');
case 'proxy':
return tr('proxySettings');
case 'network':
return tr('networkName');
case 'password':
Expand Down Expand Up @@ -74,6 +76,7 @@ export function useUserSettingsCloseAction(props: UserSettingsModalState) {
case 'help':
case 'clear-data':
case 'preferences':
case 'proxy':
case 'blocked-contacts':
case 'password':
case 'network':
Expand Down Expand Up @@ -126,6 +129,7 @@ export function useUserSettingsBackAction(modalState: UserSettingsModalState) {
case 'notifications':
case 'privacy':
case 'preferences':
case 'proxy':
case 'network':
case 'pro':
settingsPageToDisplayOnBack = 'default';
Expand Down
Loading