Skip to content

feat: bind torrent client to a network adapter#2405

Open
Moyasee wants to merge 5 commits into
mainfrom
feat/GH-2023
Open

feat: bind torrent client to a network adapter#2405
Moyasee wants to merge 5 commits into
mainfrom
feat/GH-2023

Conversation

@Moyasee

@Moyasee Moyasee commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

When submitting this pull request, I confirm the following (please check the boxes):

  • I have read the Hydra documentation.
  • I have checked that there are no duplicate pull requests related to this request.
  • I have considered, and confirm that this submission is valuable to others.
  • I accept that this submission may not be used and the pull request may be closed at the discretion of the maintainers.

Fill in the PR content:

Closes #2023.

Adds a setting to bind the torrent client to a specific network adapter, so torrent traffic only goes through it. The main use case is forcing torrents through a VPN.

It works as a kill switch: while bound to an adapter, torrents only connect through that adapter, and downloads stop if it goes down instead of falling back to your regular connection and leaking traffic.

How it works

  • The selected adapter is applied to the libtorrent session through listen_interfaces and outgoing_interfaces, the same approach qBittorrent uses, so we bind by device name and it stays stable across VPN reconnects.
  • The choice is saved as a user preference (torrentNetworkInterface), applied live when changed, and re-applied on RPC startup before any torrent is added.
  • A new getNetworkInterfaces IPC handler lists the available adapters (via os.networkInterfaces()) for the dropdown. Loopback is filtered out.
  • New setting lives under Settings > Downloads, with a hint explaining the kill-switch behavior. If a previously selected adapter is currently unavailable (VPN down), it still shows in the dropdown so the saved choice is visible.
  • Scope is torrents only. Direct/debrid HTTP downloads are untouched.
  • Translations added for en, es, pt-BR, pt-PT and ru.

Notes

Device-name binding works across platforms with libtorrent 2.0. On Windows the adapter friendly names from os.networkInterfaces() are used, which match what libtorrent expects, but it would be good to get a Windows test in.

Testing

  • Select a VPN adapter, start a torrent, and confirm peer connections egress the VPN interface.
  • Bring the VPN down and confirm torrents stall instead of leaking, then restore.
  • Restart the app and confirm the saved adapter persists and is re-applied before downloads resume.

Adds a setting to bind the torrent client to a specific network
adapter, so torrent traffic only flows through it (e.g. a VPN). The
binding acts as a kill switch: if the selected adapter goes down,
torrents stop instead of leaking through the default connection.

The chosen adapter is applied to the libtorrent session via
listen_interfaces and outgoing_interfaces, persisted as a user
preference, and re-applied on RPC startup. A new IPC handler
enumerates the available adapters for the settings dropdown.

Closes #2023
@Moyasee Moyasee added the enhancement New feature or request label Jun 24, 2026
@greptile-apps

greptile-apps Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a setting under Downloads to bind the libtorrent session to a specific network adapter (e.g., a VPN), acting as a kill switch — if the adapter goes down, torrents stall instead of leaking over the default connection.

  • A new getNetworkInterfaces IPC handler enumerates non-loopback adapters via os.networkInterfaces() and surfaces them in a SelectField dropdown; adapters that were saved but are currently offline remain visible so the saved choice is always displayed.
  • DownloadManager.applyNetworkInterface applies the binding via a new set_network_interface RPC action to the Python libtorrent session (listen_interfaces + outgoing_interfaces), and is called both on RPC startup (before any seeding starts) and live when the user changes the setting.
  • Translations are added for en, es, pt-BR, pt-PT, and ru.

Confidence Score: 5/5

Safe to merge — the feature is self-contained and follows established patterns for RPC settings, with the interface applied before any torrent activity on startup.

The binding logic, IPC wiring, and UI are all consistent with how download-speed-limit was implemented. The DB save always precedes the RPC apply call, so the preference is applied correctly in all reachable flows. No data is at risk and the change is isolated to torrent traffic only.

download-manager.ts — the null vs. undefined distinction in applyNetworkInterface is worth a second look, though it is harmless under the current call ordering.

Important Files Changed

