Skip to content

Add option to bind aMule to a specific network interface - #281

Merged
got3nks merged 13 commits into
amule-org:masterfrom
got3nks:feat/bind-to-interface
Jul 3, 2026
Merged

Add option to bind aMule to a specific network interface#281
got3nks merged 13 commits into
amule-org:masterfrom
got3nks:feat/bind-to-interface

Conversation

@got3nks

@got3nks got3nks commented Jul 3, 2026

Copy link
Copy Markdown

Closes #173.

The existing Address= setting binds to a local IP, which on Linux does not override the kernel's routing decision — outbound traffic can still leak via the default-route interface even when the bind IP belongs to a VPN tunnel. This adds a proper interface bind that pins the egress interface itself, closing that leak.

It uses IP_UNICAST_IF / IPV6_UNICAST_IF on Linux and Windows, and IP_BOUND_IF / IPV6_BOUND_IF on macOS/BSD. These require no elevated privileges, unlike SO_BINDTODEVICE which needs CAP_NET_RAW — aMule keeps running as a normal user.

The bind is applied to every socket aMule opens: the TCP listen socket, outbound TCP (client and server connections), and the UDP socket shared by the ed2k client, ed2k server and Kad.

The Connection preferences page gets a "Bind to network interface" control next to "Bind local address to". It is an editable drop-down populated with the machine's interfaces (getifaddrs on POSIX, GetAdaptersAddresses on Windows, loopback filtered out), but stays editable so an interface that is down when the dialog opens — a VPN tunnel — can still be typed in. The stored value is a POSIX interface name (en0, eth0, tun0), a Windows adapter friendly name (Ethernet, Wi-Fi), or a bare numeric index; empty means "any" (unchanged default). Like the address bind, it is a daemon-side setting stored as /eMule/NetworkInterface and hidden in the remote GUI. Changing it is flagged restart-needed, consistent with the TCP/UDP port settings.

Because it is a leak-prevention setting, the outcome is made visible: on startup a valid interface is confirmed in the log (Binding all network traffic to interface: <name>), and an unresolvable one produces a loud WARNING that traffic is not bound and may leave via the default route — so a typo can't silently defeat the protection. All resolution lives in one place in the socket layer, which the core reuses to validate the preference before any socket opens.

Tested: builds clean on macOS, Linux and Windows (monolithic, daemon, remote GUI, amulecmd, amuleweb and the unit tests). The interface-bind syscall was verified returning success as an unprivileged user on all three platforms.

got3nks added 8 commits July 3, 2026 11:28
The existing Address= setting binds to a local IP, which on Linux does
not override the kernel's routing decision - outbound traffic can leak
via the default-route interface even when the bind IP belongs to a VPN
tunnel (amule-project#173). This adds a proper interface bind.

Uses IP_UNICAST_IF / IPV6_UNICAST_IF (Linux, Windows) and IP_BOUND_IF /
IPV6_BOUND_IF (macOS/BSD), which need no elevated privileges (unlike
SO_BINDTODEVICE / CAP_NET_RAW). The interface is named (resolved via
if_nametoindex) or a bare index (the Windows path); empty means any.

Applied to every socket: TCP listen, outbound TCP, and the UDP socket
shared by ed2k client, ed2k server and Kad. New 'Bind to network
interface' field on the Connection preferences page, stored as
/eMule/NetworkInterface, hidden in the remote GUI like the address bind.
Replace the plain text field with an editable combo box populated with
the machine's network interfaces (getifaddrs on POSIX, GetAdaptersAddresses
on Windows), loopback filtered out. It stays editable so an interface that
is down when the dialog opens (a VPN tunnel) can still be typed in.

The stored value is unchanged in kind - a POSIX interface name or a Windows
adapter friendly name - and all resolution to an interface index is
centralized in LibSocketAsio.cpp's SetBoundInterface: if_nametoindex on
POSIX, GetAdaptersAddresses friendly-name lookup on Windows (needs iphlpapi,
linked into mulesocket), with a bare-numeric-index fallback. So the daemon
still owns resolution and works headless.
The first commit had LibSocketAsio.cpp (in the mulesocket library) call
thePrefs::GetNetworkInterface() directly. EC-only tools (amulecmd, amuleweb)
link mulesocket but not the full CPreferences, so they failed to link with
'undefined symbol: CPreferences::s_NetworkInterface'.

Keep mulesocket free of CPreferences: it now reads a module-local string set
by the core via SetSocketBindInterface(), which CamuleApp pushes from
thePrefs at startup (before any socket opens). A changed interface is flagged
restart-needed in the prefs dialog, consistent with the TCP/UDP port
settings that likewise only take effect on new sockets.
When a network interface is configured, print a normal-level line at
startup ('Binding all network traffic to interface: <name>') so the user
gets visible confirmation the VPN-leak protection is active, alongside the
listen-socket lines. Nothing is printed in the default (empty) case. The
per-socket detail stays on the logAsio debug channel.
mulesocket is a static library, so a PRIVATE dependency doesn't propagate
to consumers at link time. The main executables pulled iphlpapi in
transitively via other libraries, but the minimal NetworkFunctionsTest
unit-test exe did not, failing with 'undefined symbol: GetAdaptersAddresses'.
Make the link PUBLIC so every consumer of mulesocket gets it.
A leak-prevention setting must fail loud, not silent. Resolve the interface
once at startup (new exported ResolveBindInterfaceIndex) and, when it does not
resolve, print a critical-level WARNING telling the user traffic is NOT bound
and may leave via the default route. The per-socket path keeps its detail on
the debug channel to avoid spamming the normal log on every connect.
The test compiles src/LibSocket.cpp directly rather than linking the
mulesocket library, so mulesocket's PUBLIC iphlpapi doesn't reach it. Link
iphlpapi on the test target too for GetAdaptersAddresses().
@got3nks got3nks added this to the 3.1.0 milestone Jul 3, 2026
got3nks added 4 commits July 3, 2026 14:52
…ent)

Testing revealed IP_UNICAST_IF is only a routing *preference* on Linux, not
a constraint: when the bound interface can't reach the destination the kernel
silently falls back to the default route, so it did not prevent leaks - the
exact amule-project#173 case. Verified with a direct probe (binding to a no-internet
interface still connected via the default route, unchanged source IP).

Switch Linux to SO_BINDTODEVICE, which is a real egress bind (verified: a
bind to a non-routable interface now fails to connect). macOS (IP_BOUND_IF)
and Windows (IP_UNICAST_IF) are genuine constraints and are unchanged.

SO_BINDTODEVICE may need CAP_NET_RAW on some kernels. Resolution and the
platform setsockopt are centralized in ApplyBindToInterface; the core probes
it once at startup via TestSocketBindInterface and, if the bind is denied,
warns loudly that traffic is NOT contained - never a silent non-enforcement.
…Country, server.met)

The socket-layer bind only covered ed2k/Kad. aMule's HTTP side-channels
(version check to GitHub, IP2Country DB, server.met-from-URL) go through
wxWebRequest, not LibSocketAsio, so they leaked past a bound interface -
confirmed on macOS: with the socket bind active, the version-check request
still reached GitHub via the default route.

Route all HTTP through one helper (CreateAmuleWebRequest): a curl-backed
wxWebSession (uniform across platforms - Linux/macOS already default to curl,
Windows forced explicitly), the proxy pref applied, and, when an interface is
set, CURLOPT_SOCKOPTFUNCTION binding the socket via the SAME per-platform
logic as the P2P sockets (BindRawSocketToInterface). Falls back to the default
session if the curl backend is unavailable at runtime.

Verified on macOS: binding to a no-route interface now kills the GitHub
version-check connection too, not just ed2k/Kad.
Forcing the libcurl wxWebRequest backend to get a bindable socket works on
Linux and macOS, but on Windows the wxMSW libcurl backend never drives the
transfer - a forced curl request hangs and times out ("Resolving timed out").
Confirmed with a standalone wxWebRequest program, so it's a wxMSW+MSYS2 curl
backend bug, not amuled-specific.

Gate the curl selection + the CURLOPT_SOCKOPTFUNCTION bind on !__WINDOWS__.
Windows keeps its working default (WinHTTP), which has no interface-bind API,
so HTTP side-channels there stay unbound; Linux and macOS bind HTTP as before.
P2P traffic remains bound on all three platforms.
On Windows only the ed2k/Kad sockets are bound; HTTP runs on WinHTTP which
can't be interface-bound. Log 'Binding aMule's peer-to-peer traffic to
interface: X (HTTP updates use the default route)' there instead of the
all-traffic message, so a Windows user isn't misled about the HTTP
side-channels. Linux/macOS keep the all-traffic message (HTTP is bound there).
@got3nks

got3nks commented Jul 3, 2026

Copy link
Copy Markdown
Author

Update: substantial changes since this PR was opened, all driven by real end-to-end enforcement testing (headless amuled on Linux/macOS/Windows, not just "does it build").

1. Linux now uses SO_BINDTODEVICE, not IP_UNICAST_IF. Testing revealed IP_UNICAST_IF is only a routing preference on Linux, not a constraint: when the bound interface can't reach the destination the kernel silently falls back to the default route — so it did not prevent leaks, the whole point of #173. A direct probe confirmed it (binding to a no-internet interface still connected via the default route, source IP unchanged). SO_BINDTODEVICE is a real egress bind (verified: the same probe now fails to connect). macOS (IP_BOUND_IF) and Windows (IP_UNICAST_IF) are genuine constraints and are unchanged. SO_BINDTODEVICE can need CAP_NET_RAW on some kernels; if the bind is denied, the daemon warns loudly that traffic is not contained rather than failing silently.

2. aMule's HTTP side-channels are now bound too (Linux/macOS). The socket-layer bind only covered ed2k/Kad. The version check, IP2Country DB, and server.met-from-URL go through wxWebRequest, not the socket layer, so they leaked past a bound interface (verified: with the socket bind active, the version-check request still reached GitHub via the default route). They now route through one shared helper that binds egress via libcurl's CURLOPT_SOCKOPTFUNCTION, reusing the exact same per-platform logic as the P2P sockets.

3. Windows HTTP stays on WinHTTP (a documented gap). Forcing the libcurl backend to get a bindable HTTP socket works on Linux/macOS but hangs on Windows — a forced curl wxWebRequest never completes ("Resolving timed out"), reproduced with a standalone wxWebRequest program, so it's a wxMSW+MSYS2 curl-backend bug, not ours. Windows therefore keeps its working default (WinHTTP), which has no interface-bind API, so the Windows HTTP side-channels are not bound. The startup log says so honestly there ("Binding aMule's peer-to-peer traffic to interface: X (HTTP updates use the default route)").

Net coverage, each verified by flipping the bound interface between a working one and a no-route one:

P2P (ed2k/Kad) HTTP (version check / IP2Country / server.met)
Linux ✅ bound ✅ bound
macOS ✅ bound ✅ bound
Windows ✅ bound WinHTTP — works, not bound

Builds clean on all three platforms (monolithic, daemon, remote GUI, amulecmd, amuleweb, unit tests).

Forcing the curl backend for all HTTP changed the stack for every user, not
just those using this feature - notably on macOS, where the default is the
native URLSession backend (its own TLS/proxy handling). Gate the curl switch
on a non-empty NetworkInterface, so non-binding users keep the platform
default and only binding users get curl (which is what makes HTTP bindable).
No behavioural change on Linux (default is already curl) or Windows (always
WinHTTP). Verified on macOS: empty interface uses URLSession and the version
check still succeeds; a bound interface uses curl and binds.
@got3nks
got3nks merged commit 3cf7c52 into amule-org:master Jul 3, 2026
10 checks passed
@got3nks
got3nks deleted the feat/bind-to-interface branch July 3, 2026 14:47
got3nks added a commit that referenced this pull request Jul 3, 2026
…iles via ffprobe (#280)

* feat(mediaprobe): ffprobe subprocess module for local media metadata

Standalone module that fronts an ffprobe subprocess to extract length
/ bitrate / codec from local shared files. Wiring into share-add and
Preferences follows in subsequent commits.

MediaProbe::AutoDetectPath() locates a usable binary in two steps:
first a bare `ffprobe -version` invocation to catch the PATH-installed
case, then a per-platform well-known-paths scan (Homebrew + MacPorts
on macOS; Chocolatey + Scoop + WinGet layouts on Windows; distro-
standard prefixes plus /snap/bin on Linux + OpenBSD). Fallback matters
because GUI-launched processes get a minimal PATH on macOS (launchd
default lacks /opt/homebrew) and unreliable PATH on Windows (system-
level updates from Chocolatey install don't always propagate to
running processes).

MediaProbe::Probe() forks ffprobe with `-of default=nk=0:nw=1` so the
output is a bare `key=value` stream — no JSON parser dependency needed
(the tree has none). Length rounds to whole seconds (FT_MEDIA_LENGTH's
wire format is uint32), bit_rate is converted from bps to kbps
(FT_MEDIA_BITRATE), and the first stream's codec_name wins (video for
video containers, audio for audio-only). Failures emit debug log
lines but never surface user-visible errors — a file the probe can't
read still shares fine, just without media tags.

Boolean return with out-param instead of std::optional so the module
matches the existing tree's C++ style. Callers MUST run Probe() off
the main thread; a subsequent commit extends SharedFileList's batch
path to do so.

Prep for #140 Phase B — advertising media metadata from own shared
files to ed2k servers + Kad.

* feat(sharing): publish FT_MEDIA_{LENGTH,BITRATE,CODEC} to ed2k + Kad

The Kad publisher at kademlia/kademlia/Search.cpp:1422 already
iterates the file's media tags — but gates the whole emit on
`file->GetMetaDataVer() > 0`. GetMetaDataVer had been a longstanding
stub returning a hardcoded 0 with a TODO comment, so the code path
was dead: no ed2k client has ever seen media metadata from an
aMule-shared file. The ed2k server publish path
(CreateOfferedFilePacket) had a matching stub — a comment reading
"There, we could add MetaData info, if we ever get to have that."
that built its outgoing tag list without touching m_taglist for
media entries.

Un-stub both:

- GetMetaDataVer now derives from FT_MEDIA_LENGTH tag presence. No
  new persisted field: MediaProbe is the only source of the tag,
  and the tag itself already rides through known.met via the
  existing m_taglist load/save. Non-zero length is the "we've
  probed and have data worth publishing" signal, which is exactly
  what Kad's gate needs.

- CreateOfferedFilePacket appends FT_MEDIA_LENGTH / FT_MEDIA_BITRATE
  as CTagVarInt (VBT-encoded for capable eMule clients + TYPETAG-
  INTEGER-capable servers, fixed 32-bit otherwise) and FT_MEDIA_CODEC
  as CTagString when the corresponding tag is present and non-empty
  / non-zero. Each tag is optional per file; a file with only length
  publishes only length.

Prep for #140 Phase B — once MediaProbe wiring lands in
SharedFileList's share-add path, tags flow to peers automatically.
No change to known.met format (m_taglist already covers the
persistence layer).

* feat(prefs): [MediaMetadata]/Enabled + FFProbePath + UI panel

Preferences plumbing for #140's ffprobe subprocess. Cfg items live
in a fresh /MediaMetadata/ config namespace (won't clash with any
/eMule/ key) with Enabled default false — an upgraded install won't
kick off background probing until the user opts in from Preferences
-> Files.

UI panel goes at the bottom of PreferencesFilesTab: an Enable
checkbox plus a "Path to ffprobe:" row with text field, Browse and
Detect buttons. Browse routes through the existing
OnButtonBrowseApplication switch (same file-selector infra the
video-player / browser fields use). Detect fires
MediaProbe::AutoDetectPath() and either populates the field or
shows a friendly "install ffmpeg or Browse manually" info dialog.

The whole box hides in amulegui via PrefsUnifiedDlg's
amuledOnlyPrefs[] — probing runs daemon-side, the remote GUI has
no business setting the path.

IDC IDs sit in the 10420-10423 functional band and 10370 orphan-
label slot, chosen to leave a 10-ID cushion above bind-to-
interface's IDC_INTERFACE = 10410 so parallel branches can grow
without collision. MediaProbe moves to COMMON_SOURCES so the
Detect button's handler still links in remote-GUI builds (the
button is hidden there but the event-table binding still needs
the symbol).

Follow-ups on the same branch: SharedFileList wiring (probe on
share-add + known.met load), extension gate, background-probe
throttling.

* feat(shared): probe media metadata off-main + attach to CKnownFile

Closes the #140 wiring loop: shared audio / video files now get
probed with ffprobe, and the resulting FT_MEDIA_LENGTH / _BITRATE /
_CODEC tags are attached to the CKnownFile so the earlier ed2k +
Kad publisher un-stubs pick them up.

Threading + throttling:

