Skip to content

Conversation

@scrense-hash
Copy link

@scrense-hash scrense-hash commented Jan 3, 2026

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

Fork release (recommended download link)

https://github.com/scrense-hash/session-desktop/releases/tag/v1.17.6-socks5-proxy

Testing

Tested scenarios:

  • ✅ Tor SOCKS5 proxy (localhost:9050)
  • ✅ Authenticated SOCKS5 proxies (username/password)
  • ✅ Unauthenticated SOCKS5 proxies
  • ✅ Bootstrap-only mode (SEED_NODE via proxy; other destinations direct)
  • ✅ Full proxy mode (global Electron proxy enabled; app traffic routed via SOCKS)
  • ✅ TLS/certificate pinning preserved when tunneling through SOCKS (SocksProxyAgentWithTls)
  • ✅ Auto-updater works via proxy (no netSession TypeError; uses dedicated session when bootstrap-only)

scrense-hash and others added 15 commits January 3, 2026 21:53
Add "Bootstrap Only" toggle in proxy settings to use proxy only for
initial connection phase (handshake). When enabled, only SEED_NODE
requests go through proxy, all other traffic is direct.

Changes:
- Add proxyBootstrapOnly setting key
- Add UI toggle in ProxySettingsPage
- Implement shouldUseProxyForDestination() logic
- Add localization for ru and en
- Update getProxyAgent() to accept destination parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Resolved conflicts:
- _locales/en/messages.json and _locales/ru/messages.json were deleted in upstream (migrated to new localization system)
- Added SOCKS5 proxy translations to new localization system (ts/localization/generated/english.ts and translations.ts)
- Updated package.json with SOCKS5 dependencies (smart-buffer, socks, socks-proxy-agent)
- Updated yarn.lock from upstream

New localization system includes all proxy-related strings:
- proxyEnabled, proxyHost, proxyPort
- proxyAuthUsername, proxyAuthPassword
- proxyBootstrapOnly, proxyBootstrapOnlyDescription
- proxySettings, proxySaved, proxyValidationError

All SOCKS5 proxy functionality maintained with Bootstrap Only mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This file was auto-generated by the localization code generator.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added sessionProxy and all related proxy tokens to generated localization files:
- sessionProxy, proxyEnabled, proxyDescription
- proxyBootstrapOnly, proxyBootstrapOnlyDescription
- proxyHost, proxyPort, proxyAuthUsername, proxyAuthPassword
- proxySaved, proxyValidationError tokens

This fixes the missing "Proxy" menu item in Settings UI.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added all proxy-related translations to Russian locale:
- sessionProxy, proxyEnabled, proxyDescription
- proxyBootstrapOnly, proxyBootstrapOnlyDescription
- proxyHost, proxyPort, proxyAuthUsername, proxyAuthPassword
- proxySaved, proxyValidationError and all related tokens

This completes the proxy UI localization.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@scrense-hash scrense-hash force-pushed the feature/socks5-proxy-for-pr branch 2 times, most recently from 9300e8d to fbb63e2 Compare January 9, 2026 16:41
@scrense-hash scrense-hash reopened this Jan 9, 2026
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file can be removed

package.json Outdated
"redux-promise-middleware": "6.2.0",
"reselect": "5.1.1",
"rimraf": "6.1.2",
"smart-buffer": "^4.2.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smart-buffer is a dependency of socks and socks-proxy-agent.
We don't seem to import it directly, so I think it can be removed from our explicit list of dependencies.

password: string;
};

async function loadProxySettings(): Promise<ProxySettings> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this async is not needed

Comment on lines 100 to 122
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
const forceUpdate = useUpdate();

const [settings, setSettings] = useState<ProxySettings>({
enabled: false,
bootstrapOnly: false,
host: '',
port: '1080',
username: '',
password: '',
});

const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
void (async () => {
const loadedSettings = await loadProxySettings();
setSettings(loadedSettings);
setIsLoading(false);
})();
}, []);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once loadProxySettings is not async this can become this, I think:

Suggested change
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
const forceUpdate = useUpdate();
const [settings, setSettings] = useState<ProxySettings>({
enabled: false,
bootstrapOnly: false,
host: '',
port: '1080',
username: '',
password: '',
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
void (async () => {
const loadedSettings = await loadProxySettings();
setSettings(loadedSettings);
setIsLoading(false);
})();
}, []);
const backAction = useUserSettingsBackAction(modalState);
const closeAction = useUserSettingsCloseAction(modalState);
const title = useUserSettingsTitle(modalState);
const forceUpdate = useUpdate();
const [settings, setSettings] = useState<ProxySettings>(loadProxySettings());


// Proxy settings
const proxyEnabled = 'proxy-enabled';
const proxyType = 'proxy-type';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think proxyType is used anywhere, to remove


// CRITICAL: Use longer timeout when proxy is enabled
// SOCKS handshake + proxy routing requires more time than direct connection
const requestTimeout = isProxyEnabled() ? 30000 : 10000;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment

Comment on lines 333 to 343
// Enhanced error logging for debugging proxy issues
const errorDetails = {
message: (e as Error).message,
name: (e as Error).name,
code: (e as any).code,
errno: (e as any).errno,
syscall: (e as any).syscall,
type: (e as any).type,
stack: (e as Error).stack?.split('\n').slice(0, 3).join('\n'),
};
window?.log?.error('insecureNodeFetch error', errorDetails);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is going to be very verbose.
Maybe split it with two debug levels:
one Window?.log?.error with just the insecureNodeFetch error: ${e.message} and one with window?.log?.debug with what you have currently

'[updater] SOCKS proxy is enabled, skipping auto-update check to prevent traffic leaks. Please update manually.'
);
return false;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have that logic work with proxy.

Comment on lines +120 to +121
"socks": "^2.8.3",
"socks-proxy-agent": "^8.0.4",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've added some packages but there is no corresponding changes in the yarn.lock. Why?

Comment on lines 309 to 312
// Extract TLS options from the original agent (if present) to preserve security settings
// This ensures certificate pinning and other TLS configurations work through the proxy
const tlsOptions = getTlsOptionsFromAgent(params.fetchOptions?.agent);
const proxyAgent = getProxyAgent(tlsOptions, params.destination);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not too sure about this way of grabbing some options from the original agent and copying them.

I'd much prefer have a way to build an agent ready to be used from the top, and that agent have the proxy options already provided.
Can you do that? Otherwise I can have a look once you've fixed the other issues.

…oves unnecessary async toggles, and simplifies the initial useEffect per Bilb’s suggestion; proxyType removed from settings-key.ts.

Proxy plumbing: insecureNodeFetch now builds a final agent from caller-provided TLS options (call sites pass explicit TLS opts for seed-node and snode agents) and keeps detailed errors at debug with a short error message. Seed/onion/session RPC calls supply TLS options explicitly to avoid introspection. Proxy metadata file SOCKS_PROXY_PATCH.md removed.
Proxy + updater: main process applyProxySettings sets proxy env vars (with auth) and default session proxy, clears them when disabled/bootstrap-only, and keeps credentials out of logs. Auto-updater uses session.defaultSession and no longer short-circuits when proxy is on, so updates can flow via the proxy.
…po, and regenerated yarn.lock based on the current package.json.

Ran yarn lint (Prettier adjusted the formatting in InsecureNodeFetch.ts; I reverted the formatting for locales.ts).
@scrense-hash
Copy link
Author

scrense-hash commented Jan 12, 2026

Further testing is required, DPI with bootstrap only enabled behaves inconsistently.
Снимок экрана от 2026-01-12 23-56-28

WARN 2026-01-12T19:37:30.975Z requestSnodesForPubkeyWithTargetNode attempt #3 failed. 1 retries left...
ERROR 2026-01-12T19:37:41.402Z insecureNodeFetch error: network timeout at: [REDACTED]
WARN 2026-01-12T19:37:41.402Z sendOnionRequestNoRetries error message: network timeout at: [REDACTED]
WARN 2026-01-12T19:37:41.403Z processOnionRequestErrorOnPath httpStatusCode: 8888 ciphertext:
WARN 2026-01-12T19:37:41.403Z [path] Got status: 8888
INFO 2026-01-12T19:37:41.403Z [onionPaths] incrementBadPathCountOrDrop starting with guard (...e86c5a), reason: "processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count"
INFO 2026-01-12T19:37:41.403Z [onionPaths] handling bad path for path index1, reason: "processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count"
WARN 2026-01-12T19:37:41.403Z Failure threshold reached for snode: (...7edde6); dropping it.
WARN 2026-01-12T19:37:41.405Z [snodePool] Dropping (...7edde6) from snode pool for reason: "Failure threshold reached for snode: (...7edde6); dropping it. (processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count)". 1471 snodes remaining in randomPool
INFO 2026-01-12T19:37:41.426Z [onionPaths] dropping snode (...7edde6) from path index: 1 with reason: "Failure threshold reached for snode: (...7edde6); dropping it. (processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count)"
WARN 2026-01-12T19:37:41.449Z Failure threshold reached for snode: (...2e815d); dropping it.
WARN 2026-01-12T19:37:41.449Z [snodePool] Dropping (...2e815d) from snode pool for reason: "Failure threshold reached for snode: (...2e815d); dropping it. (processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count)". 1470 snodes remaining in randomPool
INFO 2026-01-12T19:37:41.454Z [onionPaths] dropping snode (...2e815d) from path index: 1 with reason: "Failure threshold reached for snode: (...2e815d); dropping it. (processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count)"
WARN 2026-01-12T19:37:41.460Z [snodePool] Dropping (...e86c5a) from snode pool for reason: "processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count". 1469 snodes remaining in randomPool
INFO 2026-01-12T19:37:41.465Z [onionPaths] Dropping path starting with guard node (...e86c5a); index:1 with reason: "processAnyOtherErrorOnPath: Otherwise we increment the whole path failure count"
INFO 2026-01-12T19:37:41.467Z [onionPaths] buildNewOnionPaths - building new onion paths...

Снимок экрана от 2026-01-12 23-58-22

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants