Skip to content

tcp proxy#14633

Merged
rustdesk merged 13 commits intomasterfrom
tcp-proxy
Apr 3, 2026
Merged

tcp proxy#14633
rustdesk merged 13 commits intomasterfrom
tcp-proxy

Conversation

@rustdesk
Copy link
Copy Markdown
Owner

@rustdesk rustdesk commented Mar 27, 2026

Summary

This PR adds TCP proxy support for API requests in src/common.rs.

  • Add a raw TCP proxy path for API calls that target the configured non-public API server
  • Reuse the rendezvous server as the TCP proxy endpoint
  • Keep normal HTTP as the default path, but fall back to the TCP proxy on connection errors or 5xx responses
  • Preserve 4xx responses as-is so application-level errors are still returned normally
  • Cover both post_request and http_request_sync
  • Add unit tests for API host matching and HttpProxyResponse JSON serialization

Why

In some self-hosted/private deployments, the API server is not directly reachable from the client over normal HTTP, while the rendezvous server is reachable. This change allows those API requests to go
through the existing secure TCP channel instead of failing.

Notes

  • The TCP proxy path only applies when the target matches the configured API host
  • Public API targets are excluded
  • Direct TCP mode is gated by the use-raw-tcp-for-api option and disabled when WebSocket mode is in use

Summary by CodeRabbit

  • New Features

    • TCP-proxy routing for API requests with direct proxying when configured.
  • Improvements

    • HTTP requests try HTTP first, automatically falling back to TCP-proxy on connection errors or server (5xx) errors; 4xx responses are preserved.
    • Proxy responses use a consistent JSON format and header handling is more robust.
    • Reduced noisy "connection secured" logging for proxied TCP connections.
  • Tests

    • Expanded unit tests for proxy routing, address handling, header validation, and response conversion.

@rustdesk rustdesk requested review from 21pages and Copilot March 27, 2026 05:33
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added TCP-proxy routing and HTTP-first-with-TCP-fallback control flow in src/common.rs: request paths now choose between direct TCP-proxy, HTTP then optional TCP fallback, or HTTP-only behavior; introduced TCP rendezvous transport, proxy response→JSON conversion, refined header parsing, and silent TCP encryption variant. Tests added.

Changes

Cohort / File(s) Summary
Core logic
src/common.rs
Added TCP-proxy predicates (should_use_raw_tcp_for_api, should_use_tcp_proxy_for_api_url, can_fallback_to_raw_tcp, is_tcp_proxy_api_target), split POST/HTTP flows (post_request_http, post_request_via_tcp_proxy, post_request updated), added tcp_proxy_request, post_request_via_tcp_proxy, and http_proxy_response_to_json.
HTTP sync/path variants
src/common.rs
Refactored sync HTTP path: http_request_http returns (status, json), http_request_via_tcp_proxy added, http_request_sync now routes HTTP-first-with-optional-TCP-fallback or direct-TCP when configured.
Header parsing
src/common.rs
Introduced parse_json_header_entries (replaces/extends earlier parsing), updated get_http_response_async and header handling to consume parsed JSON header entries and invariants (Content-Type handling).
Encryption / logging
src/common.rs
Reworked secure TCP: internal secure_tcp_impl(conn, key, log_on_success) and public wrappers, added secure_tcp_silent used for TCP-proxy to suppress "Connection secured" log.
Tests
tests/*
Added unit tests: TCP-proxy API host matching, rendezvous TCP address normalization (bare IPv6), JSON header parsing failure cases and invariants (including Content-Type), fallback log-condition constant, URL redaction for proxy, and HttpProxyResponse→JSON conversion.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Handler as Request Handler
    participant HTTP as HTTP Layer
    participant TCP as TCP Proxy / Rendezvous
    participant Final as Response Finalizer

    Client->>Handler: post_request(url, body, headers)
    alt Direct TCP-proxy target (flag + host match & WS disabled)
        Handler->>TCP: post_request_via_tcp_proxy(url, body, headers)
        TCP-->>Handler: HttpProxyResponse -> JSON
        Handler->>Final: return proxy JSON result
    else Not direct TCP-proxy target
        Handler->>HTTP: post_request_http(url, body, headers)
        HTTP-->>Handler: (status_code, body_text)
        alt 2xx/3xx
            Handler->>Final: return HTTP success
        else 4xx
            Handler->>Final: return HTTP body (no TCP fallback)
        else 5xx or HTTP connection error (and fallback allowed)
            Handler->>TCP: post_request_via_tcp_proxy(...)
            TCP-->>Handler: proxy response (or error)
            alt proxy success
                Handler->>Final: return proxy JSON result
            else proxy fails
                Handler->>Final: return original HTTP result
            end
        end
    end
    Final-->>Client: Final response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • fufesou

Poem

🐇 I tunneled bytes where headers meet,
I braided JSON for a proxy seat.
When HTTP stumbles with a five-oh cry,
I whisper TCP and leap right by.
Small paws, swift hops — the network sighs.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "tcp proxy" is vague and lacks specificity about the actual change; it does not convey meaningful context about adding TCP proxy support for API requests. Revise the title to be more descriptive, such as 'Add TCP proxy support for API requests' or 'Implement TCP proxy fallback for non-public API calls'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tcp-proxy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a raw TCP proxy path for API-bound HTTP requests by tunneling them through the rendezvous server, with opt-in “direct TCP” mode and automatic fallback from HTTP on failures/5xx responses.

Changes:

  • Introduces TCP-proxy request helpers and API-target matching logic to decide when to proxy.
  • Updates post_request and http_request_sync to support direct TCP proxy and HTTP→TCP fallback behavior.
  • Adds unit tests for API host matching and HttpProxyResponse → JSON serialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51b562c6c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/common.rs`:
- Around line 1313-1325: The current logic in post_request and http_request_sync
uses Err(_) or any 5xx to trigger a TCP proxy fallback (via
can_fallback_to_raw_tcp and post_request_via_tcp_proxy), which can replay
non-idempotent mutations; update both post_request and http_request_sync to only
perform the automatic TCP fallback when the HTTP method is safe/idempotent (GET,
HEAD, OPTIONS, PUT when proven idempotent) or when the caller provides an
explicit idempotency key header; otherwise return the original Err/5xx to the
caller and do not call post_request_via_tcp_proxy. Ensure you check the request
method (method param) and presence of an idempotency header before calling
can_fallback_to_raw_tcp/post_request_via_tcp_proxy and document the new
requirement in the function comments.
- Around line 1628-1647: The current raw-TCP header handling silently ignores
malformed or non-object JSON and unconditionally appends Content-Type, causing
behavior divergence from the HTTP branch; change the logic around the header
variable so that if header is non-empty you strictly parse it with
serde_json::from_str and if parsing fails or the parsed value is not an object
return or propagate an error (matching the HTTP path) instead of ignoring it;
when parsing succeeds iterate the object and push HeaderEntry (name/value) into
headers as before, and only inject a default Content-Type HeaderEntry if the
parsed headers did not already contain one (to mirror parse_simple_header
behavior).
- Around line 1143-1155: The function should_use_tcp_proxy_for_api_url currently
calls is_public(api_url) on the raw string; instead parse both url and api_url
first (use url::Url::parse) and run is_public on the parsed api host (or its
registrable domain like "rustdesk.com"/"*.rustdesk.com") so URLs with explicit
ports (e.g. "https://admin.rustdesk.com:21114") are correctly treated as public
and not proxied; adjust the logic in should_use_tcp_proxy_for_api_url to check
parsed api_host via is_public(parsed_api_host) and then compare parsed host
strings for equality, and add a regression test that asserts a URL with an
explicit port on the public api host is not proxied.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68bdcbf6-2278-4a14-9df2-33fd13a3c968

📥 Commits

Reviewing files that changed from the base of the PR and between f02cd9c and 51b562c.

📒 Files selected for processing (1)
  • src/common.rs

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6f590a07b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/common.rs (2)

1649-1656: ⚠️ Potential issue | 🟠 Major

Keep proxy-mode headers identical to the HTTP branch.

http_request_http forwards only the caller’s headers, but this path always appends Content-Type: application/json. Once proxy mode or fallback kicks in, the same logical request can go out with a different header set, and a caller-supplied Content-Type becomes duplicated.

💡 Proposed fix
 async fn http_request_via_tcp_proxy(
     url: &str,
     method: &str,
     body: Option<&str>,
     header: &str,
 ) -> ResultType<String> {
-    let mut headers = parse_json_header_entries(header)?;
+    let headers = parse_json_header_entries(header)?;
     let body_bytes = body.unwrap_or("").as_bytes();
-    // Always include Content-Type for consistency with parse_simple_header
-    headers.push(HeaderEntry {
-        name: "Content-Type".into(),
-        value: "application/json".into(),
-        ..Default::default()
-    });
 
     let resp = tcp_proxy_request(method, url, body_bytes, headers).await?;
     http_proxy_response_to_json(resp)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1649 - 1656, The proxy/fallback path is
unconditionally appending a Content-Type header which can duplicate
caller-supplied headers and diverge from the http_request_http behavior; update
the logic in the block that calls parse_json_header_entries(header) (and builds
headers/Body bytes) to only push the HeaderEntry { name: "Content-Type", value:
"application/json", .. } when the parsed headers do not already contain a
Content-Type (use a case-insensitive check on HeaderEntry.name), so the header
set remains identical to the HTTP branch and avoids duplicate Content-Type
entries.

1333-1346: ⚠️ Potential issue | 🔴 Critical

Avoid automatic TCP fallback for non-idempotent calls.

These wrappers retry on any transport error or 5xx. For post_request, and for non-idempotent http_request_sync calls, the upstream may already have applied the write before the client observes that failure, so the TCP retry can duplicate side effects. src/hbbs_http/sync.rs:210 already sends a mutating request through post_request.

Also applies to: 1617-1630

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1333 - 1346, The TCP-proxy fallback currently
runs for any transport error or 5xx, which can duplicate side-effects for
non-idempotent requests; update the fallback guard so post_request and
non-idempotent http_request_sync calls never fall back to
post_request_via_tcp_proxy. Concretely, in the block that calls
post_request_http and then maybe post_request_via_tcp_proxy, remove or
short-circuit the fallback when the request is non-idempotent (e.g., HTTP POST)
by checking the request method (or add an is_idempotent_method helper) before
evaluating can_fallback_to_raw_tcp(&url); apply the same guard for the other
occurrence around the http_request_sync path so only idempotent methods
(GET/HEAD/PUT/DELETE as appropriate) use the TCP fallback. Ensure you reference
and modify the logic around post_request_http, post_request_via_tcp_proxy,
can_fallback_to_raw_tcp, post_request, and http_request_sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/common.rs`:
- Around line 2788-2809: The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates process-global Config
(via Config::set_option/get_option and keys::OPTION_CUSTOM_RENDEZVOUS_SERVER)
and only restores it on drop, which can race with other tests; serialize this by
acquiring a global test mutex (or applying a serial-test attribute) around the
test body so the temporary Config change is exclusive while calling
get_tcp_proxy_addr() (and still restore the original value in
RestoreCustomRendezvousServer on drop); reference the test name,
RestoreCustomRendezvousServer, Config::set_option/get_option,
keys::OPTION_CUSTOM_RENDEZVOUS_SERVER, get_tcp_proxy_addr, and RENDEZVOUS_PORT
when making the change.
- Around line 1276-1290: The parser parse_json_header_entries currently fails on
an empty or whitespace-only header string; change it to treat trimmed empty
input as "no headers" by returning Ok(vec![]) early (instead of attempting to
parse), and keep existing behavior for non-empty input (parsing to
Vec<HeaderEntry>); update callers get_http_response_async and
http_request_via_tcp_proxy tests by adding a regression test that passes an
empty string (and a whitespace string) to ensure they succeed and no
request-level error occurs.

---

Duplicate comments:
In `@src/common.rs`:
- Around line 1649-1656: The proxy/fallback path is unconditionally appending a
Content-Type header which can duplicate caller-supplied headers and diverge from
the http_request_http behavior; update the logic in the block that calls
parse_json_header_entries(header) (and builds headers/Body bytes) to only push
the HeaderEntry { name: "Content-Type", value: "application/json", .. } when the
parsed headers do not already contain a Content-Type (use a case-insensitive
check on HeaderEntry.name), so the header set remains identical to the HTTP
branch and avoids duplicate Content-Type entries.
- Around line 1333-1346: The TCP-proxy fallback currently runs for any transport
error or 5xx, which can duplicate side-effects for non-idempotent requests;
update the fallback guard so post_request and non-idempotent http_request_sync
calls never fall back to post_request_via_tcp_proxy. Concretely, in the block
that calls post_request_http and then maybe post_request_via_tcp_proxy, remove
or short-circuit the fallback when the request is non-idempotent (e.g., HTTP
POST) by checking the request method (or add an is_idempotent_method helper)
before evaluating can_fallback_to_raw_tcp(&url); apply the same guard for the
other occurrence around the http_request_sync path so only idempotent methods
(GET/HEAD/PUT/DELETE as appropriate) use the TCP fallback. Ensure you reference
and modify the logic around post_request_http, post_request_via_tcp_proxy,
can_fallback_to_raw_tcp, post_request, and http_request_sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7041c87-b3da-484b-9ee2-62c13caf034d

📥 Commits

Reviewing files that changed from the base of the PR and between 51b562c and 6f590a0.

📒 Files selected for processing (1)
  • src/common.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (4)
src/common.rs (4)

1276-1289: ⚠️ Potential issue | 🟠 Major

Treat empty header JSON as “no headers.”

serde_json::from_str("") fails, but Lines 1474-1476 and Line 1649 call this helper unconditionally. Any caller that passes "" or whitespace now errors locally before any request is sent. Return Ok(vec![]) for trimmed-empty input and add regression coverage for both empty and whitespace-only strings.

Suggested fix
 fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
+    if header.trim().is_empty() {
+        return Ok(Vec::new());
+    }
     let v: Value = serde_json::from_str(header)?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1276 - 1289, The parse_json_header_entries helper
should treat trimmed-empty input as "no headers": in
parse_json_header_entries(header: &str) return Ok(vec![]) immediately when
header.trim().is_empty() instead of calling serde_json::from_str; otherwise
proceed with the existing serde_json parsing and construction of
Vec<HeaderEntry>. Update tests to add regression coverage calling
parse_json_header_entries with "" and with a whitespace-only string to assert
they both return Ok(empty vec). Ensure the function still returns the same
ResultType and preserves the existing error path for non-object JSON.

1143-1155: ⚠️ Potential issue | 🟠 Major

Base the public-host exclusion on the parsed host.

Lines 1056-1060 only strip :21114 when OPTION_ALLOW_HTTPS_21114 != "Y". With that builtin enabled, https://admin.rustdesk.com:21114 can still reach this helper, is_public(api_url) returns false on the raw string, and the public admin host becomes proxyable once the parsed hosts match. Check is_public against api_host instead, and add a regression case for the explicit-port public URL.

Suggested fix
 fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool {
-    if api_url.is_empty() || is_public(api_url) {
+    if api_url.is_empty() {
         return false;
     }
@@
-    matches!((target_host, api_host), (Some(target), Some(api)) if target == api)
+    matches!(
+        (target_host, api_host),
+        (Some(target), Some(api)) if !is_public(&api) && target == api
+    )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1143 - 1155, The initial public-host check uses
the raw api_url string which can be non-public only due to an explicit port
(e.g. ":21114"); update should_use_tcp_proxy_for_api_url to parse api_url first
and call is_public on the parsed host (api_host) rather than the raw api_url,
keeping the fallback behavior when parsing fails; ensure the function still
extracts target_host and api_host via url::Url::parse and then performs
is_public(&api_host) before deciding to return false, and add a regression test
that verifies a public host with an explicit port (e.g.
"https://admin.rustdesk.com:21114") is treated as public and not proxyable.

2782-2803: ⚠️ Potential issue | 🟡 Minor

Serialize this test's global Config mutation.

This test temporarily rewrites OPTION_CUSTOM_RENDEZVOUS_SERVER and only restores it on drop. cargo test runs tests in parallel by default, so another test can observe the temporary rendezvous value and fail nondeterministically. Guard the body with a shared test mutex or a serial-test style attribute.

Suggested fix
+    static TEST_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
     #[test]
     fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() {
+        let _guard = TEST_MUTEX.lock().unwrap();
         struct RestoreCustomRendezvousServer(String);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 2782 - 2803, The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates global Config via
Config::set_option/Config::get_option for OPTION_CUSTOM_RENDEZVOUS_SERVER and
must be serialized to avoid races; wrap the test body with a global test mutex
or add a serial-test style attribute so only one test manipulates Config at a
time, keeping the RestoreCustomRendezvousServer drop-based restore and using the
same get_tcp_proxy_addr assertion unchanged.

1334-1353: ⚠️ Potential issue | 🔴 Critical

Don't auto-fallback mutating requests after ambiguous failures.

Err(_) here is broader than transport failures, and post_request is always replaying a POST. Retrying through the TCP proxy after a timeout or 5xx can duplicate server-side side effects; in http_request_sync the same block also affects POST/PUT/DELETE. Restrict automatic fallback to verified transport failures on idempotent requests, or require an idempotency guarantee from the caller.

Also applies to: 1618-1636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1334 - 1353, The current fallback logic uses
Err(_) and 5xx to trigger TCP proxy retries in post_request (and
http_request_sync), which can replay mutating POST/PUT/DELETE requests; change
it to only fall back for verified transport failures and idempotent requests:
inspect the HTTP method (or require an idempotency guarantee header) and only
allow fallback when method is idempotent (e.g., GET/HEAD/OPTIONS) or an
Idempotency-Key is present, and change the Err(_) check to detect
transport-level errors (e.g., connection refused, timeout, or
reqwest::Error::is_connect()/is_timeout()) instead of matching all errors;
update should_fallback calculation (the match on http_result), the log call
using tcp_proxy_fallback_log_condition(), and the fallback call site
post_request_via_tcp_proxy so non-idempotent requests are not retried
automatically (and mirror the same change in http_request_sync).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/common.rs`:
- Around line 1276-1289: The parse_json_header_entries helper should treat
trimmed-empty input as "no headers": in parse_json_header_entries(header: &str)
return Ok(vec![]) immediately when header.trim().is_empty() instead of calling
serde_json::from_str; otherwise proceed with the existing serde_json parsing and
construction of Vec<HeaderEntry>. Update tests to add regression coverage
calling parse_json_header_entries with "" and with a whitespace-only string to
assert they both return Ok(empty vec). Ensure the function still returns the
same ResultType and preserves the existing error path for non-object JSON.
- Around line 1143-1155: The initial public-host check uses the raw api_url
string which can be non-public only due to an explicit port (e.g. ":21114");
update should_use_tcp_proxy_for_api_url to parse api_url first and call
is_public on the parsed host (api_host) rather than the raw api_url, keeping the
fallback behavior when parsing fails; ensure the function still extracts
target_host and api_host via url::Url::parse and then performs
is_public(&api_host) before deciding to return false, and add a regression test
that verifies a public host with an explicit port (e.g.
"https://admin.rustdesk.com:21114") is treated as public and not proxyable.
- Around line 2782-2803: The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates global Config via
Config::set_option/Config::get_option for OPTION_CUSTOM_RENDEZVOUS_SERVER and
must be serialized to avoid races; wrap the test body with a global test mutex
or add a serial-test style attribute so only one test manipulates Config at a
time, keeping the RestoreCustomRendezvousServer drop-based restore and using the
same get_tcp_proxy_addr assertion unchanged.
- Around line 1334-1353: The current fallback logic uses Err(_) and 5xx to
trigger TCP proxy retries in post_request (and http_request_sync), which can
replay mutating POST/PUT/DELETE requests; change it to only fall back for
verified transport failures and idempotent requests: inspect the HTTP method (or
require an idempotency guarantee header) and only allow fallback when method is
idempotent (e.g., GET/HEAD/OPTIONS) or an Idempotency-Key is present, and change
the Err(_) check to detect transport-level errors (e.g., connection refused,
timeout, or reqwest::Error::is_connect()/is_timeout()) instead of matching all
errors; update should_fallback calculation (the match on http_result), the log
call using tcp_proxy_fallback_log_condition(), and the fallback call site
post_request_via_tcp_proxy so non-idempotent requests are not retried
automatically (and mirror the same change in http_request_sync).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb7a6cbe-1042-4cd1-8501-8de51774332e

📥 Commits

Reviewing files that changed from the base of the PR and between 6f590a0 and 27c0cd4.

📒 Files selected for processing (1)
  • src/common.rs

Signed-off-by: 21pages <sunboeasy@gmail.com>
@21pages
Copy link
Copy Markdown
Collaborator

21pages commented Mar 29, 2026

Tested:

  • OPTION_USE_RAW_TCP_FOR_API with option off and on: login / OIDC / 2FA / Address book / connection log / strategy sync
  • fallback to TCP HTTP proxy
  • compared HTTP payloads between option off and on

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
src/common.rs (3)

1276-1290: ⚠️ Potential issue | 🟠 Major

Treat blank header payloads as “no headers”.

http_request_sync() now routes every header string through this parser, so "" / whitespace fail locally with an EOF parse error before either transport is attempted. Return Ok(vec![]) for trimmed-empty input and add a regression test next to the new header-parser tests.

💡 Proposed fix
 fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
+    if header.trim().is_empty() {
+        return Ok(Vec::new());
+    }
     let v: Value = serde_json::from_str(header)?;
     if let Value::Object(obj) = v {
         Ok(obj
             .iter()
             .map(|(key, value)| HeaderEntry {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1276 - 1290, parse_json_header_entries currently
fails on empty/whitespace header strings because serde_json::from_str is called
unconditionally; change parse_json_header_entries to trim the header argument
and return Ok(vec![]) when trimmed header is empty, otherwise proceed to parse
as JSON as before (keep producing Vec<HeaderEntry> on success and the existing
error on non-object JSON). Also add a regression test alongside the existing
header-parser tests that calls http_request_sync() (or directly
parse_json_header_entries) with "" and whitespace-only strings to assert
Ok(vec![]) is returned.

2792-2813: ⚠️ Potential issue | 🟡 Minor

Serialize this test’s global Config mutation.

This test rewrites process-global rendezvous state and restores it only on drop. cargo test runs tests in parallel by default, so another test can observe the temporary "1:2" value and fail nondeterministically. Guard the body with a shared test mutex before calling Config::set_option() / get_tcp_proxy_addr().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 2792 - 2813, The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates global rendezvous
state via Config::set_option/get_option and must be serialized; wrap the body
that sets/restores keys::OPTION_CUSTOM_RENDEZVOUS_SERVER and calls
get_tcp_proxy_addr() in the project-wide test mutex (the shared test lock used
across tests) so the global Config mutation cannot run concurrently with other
tests—acquire the mutex at the start of the test, perform the
Config::set_option, call get_tcp_proxy_addr(), assert the value, then
drop/release the mutex after restoring state via RestoreCustomRendezvousServer.

1328-1358: ⚠️ Potential issue | 🔴 Critical

Limit TCP fallback to retry-safe cases.

Both wrappers still retry on every Err(_) and every 5xx. That can replay mutations after the upstream already applied them, and in http_request_sync() it also treats local failures like unsupported methods or bad header JSON as retryable. Only fall back on proven transport/connect failures, and only for idempotent requests or when the caller supplies an idempotency guard.

Also applies to: 1607-1636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1328 - 1358, post_request currently falls back to
TCP for any Err(_) or any 5xx, which can replay non-idempotent mutations; change
the fallback logic in post_request (and the same pattern in http_request_sync)
so we only attempt post_request_via_tcp_proxy when the HTTP error is a proven
transport/connect failure (e.g., timeout, connection reset, DNS/connection
refused) OR when the response is 5xx and the request is idempotent or the caller
provided an idempotency guard/header. Concretely, replace the broad Err(_) /
status>=500 check with: (1) inspect the Err variant from post_request_http to
detect transport/connect errors and only treat those as retryable; (2) for
Ok((status, _)) only allow fallback for >=500 when either the request is
idempotent (detect via method/flags you have) or an idempotency header/guard is
present; and (3) keep can_fallback_to_raw_tcp(&url) gating. Apply the same
stricter check in http_request_sync where it currently treats local failures as
retryable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/common.rs`:
- Around line 1276-1290: parse_json_header_entries currently fails on
empty/whitespace header strings because serde_json::from_str is called
unconditionally; change parse_json_header_entries to trim the header argument
and return Ok(vec![]) when trimmed header is empty, otherwise proceed to parse
as JSON as before (keep producing Vec<HeaderEntry> on success and the existing
error on non-object JSON). Also add a regression test alongside the existing
header-parser tests that calls http_request_sync() (or directly
parse_json_header_entries) with "" and whitespace-only strings to assert
Ok(vec![]) is returned.
- Around line 2792-2813: The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates global rendezvous
state via Config::set_option/get_option and must be serialized; wrap the body
that sets/restores keys::OPTION_CUSTOM_RENDEZVOUS_SERVER and calls
get_tcp_proxy_addr() in the project-wide test mutex (the shared test lock used
across tests) so the global Config mutation cannot run concurrently with other
tests—acquire the mutex at the start of the test, perform the
Config::set_option, call get_tcp_proxy_addr(), assert the value, then
drop/release the mutex after restoring state via RestoreCustomRendezvousServer.
- Around line 1328-1358: post_request currently falls back to TCP for any Err(_)
or any 5xx, which can replay non-idempotent mutations; change the fallback logic
in post_request (and the same pattern in http_request_sync) so we only attempt
post_request_via_tcp_proxy when the HTTP error is a proven transport/connect
failure (e.g., timeout, connection reset, DNS/connection refused) OR when the
response is 5xx and the request is idempotent or the caller provided an
idempotency guard/header. Concretely, replace the broad Err(_) / status>=500
check with: (1) inspect the Err variant from post_request_http to detect
transport/connect errors and only treat those as retryable; (2) for Ok((status,
_)) only allow fallback for >=500 when either the request is idempotent (detect
via method/flags you have) or an idempotency header/guard is present; and (3)
keep can_fallback_to_raw_tcp(&url) gating. Apply the same stricter check in
http_request_sync where it currently treats local failures as retryable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c5f68b01-9558-41cb-b5c7-0f3a91d20417

📥 Commits

Reviewing files that changed from the base of the PR and between 27c0cd4 and 9f2ce33.

📒 Files selected for processing (1)
  • src/common.rs

21pages added 2 commits March 29, 2026 16:21
… clone

Signed-off-by: 21pages <sunboeasy@gmail.com>
Signed-off-by: 21pages <sunboeasy@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: 21pages <sunboeasy@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/common.rs (3)

1350-1368: ⚠️ Potential issue | 🔴 Critical

Don’t replay mutating requests after ambiguous failures.

These branches retry on every transport error and every 5xx. For post_request, and for http_request_sync when method is post / put / delete, the upstream may already have applied the mutation before the client observes the failure, so the TCP fallback can duplicate side effects.

Also applies to: 1631-1646

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1350 - 1368, The code currently retries via TCP
proxy on any transport error or 5xx for post_request and http_request_sync;
prevent replaying mutating requests by restricting the TCP fallback to
safe/idempotent methods only (e.g., allow fallback for GET, HEAD, OPTIONS, PUT
when known idempotent, but disallow for POST/PUT/DELETE that can cause side
effects) and by checking the HTTP method before calling
can_fallback_to_raw_tcp/post_request_via_tcp_proxy; update the logic in
post_request and http_request_sync to consult the request method (and any
idempotency markers) and skip the TCP fallback branch for non-idempotent methods
so mutating requests are not retried.

1296-1310: ⚠️ Potential issue | 🟠 Major

Treat empty header JSON as “no headers”.

parse_json_header_entries("") now errors, but both callers invoke it even when the request has no headers. That turns a header-less http_request_sync / TCP-proxy call into a local parse failure.

💡 Proposed fix
 fn parse_json_header_entries(header: &str) -> ResultType<Vec<HeaderEntry>> {
+    if header.trim().is_empty() {
+        return Ok(Vec::new());
+    }
     let v: Value = serde_json::from_str(header)?;
     if let Value::Object(obj) = v {
         Ok(obj
             .iter()
             .map(|(key, value)| HeaderEntry {

Please add a regression test for both "" and whitespace-only input.

Also applies to: 1493-1495, 1662-1663

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 1296 - 1310, The function
parse_json_header_entries currently fails on empty or whitespace-only input and
should treat these cases as "no headers"; modify parse_json_header_entries to
trim the header string and if it is empty return Ok(vec![]) before attempting
serde_json::from_str, keeping the rest of the logic unchanged (use the existing
HeaderEntry construction and error path for non-object JSON). Add regression
tests that call parse_json_header_entries with "" and with a whitespace-only
string and assert an empty Vec is returned, and also add integration tests
exercising the callers (http_request_sync and the TCP-proxy path) to ensure they
no longer error when headers are absent.

2805-2827: ⚠️ Potential issue | 🟡 Minor

Serialize this test’s process-global Config mutation.

This test rewrites keys::OPTION_CUSTOM_RENDEZVOUS_SERVER and only restores it on drop. cargo test runs tests in parallel by default, so another test can observe the temporary value and fail nondeterministically.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/common.rs` around lines 2805 - 2827, The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates the global Config via
Config::set_option/get_option which can race with other tests; serialize this
mutation by running the test under a test-level lock or serial test attribute
(e.g. use serial_test::serial or your project’s test serialization utility) so
no other test can observe the temporary keys::OPTION_CUSTOM_RENDEZVOUS_SERVER
value; keep the RestoreCustomRendezvousServer drop-based restore and wrap or
annotate the test (or surrounding setup) to acquire the global test lock before
calling Config::set_option and release it on drop.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/common.rs`:
- Around line 1350-1368: The code currently retries via TCP proxy on any
transport error or 5xx for post_request and http_request_sync; prevent replaying
mutating requests by restricting the TCP fallback to safe/idempotent methods
only (e.g., allow fallback for GET, HEAD, OPTIONS, PUT when known idempotent,
but disallow for POST/PUT/DELETE that can cause side effects) and by checking
the HTTP method before calling
can_fallback_to_raw_tcp/post_request_via_tcp_proxy; update the logic in
post_request and http_request_sync to consult the request method (and any
idempotency markers) and skip the TCP fallback branch for non-idempotent methods
so mutating requests are not retried.
- Around line 1296-1310: The function parse_json_header_entries currently fails
on empty or whitespace-only input and should treat these cases as "no headers";
modify parse_json_header_entries to trim the header string and if it is empty
return Ok(vec![]) before attempting serde_json::from_str, keeping the rest of
the logic unchanged (use the existing HeaderEntry construction and error path
for non-object JSON). Add regression tests that call parse_json_header_entries
with "" and with a whitespace-only string and assert an empty Vec is returned,
and also add integration tests exercising the callers (http_request_sync and the
TCP-proxy path) to ensure they no longer error when headers are absent.
- Around line 2805-2827: The test
test_get_tcp_proxy_addr_normalizes_bare_ipv6_host mutates the global Config via
Config::set_option/get_option which can race with other tests; serialize this
mutation by running the test under a test-level lock or serial test attribute
(e.g. use serial_test::serial or your project’s test serialization utility) so
no other test can observe the temporary keys::OPTION_CUSTOM_RENDEZVOUS_SERVER
value; keep the RestoreCustomRendezvousServer drop-based restore and wrap or
annotate the test (or surrounding setup) to acquire the global test lock before
calling Config::set_option and release it on drop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc9db496-f86a-4122-b764-349abbb24e86

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2ce33 and a965e8c.

📒 Files selected for processing (1)
  • src/common.rs

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: 21pages <sunboeasy@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: 21pages <sunboeasy@gmail.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…ll timeout

- Extract duplicated TCP proxy fallback logic into generic
  `with_tcp_proxy_fallback` helper used by both `post_request` and
  `http_request_sync`, eliminating code drift risk
- Allow caller-supplied Content-Type to override the default in
  `parse_simple_header` instead of silently dropping it
- Take body by reference in `post_request_http` to avoid eager clone
  when no fallback is needed
- Wrap entire `tcp_proxy_request` flow (connect + handshake + send +
  receive) in an overall timeout to prevent indefinite stalls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e7484c51b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

21pages added 2 commits March 31, 2026 11:05
… URLs

Signed-off-by: 21pages <sunboeasy@gmail.com>
…eping TLS warmup

Signed-off-by: 21pages <sunboeasy@gmail.com>
@21pages
Copy link
Copy Markdown
Collaborator

21pages commented Mar 31, 2026

Changes:

  • OIDC also talks to the hbbs API server, so it now supports the TCP HTTP proxy path.
  • make is_public case-insensitive

Tested:

  • OIDC login works with the option enabled/disabled
  • Software update URL can still be fetched with the option enabled/disabled

Note:

  • The avatar URL does not start with /api/, but it is loaded directly by the UI control rather than through Rust HTTP request handling.
  • record upload was not updated because it is not used.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…e method

  Remove TryFrom<Response> impl that was never called and replace the
  private parse_hbb_http_response helper in account.rs with a public
  parse() method on HbbHttpResponse, eliminating code duplication.

Signed-off-by: 21pages <sunboeasy@gmail.com>
@rustdesk rustdesk merged commit 4e30ee8 into master Apr 3, 2026
20 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.

3 participants