- New CMediaProbeTask (ETP_Low) rides on the existing
  CThreadScheduler queue. That queue serialises tasks — a large
  library at first-boot-after-upgrade retrofits one file at a time,
  never stepping on hashing or completion which run at higher
  priority. No custom thread pool.

- Task ctor snapshots the ffprobe path so the worker never touches
  thePrefs, and remembers just the file's CMD4Hash + CPath. Result
  marshals back via CMediaProbeEvent (new MULE_EVT_MEDIA_PROBE),
  main thread resolves the hash to a live CKnownFile via
  CKnownFileList::FindKnownFileByID (the file may have been
  unshared while we were probing) and calls AddTagUnique for each
  populated field, MarkECChanged, and knownfiles->Save so the tags
  survive a crash.

Gating:

- Preference /MediaMetadata/Enabled must be true AND FFProbePath
  non-empty.
- Extension gate: only ED2KFT_AUDIO or ED2KFT_VIDEO files (from
  GetED2KFileTypeID) — skips the mass of docs / archives / images
  a typical share tree carries.
- Already-probed gate: skip when FT_MEDIA_LENGTH > 0. This is the
  retrofit cache — once a file has been probed successfully its
  tag rides through known.met and future launches skip re-probing.

Hook site is CSharedFileList::AddFile — the single choke point for
both runtime share-adds (user picks a new folder / drops a file in
Incoming) AND Reload()'s known.met walk, so an upgraded install
retrofits every existing shared file on the first launch after the
user enables the feature. No separate load-vs-add code path.

Event-table bindings for MULE_EVT_MEDIA_PROBE landed in both
amuled.cpp (daemon) and amule-gui.cpp (monolithic / remote GUI) so
the handler fires regardless of which app the scheduler dispatched
from.

* chore(clang-format): apply clang-format-18 to #140 files

* i18n(pot): regenerate catalogs for #140 UI additions

Rerun of scripts/update-po.sh on top of the post-#281 (bind-to-
interface) master so the Media metadata strings coexist with the
new bind-to-interface strings. Replaces the earlier standalone
regen that was skipped during the rebase.

* feat(prefs): gray out ffprobe path controls when Media metadata is off

The [MediaMetadata] Enabled checkbox now drives the enabled state of
IDC_MEDIAMETA_FFPROBEPATHTEXT / _FFPROBEPATH / _FFPROBEBROWSE /
_FFPROBEDETECT via the existing OnCheckBoxChange handler, and the
initial state is applied at dialog open. Previously the path input,
Browse and Detect buttons stayed live-looking with the feature
disabled, which read as "I can configure ffprobe here" while
nothing downstream would ever run.
Cflsft pushed a commit to Cflsft/amule that referenced this pull request Jul 6, 2026
Closes amule-org#173.

Bind all of aMule's egress to a chosen network interface, unprivileged, so
traffic can't leak via the default route (e.g. a VPN tunnel). Uses the real
per-platform egress constraints - SO_BINDTODEVICE (Linux), IP_BOUND_IF (macOS)
and IP_UNICAST_IF (Windows) - with no elevated privileges, except where a kernel
requires CAP_NET_RAW for SO_BINDTODEVICE, in which case the failed bind is warned
about loudly instead of failing silently. Covers the TCP listen socket, outbound
TCP (servers + peers) and the UDP sockets shared by the ed2k client, ed2k server
and Kad.

aMule's HTTP side-channels (version check, IP2Country, server.met) are bound on
Linux and macOS via the libcurl backend; on Windows they stay on WinHTTP (its
wxWidgets curl backend hangs, and WinHTTP has no interface-bind API), which the
startup log states honestly. Peer-to-peer traffic is bound on all three platforms.

