Skip to content

Dev - #244

Merged
Anof-cyber merged 22 commits into
masterfrom
dev
Oct 11, 2026
Merged

Dev#244
Anof-cyber merged 22 commits into
masterfrom
dev

Conversation

@Anof-cyber

Copy link
Copy Markdown
Member

Pull Request

⚠️ IMPORTANT - READ BEFORE SUBMITTING

Required Pre-Submission Checklist

  • I have read the CONTRIBUTING.md file
  • I have created a GitHub issue for this change and linked it below
  • I have forked the dev branch (NOT master)
  • I understand that the master branch is ONLY updated when a new release is created
  • I understand that pull requests to master will be REJECTED

⛔ Critical Information

DO NOT submit a pull request directly to the master branch. The master branch is only updated when a new release is created. No direct commits or merges are accepted to master - neither from the InterceptSuite team nor from contributors.

All changes MUST be submitted to the dev branch, which contains the latest code and commits.

You MUST create a GitHub issue first before submitting a pull request. This allows the InterceptSuite team to:

  • Verify if the bug is already fixed in the development branch
  • Confirm if a new feature is accepted and approved
  • Avoid duplicate work

Pull requests without an associated issue will be rejected.


Pull Request Details

Type of Change

  • Bug Fix
  • New Feature
  • Code Improvement/Refactoring
  • Documentation Update
  • Other (please describe):

Related GitHub Issue

Issue Link:

Platform(s) Affected

  • Windows
  • macOS
  • Linux
  • Cross-platform/All

macOS Application Changes

Does this pull request include changes to the macOS application?

  • Yes
  • No

If YES, you MUST answer the following:

macOS Code Signing & Verification

  • I have signed the macOS app with a valid Apple Developer account
  • I have run the signed application on macOS to verify my changes work as expected
  • I confirm the application launches and functions correctly with my changes

⚠️ Important: If you cannot sign the macOS app to verify your code changes work, please DO NOT submit this pull request. Instead, use the GitHub issue you created to let the InterceptSuite team fix the bug or add the feature. You will need to wait for the next release.


Description of Changes

What does this pull request do?

How has this been tested?

  • Test environment (OS, version, etc.):
  • Test steps:
  • Test results:

Compilation & Dependencies

  • I have reviewed the CONTRIBUTING.md file for compilation instructions
  • I have verified all required libraries and packages are documented
  • My changes compile without errors
  • My changes do not introduce new dependencies (or I have documented them below)

New dependencies (if any):


Screenshots/Logs (if applicable)


Additional Context


Final Checklist

  • My code follows the project's coding style
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated documentation as needed
  • My changes do not generate new warnings or errors
  • I have tested my changes on the target platform(s)
  • I have verified this change is based on the latest dev branch
  • I understand that pull requests without an associated GitHub issue will be rejected

Anof-cyber and others added 22 commits October 10, 2026 10:07
ProxyBridge.exe no longer requires administrator. A LocalSystem service
(ProxyBridgeSvc.exe, registered once by the installer) owns
ProxyBridgeCore.dll and WinDivert; the GUI drives it over two named pipes
(request/response + event stream), the same model Proxifier uses.

- add src/service: service host and wire protocol
- add gui/api/pb_ipc.h: pipe-backed PBApi; PB_Load -> PB_LoadDirect
- manifest: requireAdministrator -> asInvoker
- no service (portable copy): relaunch elevated and load the core directly
- Run at Startup: per-user HKCU Run entry instead of elevated logon task
- installer: create/start/remove the service, drop legacy logon task
- compile.ps1: build ProxyBridgeSvc.exe
- ignore MSVC .exp/.lib/.obj intermediates

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…session failures

Per-source-port decision cache and connection tracking were only cleared on an
outbound FIN/RST. Connections that end without one (peer reset, connect timeout,
app killed) left stale state, and when Windows reused the ephemeral port a new
connection was either sent DIRECT (stale bit) or redirected to the previous
connection's destination (stale tracking entry). A fresh SYN now clears both,
for IPv4 and IPv6.

The capture loop retried immediately and logged on every non-INVALID_HANDLE
receive error. It now exits quietly on shutdown errors, rate-limits logging,
backs off on persistent failure and reports when capture is effectively dead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… DIRECT bypass, #205)

Two lookup defects made fresh connections look unowned, so they were sent DIRECT
without being logged or rule-matched:

- The TCP table lists closed connections in TIME_WAIT with PID 0. When Windows reuses
  that ephemeral port both rows exist and the lookup could take the TIME_WAIT row.
  Rows with no owner are now skipped (IPv4 and IPv6).
- The size-query-then-fetch pattern failed with ERROR_INSUFFICIENT_BUFFER whenever the
  table grew in between (busy machine). Fetch now adds headroom and retries (TCP/UDP,
  IPv4/IPv6).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ion into DIRECT (#205)

- A new outbound TCP connection can be absent from the owner-PID table when its SYN
  arrives. Retry the lookup briefly (3 x ~0.5 ms) before treating it as ownerless.
  The capture layer only sees traffic to/from this host, so a persistent miss is rare.
- When the process cannot be opened (access denied / protected) fall back to the
  basename from a toolhelp snapshot so name-based rules still match.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ST app on handshake failure

"SOCKS5 IPv6: CONNECT failed (reply=-1)" gave no destination and a meaningless code;
-1 actually means the proxy closed the connection or never answered. Failures now
name the destination and the reason (RFC 1928 REP names for real replies), and
identical lines within 5 s are collapsed to a "repeated N more times" note.

The relay completes the app's TCP handshake before talking to the proxy, so on a
failed upstream handshake it now resets (RST) the app socket instead of a silent
close, so the app fails or retries immediately.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ssion #190)

The GUI is system-DPI aware but used fixed 16 px fonts and a fixed 1200x820 window, so on
high-DPI screens (e.g. 3072x1920 14") the log text and window were tiny and nothing scaled.

- fonts and main-window layout metrics scale with the system DPI
- default window size scales with DPI, capped at 90% of the work area
- Settings > Increase/Decrease/Reset Text Size, plus Ctrl+mouse wheel and Ctrl +/-/0 on
  the log boxes and search fields; 60-300%, persisted per profile as "FontZoom"
- strings added for English and Chinese

Dialogs keep their fixed-pixel layout.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e-port collision with relay upstream)

The relay's upstream sockets to the proxy draw ephemeral ports from the same pool as the
apps, and the packet loop checked per-port state (DIRECT bit / tracked entry) before
looking at who the packet was for. After the port range had cycled for hours, a leftover
entry for an earlier app flow could match the relay's own connection to the proxy and
redirect it back into the relay, so proxied apps hung while the log still looked normal.

Outbound TCP whose destination is a configured proxy server (IPv4) is now passed through
untouched before any per-port state is consulted.
…ost (Discord voice, slowdown)

UDP "connections" are tracked by source port only, with a single destination, and once
tracked a flow was never re-checked:

- A socket that moves to another server (e.g. Discord migrating a voice session) kept
  sending to the first destination forever, so the call dropped and never recovered.
- A UDP port reused by a different socket inherited the old flow's routing for as long as
  traffic kept the entry alive.
- The relay's own UDP socket to the proxy's UDP relay draws its port from the same pool as
  the apps; a collision with a tracked app port redirected the relay's packets back into
  the relay and broke all proxied UDP until restart.

Fixes: udp_flow_validate() retargets the entry when the destination changes and evicts it
after 20 s idle so the rules are re-evaluated; relay <-> proxy UDP relay traffic is passed
through untouched.

DIRECT UDP flows had no decision cache, so every datagram re-ran the process lookup
(OpenProcess per packet) on the single capture thread, which saturates it at high packet
rates and slows all traffic. DIRECT decisions are now cached for 5 s per
(src port, destination).

Also: the PID cache stored a 64-bit tick count in a 32-bit field, which breaks its TTL
math after ~49.7 days of uptime; widened to 64 bits.
With --verbose the log/connection callbacks printf() directly. The engine calls them on its
single packet-capture thread, so a console that stops draining (QuickEdit selection, or
stdout redirected to a pipe/file that nobody reads, as on a headless host) blocks that
thread and with it all captured traffic. The callbacks now only enqueue into a bounded
ring (drops when full, with a "dropped" note); a writer thread does the output.

Also include <shellapi.h> for ShellExecuteA (its return value was being truncated on x64).
… of scanning the TCP table

Every new outbound TCP connection triggered GetExtendedTcpTable on the single capture thread
(twice: a sizing call and the fetch). The table includes every TIME_WAIT socket, so on a busy
machine it has tens of thousands of rows and one fetch costs several ms (measured 3.6 ms at
~9.6k rows). A proxied connection evaluates three SYNs (app, relay->proxy, proxy->target), so
the capture thread was the bottleneck: ~49 connections/s at 101% of one core, median latency
1.2-1.6 s and growing with the table.

- pb_sockmap.c: a sniff-only WinDivert SOCKET-layer handle records the PID of every TCP
  connect() by local port before the SYN is sent; lookups hit it first (entries valid 8 s)
  and fall back to the table scan on a miss, so behaviour is never worse than before.
- owner_table_fetch(): try the previous scratch buffer first and only size the table when it
  is too small (halves the cost of the remaining scans).

Measured on the same machine with a SOCKS5 test proxy: 1500 proxied connections went from
30.5 s (p50 1240 ms) to 1.5 s (p50 60 ms); a 14k-connection churn holds a flat ~16 ms median
with 100% of connections proxied.
…nection count (#213)

The data-transfer stage ran two blocking threads (upload + download) and two 128 KB buffers
per proxied connection, so a BitTorrent-style workload with ~1000 peers meant 2000+ threads,
~350 KB and ~9 handles per connection (issue #213: machine sluggish, 1000+ threads).

Both sockets are now handed to a small pool of worker threads (2-8, by core count) driving
overlapped WSARecv/WSASend through an I/O completion port. Semantics are unchanged: each
direction copies recv -> send, and when either direction ends both sockets are shut down and
closed once both have drained. Buffers and socket buffers shrink as the number of live
relays grows (64 KB -> 16 KB -> 8 KB; 4 MB -> 1 MB -> 256 KB -> 64 KB socket buffers).
ProxyBridge_Stop() aborts every live relay and joins the workers before returning, so the
engine can be unloaded safely. If the pool cannot start, connections fall back to the
previous blocking relay threads.

Measured on the same machine (800 held connections with light traffic):
  threads   1608 -> 22      private memory 285 MB -> 42 MB      handles 7530 -> 2002
3000 held connections: 18 threads, 61 MB. Bulk throughput 364/428 MB/s -> 413/500 MB/s
(up/down); 384 MB of random data echoed through it verified byte-for-byte; a 1500-connection
churn stayed 100% proxied.
…tions

Shrinking per-connection buffers as the number of relays grew (64 KB -> 16 KB -> 8 KB) made fast
transfers slow whenever many other connections were open: with 800 connections held, a single
stream dropped to 164/294 MB/s up/down versus 344/492 MB/s with full-size buffers.

An idle direction now waits with a zero-byte receive and owns no buffer. When data arrives it
takes a 64 KB buffer from a shared pool, reads and sends, then goes back to waiting unless the
stream is still busy (>= 16 KB per read), in which case it keeps the buffer. Socket buffers
only shrink (4 MB -> 1 MB) above 1024 live relays.

Measured through the proxy, 4 streams up/down MB/s while N other connections are held open:
  N=0     old relay 359/477   this 433/532
  N=800   old relay 303/347   this 365/399
  N=3000  old relay 105/94    this 179/189
Private memory with 3000 held connections: 12 MB (old relay ~1 GB projected, small-buffer pool
61 MB, always-large buffers 174 MB). Data integrity verified byte-for-byte at every load.
…tion

A TCP FIN/RST used to delete the connection's tracking entry immediately. A client that sends
data and closes at once can have its FIN seen before the relay has accept()ed the connection;
the relay's lookup then fails and the connection - together with the client's data - is dropped
silently. Under load this was easy to hit: with 3000 connections created over ~47 s and a 25 s
hold, ~800 of the late connect-send-close connections never reached the proxy.

The entry is now kept until both sides have sent their FIN (new fin_flags: 1 = app, 2 = relay),
and a reset still removes it right away. Port reuse is already handled by clearing stale state
on a fresh SYN. Measured: the same 3000-connection burst now proxies 3000/3000 (twice), a
16000-connection churn with 25% abnormal closes stays 100% proxied, data integrity unchanged.

Also wrap WinDivertSend: every call in the packet loop ignored its result, so a failed
re-injection dropped the packet silently. Failures are now counted and logged (first ten, then
every 500th) with protocol, direction and flags.
Matches the macOS 4.1.0 release. Updates the GUI version resource (also fixes the numeric file
version, which was still 4.0.11), the application manifest, the installer name and version info,
the core and CLI version strings, and the installer filename in CONTRIBUTING.md.
…rks (firewall rule for the relay)

ProxyBridge redirects a connection by turning the app's packet around so it arrives at the local
relay as an *inbound* connection on whichever interface the app used. Windows Firewall only
prompts to allow an app on the Private profile, and VPN adapters (WireGuard, OpenVPN) are
classified Public. With a full-tunnel VPN the redirected SYN was therefore dropped: the
Connections log looked normal, the app timed out in SYN-SENT, and the proxy never saw anything.
Reproduced with a WireGuard (UniFi Endpoint) tunnel: 0 of N connections proxied; after adding an
inbound allow rule, N of N.

The same gap affects the service: the relay now lives in ProxyBridgeSvc.exe (a background service
never gets the first-run firewall prompt), so a fresh install had no inbound rule for it at all.

- On start, when elevated, the engine adds an inbound allow rule for its host executable on all
  network profiles (one rule per path, "ProxyBridge relay <hash>"); logged in the Activity tab,
  best effort.
- The uninstaller removes the rules for ProxyBridgeSvc.exe, ProxyBridge.exe and ProxyBridge_CLI.exe.
- Because the relay is now reachable on every profile, accept() verifies the peer: the redirected
  SYN always arrives from the tracked flow's ORIGINAL destination address (or a loopback address),
  so a connection from any other address is dropped. Previously a flow was claimed by source port
  alone, which another host could have exploited once the relay port was reachable.

Verified over a WireGuard full tunnel with only the auto-created rule: 6/6 requests proxied, 800/800
burst connections, byte-verified bidirectional integrity, UDP server-switch and port reuse correct.
…/re-add (#203)

The GUI saved the id returned by the engine's AddProxyConfig as the proxy's permanent id in the
profile. That id is only valid for the current engine session (it restarts at 1 whenever the engine
is reloaded, which the service does for every GUI connection), so ids drifted and collided: after
deleting a proxy and adding another, a newly added proxy could get an id that an older proxy or an
existing rule already used, and rules resolved to the wrong proxy.

Proxy ids are now owned by the profile: PB_AllocConfigId() hands out stable ids from a counter that
is persisted ("NextProxyConfigId") and never reused, and the existing stored-to-engine id mapping
(ResolveNativeCfg) keeps translating them to the engine's session ids. On load, profiles written by
the old code are repaired: zero or duplicate ids get fresh ones (the first holder keeps its id, so
rules stay with it) and the counter is positioned above every id in use, including ids that only
rules still reference.

Tested with the real profile.c: the issue's delete/re-add scenario, save/reload persistence, and a
broken profile with duplicate/zero ids.
…reads outliving the engine)

ProxyBridgeSvc reloads the engine DLL for every GUI connection, so ProxyBridge_Stop() must leave no
thread running inside the DLL: a thread that wakes up after the DLL is unloaded executes unmapped
code and takes the whole service down. Windows logged "service terminated unexpectedly" (event 7034)
four times today, each right after the GUI was closed; the GUI hid it by restarting the service on
its next launch. In-process (GUI/CLI) the process exiting masked the same bug.

The culprit was the cleanup thread: it did Sleep(30000) while Stop() waited only one second for it,
then abandoned it. Fixes:
- the cleanup thread sleeps in 100 ms slices and checks the stop flag
- thread joins in Stop() wait up to 8 s instead of 1 s
- the firewall-rule thread is tracked and joined
- per-connection handshake threads are counted from creation to exit and Stop() waits for them
  (up to 35 s) before the engine can be unloaded
- remove the unused ACCEPT_AND_DISPATCH macro, which would have bypassed that accounting

Verified on an installed service: closing the GUI no longer kills it (same process id after 120 s,
no new 7034 events).
…ecision (#206)

Clearing the decision before testing whether it was direct made the packet fall through to a fresh rule lookup that cached DIRECT again, so teardown never evicted anything. Remember the direct state first, clear on FIN/RST, and send untracked FIN/RST untouched instead of looking up an owner that is usually already gone.
@Anof-cyber
Anof-cyber merged commit c9646aa into master Oct 11, 2026
9 of 11 checks passed
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.

1 participant