Filename Overview
python_rpc/main.py Adds normalize_network_interface and apply_network_interface helpers, and handles the new set_network_interface action; error-handled correctly, consistent with existing patterns.
src/main/services/download/download-manager.ts Adds getPersistedNetworkInterface/applyNetworkInterface and calls applyNetworkInterface in startRPC before seeding; the null ?? DB-read pattern conflates explicit null with undefined, though it is safe today due to save-before-apply ordering.
src/main/events/hardware/get-network-interfaces.ts New IPC handler that lists non-loopback interfaces using os.networkInterfaces(), sorts IPv4 before IPv6, and exports a clean NetworkInterface[] type.
src/main/events/user-preferences/update-user-preferences.ts Correctly wires torrentNetworkInterface preference updates to applyNetworkInterface; DB is persisted before apply is called, preserving correct ordering.
src/renderer/src/pages/settings/settings-context-downloads.tsx Adds SelectField with memoized options; correctly injects unavailable-adapter sentinel item; live-updates preferences on change.
src/types/level.types.ts Adds torrentNetworkInterface to UserPreferences and exports a new NetworkInterface type; no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Settings UI
    participant IPC as Electron IPC
    participant DM as DownloadManager
    participant DB as LevelDB
    participant RPC as PythonRPC
    participant LT as libtorrent session

    Note over UI,LT: User changes adapter in dropdown
    UI->>IPC: "updateUserPreferences({torrentNetworkInterface: "tun0"})"
    IPC->>DB: "put(userPreferences, {torrentNetworkInterface: "tun0"})"
    IPC->>DM: applyNetworkInterface("tun0")
    DM->>RPC: "call("action", {action: "set_network_interface", interface: "tun0"})"
    RPC->>LT: "apply_settings({listen_interfaces: "tun0:port", outgoing_interfaces: "tun0"})"
    LT-->>RPC: ok
    RPC-->>DM: ok

    Note over UI,LT: App restart
    DM->>RPC: spawn()
    DM->>DM: applyNetworkInterface() [no args]
    DM->>DB: get(userPreferences)
    DB-->>DM: "{torrentNetworkInterface: "tun0"}"
    DM->>RPC: "call("action", {action: "set_network_interface", interface: "tun0"})"
    RPC->>LT: "apply_settings({listen_interfaces: "tun0:port", outgoing_interfaces: "tun0"})"
    LT-->>RPC: ok
    DM->>DM: resumeSeeding / startDownload
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Settings UI
    participant IPC as Electron IPC
    participant DM as DownloadManager
    participant DB as LevelDB
    participant RPC as PythonRPC
    participant LT as libtorrent session

    Note over UI,LT: User changes adapter in dropdown
    UI->>IPC: "updateUserPreferences({torrentNetworkInterface: "tun0"})"
    IPC->>DB: "put(userPreferences, {torrentNetworkInterface: "tun0"})"
    IPC->>DM: applyNetworkInterface("tun0")
    DM->>RPC: "call("action", {action: "set_network_interface", interface: "tun0"})"
    RPC->>LT: "apply_settings({listen_interfaces: "tun0:port", outgoing_interfaces: "tun0"})"
    LT-->>RPC: ok
    RPC-->>DM: ok

    Note over UI,LT: App restart
    DM->>RPC: spawn()
    DM->>DM: applyNetworkInterface() [no args]
    DM->>DB: get(userPreferences)
    DB-->>DM: "{torrentNetworkInterface: "tun0"}"
    DM->>RPC: "call("action", {action: "set_network_interface", interface: "tun0"})"
    RPC->>LT: "apply_settings({listen_interfaces: "tun0:port", outgoing_interfaces: "tun0"})"
    LT-->>RPC: ok
    DM->>DM: resumeSeeding / startDownload
Loading

Reviews (2): Last reviewed commit: "refactor: address review feedback on net..." | Re-trigger Greptile

Comment thread python_rpc/main.py Outdated
Comment thread python_rpc/main.py Outdated
Comment thread src/renderer/src/pages/settings/settings-general.scss
@Moyasee

Moyasee commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add VPN Binding for Torrenting

1 participant