Adds an editable "Bind to network interface" field on the Connection preferences
page, populated with the machine's interfaces and stored as /eMule/NetworkInterface.
Like the local-address bind it is daemon-side and hidden in the remote GUI;
changing it is flagged restart-needed.
Cflsft pushed a commit to Cflsft/amule that referenced this pull request Jul 6, 2026
…iles via ffprobe (amule-org#280)

* feat(mediaprobe): ffprobe subprocess module for local media metadata

Standalone module that fronts an ffprobe subprocess to extract length
/ bitrate / codec from local shared files. Wiring into share-add and
Preferences follows in subsequent commits.

MediaProbe::AutoDetectPath() locates a usable binary in two steps:
first a bare `ffprobe -version` invocation to catch the PATH-installed
case, then a per-platform well-known-paths scan (Homebrew + MacPorts
on macOS; Chocolatey + Scoop + WinGet layouts on Windows; distro-
standard prefixes plus /snap/bin on Linux + OpenBSD). Fallback matters
because GUI-launched processes get a minimal PATH on macOS (launchd
default lacks /opt/homebrew) and unreliable PATH on Windows (system-
level updates from Chocolatey install don't always propagate to
running processes).

MediaProbe::Probe() forks ffprobe with `-of default=nk=0:nw=1` so the
output is a bare `key=value` stream — no JSON parser dependency needed
(the tree has none). Length rounds to whole seconds (FT_MEDIA_LENGTH's
wire format is uint32), bit_rate is converted from bps to kbps
(FT_MEDIA_BITRATE), and the first stream's codec_name wins (video for
video containers, audio for audio-only). Failures emit debug log
lines but never surface user-visible errors — a file the probe can't
read still shares fine, just without media tags.

Boolean return with out-param instead of std::optional so the module
matches the existing tree's C++ style. Callers MUST run Probe() off
the main thread; a subsequent commit extends SharedFileList's batch
path to do so.

Prep for amule-org#140 Phase B — advertising media metadata from own shared
files to ed2k servers + Kad.

* feat(sharing): publish FT_MEDIA_{LENGTH,BITRATE,CODEC} to ed2k + Kad

The Kad publisher at kademlia/kademlia/Search.cpp:1422 already
iterates the file's media tags — but gates the whole emit on
`file->GetMetaDataVer() > 0`. GetMetaDataVer had been a longstanding
stub returning a hardcoded 0 with a TODO comment, so the code path
was dead: no ed2k client has ever seen media metadata from an
aMule-shared file. The ed2k server publish path
(CreateOfferedFilePacket) had a matching stub — a comment reading
"There, we could add MetaData info, if we ever get to have that."
that built its outgoing tag list without touching m_taglist for
media entries.

Un-stub both:

- GetMetaDataVer now derives from FT_MEDIA_LENGTH tag presence. No
  new persisted field: MediaProbe is the only source of the tag,
  and the tag itself already rides through known.met via the
  existing m_taglist load/save. Non-zero length is the "we've
  probed and have data worth publishing" signal, which is exactly
  what Kad's gate needs.

- CreateOfferedFilePacket appends FT_MEDIA_LENGTH / FT_MEDIA_BITRATE
  as CTagVarInt (VBT-encoded for capable eMule clients + TYPETAG-
  INTEGER-capable servers, fixed 32-bit otherwise) and FT_MEDIA_CODEC
  as CTagString when the corresponding tag is present and non-empty
  / non-zero. Each tag is optional per file; a file with only length
  publishes only length.

Prep for amule-org#140 Phase B — once MediaProbe wiring lands in
SharedFileList's share-add path, tags flow to peers automatically.
No change to known.met format (m_taglist already covers the
persistence layer).

* feat(prefs): [MediaMetadata]/Enabled + FFProbePath + UI panel

Preferences plumbing for amule-org#140's ffprobe subprocess. Cfg items live
in a fresh /MediaMetadata/ config namespace (won't clash with any
/eMule/ key) with Enabled default false — an upgraded install won't
kick off background probing until the user opts in from Preferences
-> Files.

UI panel goes at the bottom of PreferencesFilesTab: an Enable
checkbox plus a "Path to ffprobe:" row with text field, Browse and
Detect buttons. Browse routes through the existing
OnButtonBrowseApplication switch (same file-selector infra the
video-player / browser fields use). Detect fires
MediaProbe::AutoDetectPath() and either populates the field or
shows a friendly "install ffmpeg or Browse manually" info dialog.

The whole box hides in amulegui via PrefsUnifiedDlg's
amuledOnlyPrefs[] — probing runs daemon-side, the remote GUI has
no business setting the path.

IDC IDs sit in the 10420-10423 functional band and 10370 orphan-
label slot, chosen to leave a 10-ID cushion above bind-to-
interface's IDC_INTERFACE = 10410 so parallel branches can grow
without collision. MediaProbe moves to COMMON_SOURCES so the
Detect button's handler still links in remote-GUI builds (the
button is hidden there but the event-table binding still needs
the symbol).

Follow-ups on the same branch: SharedFileList wiring (probe on
share-add + known.met load), extension gate, background-probe
throttling.

* feat(shared): probe media metadata off-main + attach to CKnownFile

Closes the amule-org#140 wiring loop: shared audio / video files now get
probed with ffprobe, and the resulting FT_MEDIA_LENGTH / _BITRATE /
_CODEC tags are attached to the CKnownFile so the earlier ed2k +
Kad publisher un-stubs pick them up.

Threading + throttling:

- New CMediaProbeTask (ETP_Low) rides on the existing
  CThreadScheduler queue. That queue serialises tasks — a large
  library at first-boot-after-upgrade retrofits one file at a time,
  never stepping on hashing or completion which run at higher
  priority. No custom thread pool.

- Task ctor snapshots the ffprobe path so the worker never touches
  thePrefs, and remembers just the file's CMD4Hash + CPath. Result
  marshals back via CMediaProbeEvent (new MULE_EVT_MEDIA_PROBE),
  main thread resolves the hash to a live CKnownFile via
  CKnownFileList::FindKnownFileByID (the file may have been
  unshared while we were probing) and calls AddTagUnique for each
  populated field, MarkECChanged, and knownfiles->Save so the tags
  survive a crash.

Gating:

- Preference /MediaMetadata/Enabled must be true AND FFProbePath
  non-empty.
- Extension gate: only ED2KFT_AUDIO or ED2KFT_VIDEO files (from
  GetED2KFileTypeID) — skips the mass of docs / archives / images
  a typical share tree carries.
- Already-probed gate: skip when FT_MEDIA_LENGTH > 0. This is the
  retrofit cache — once a file has been probed successfully its
  tag rides through known.met and future launches skip re-probing.

Hook site is CSharedFileList::AddFile — the single choke point for
both runtime share-adds (user picks a new folder / drops a file in
Incoming) AND Reload()'s known.met walk, so an upgraded install
retrofits every existing shared file on the first launch after the
user enables the feature. No separate load-vs-add code path.

Event-table bindings for MULE_EVT_MEDIA_PROBE landed in both
amuled.cpp (daemon) and amule-gui.cpp (monolithic / remote GUI) so
the handler fires regardless of which app the scheduler dispatched
from.

* chore(clang-format): apply clang-format-18 to amule-org#140 files

* i18n(pot): regenerate catalogs for amule-org#140 UI additions

Rerun of scripts/update-po.sh on top of the post-amule-org#281 (bind-to-
interface) master so the Media metadata strings coexist with the
new bind-to-interface strings. Replaces the earlier standalone
regen that was skipped during the rebase.

* feat(prefs): gray out ffprobe path controls when Media metadata is off

The [MediaMetadata] Enabled checkbox now drives the enabled state of
IDC_MEDIAMETA_FFPROBEPATHTEXT / _FFPROBEPATH / _FFPROBEBROWSE /
_FFPROBEDETECT via the existing OnCheckBoxChange handler, and the
initial state is applied at dialog open. Previously the path input,
Browse and Detect buttons stayed live-looking with the feature
disabled, which read as "I can configure ffprobe here" while
nothing downstream would ever run.
got3nks added a commit that referenced this pull request Jul 16, 2026
The interface bind added in #281 (IP_UNICAST_IF / IP_BOUND_IF) pins every
socket aMule opens — including the External Connection listener — to a single
interface, so ed2k/Kad cannot run over a VPN tunnel while the EC control port
stays on the LAN.

Decouple the EC listener with a new daemon-side setting,
/ExternalConnect/ECNetworkInterface (empty = any), that binds only aMule's EC
acceptor, independent of the global P2P interface pin. It sits beside the
existing ECAddress IP bind, giving the EC channel both its own IP and its own
interface.

CLibSocketServer gains a per-server interface override; only CExternalConnListener
uses it. ed2k/Kad TCP and UDP, outbound connections and HTTP keep following the
global setting untouched. A "Bind to network interface" selector is added to the
Remote Controls page (reusing the existing P2P label); like the other EC-listener
settings it is daemon-only — hidden in the remote GUI and not carried over EC —
and flagged restart-needed.
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.

FR, Security: Bind to specific network interface

1 participant