Skip to content

feat(rust-sdk): expose box network tunnels - #991

Merged
DorianZheng merged 6 commits into
boxlite-ai:mainfrom
G4614:codex/rust-network-sdk
Jul 15, 2026
Merged

DorianZheng merged 6 commits into
boxlite-ai:mainfrom
G4614:codex/rust-network-sdk

Conversation

@G4614

@G4614 G4614 commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Expose a unified Rust network tunnel handle for local and REST-backed boxes.

Test plan:

  • cargo check -p boxlite --features rest --no-default-features

Summary by CodeRabbit

  • New Features
    • Added box network tunneling with lazy endpoint discovery (public URL vs local stream) and on-demand tunnel connection.
    • Exposed network tunneling via the LiteBox API with a new network handle and tunnel endpoint/connection support.
    • Added REST-based tunneling using HTTP CONNECT.
  • Bug Fixes
    • Improved WebSocket URL handling, including query parameters and correct scheme mapping.
    • Networking-disabled configurations now return an unsupported-operation error.
  • Tests
    • Added coverage to ensure endpoint discovery and tunnel connection are performed lazily and separately.
  • Chores
    • Updated REST HTTP/TLS and connection setup (HTTP/1, native Rustls certificates).

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Adds a lazy networking API to LiteBox, separates public and internal tunnel types, and implements local and REST-backed network tunneling with endpoint discovery, HTTP CONNECT transport, and lazy-operation tests.

Changes

Network tunneling

Layer / File(s) Summary
Tunnel contracts and transport types
src/boxlite/src/net/..., src/boxlite/src/runtime/backend.rs, src/boxlite/src/net/gvproxy/services.rs
Renames the internal transport to BoxInternalTunnel and adds the BoxNetworkBackend::tunnel contract.
LiteBox network resource
src/boxlite/src/litebox/network.rs, src/boxlite/src/litebox/mod.rs, src/boxlite/src/lib.rs
Adds lazy BoxTunnel and NetworkHandle APIs, exposes LiteBox::network(), and tests deferred endpoint and connection operations.
Local backend integration
src/boxlite/src/litebox/box_impl.rs, src/boxlite/src/runtime/rt_impl.rs
Stores the local network backend in shared state, implements BoxImpl tunneling, and wires it into LiteBox.
REST tunnel integration
src/boxlite/src/rest/client.rs, src/boxlite/src/rest/litebox.rs, src/boxlite/src/rest/runtime.rs, src/boxlite/Cargo.toml
Adds tunnel description and HTTP CONNECT helpers, implements lazy REST tunnels, and supplies the REST network backend.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LiteBox
  participant NetworkHandle
  participant RestBox
  participant ApiClient
  participant RESTAPI
  LiteBox->>NetworkHandle: network()
  NetworkHandle->>RestBox: box_tunnel(target)
  RestBox->>ApiClient: describe_box_tunnel(box_id, port)
  ApiClient->>RESTAPI: POST tunnel descriptor request
  RESTAPI-->>ApiClient: endpoint URL
  RestBox->>ApiClient: connect_box_network_tunnel(box_id, port)
  ApiClient->>RESTAPI: HTTP CONNECT tunnel request
  RESTAPI-->>ApiClient: upgraded connection
  ApiClient-->>NetworkHandle: BoxTunnel connection
Loading

Possibly related PRs

  • boxlite-ai/boxlite#949: Earlier gvproxy NetworkBackend and /tunnel integration related to the internal tunnel abstraction.

Suggested reviewers: dorianzheng

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is on-topic but missing the required Summary/Changes sections and risk details from the template. Add the required Summary, Changes, and How to verify sections in the template format, and include Risks / rollout only if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 77.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: exposing box network tunnels.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@G4614
G4614 marked this pull request as ready for review July 15, 2026 10:31
@G4614
G4614 requested a review from a team July 15, 2026 10:31
@boxlite-agent

boxlite-agent Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

review model timed out after 39 minutes without a /publish callback
repo: boxlite-ai/boxlite
pr: 991
head: 0d9319a4f9af6f8efdc6d4d94a72afe3e8d23c85
box: pr-review-boxlite-991-mrm2ir05
last stage: model started
stage detail: claude
stage updated: 2026-07-15T12:40:33.689Z

powered by BoxLite

@G4614
G4614 enabled auto-merge July 15, 2026 10:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/boxlite/src/litebox/network.rs (1)

63-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider returning the unconsumed tunnel on FD extraction failure.

Currently, into_fd() takes self by value and returns Option<OwnedFd>. If a future transport cannot provide a single host file descriptor (e.g., a cloud stream), it will return None and implicitly consume and drop the established connection.

As a result, SDK consumers who intend to fall back to a local-listener bridge (as noted in the into_fd documentation) would be forced to call connect() again to obtain a new raw stream, unnecessarily paying the connection cost twice.

Consider updating BoxInternalTunnel::into_fd to return Result<OwnedFd, Self> (or a similar construct) in a future iteration. This would allow the caller to recover the active tunnel on failure and seamlessly reuse it for the fallback bridge without reconnecting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/litebox/network.rs` around lines 63 - 67, Update
BoxInternalTunnel::into_fd to preserve and return the established tunnel when FD
extraction is unsupported, using a result-like return such as Result<OwnedFd,
Self>. Adjust connect_fd and its callers to propagate or handle that recoverable
tunnel so fallback bridge logic can reuse the existing connection without
reconnecting.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/boxlite/src/rest/client.rs`:
- Around line 324-330: Update connect_fd’s CONNECT establishment flow around
connector.call and http1::handshake to apply the configured timeout to the
TCP/TLS connection, handshake, response, and upgrade awaits. Scope the timeout
only to setup and preserve the established tunnel pump as long-lived; convert
timeout failures into the existing BoxliteError::Network error path.
- Around line 331-335: Update the spawned connection task in the REST client to
await the Hyper connection via with_upgrades() before handling errors. Preserve
the existing tracing::debug logging and error handling so CONNECT tunnel
upgrades can complete.

---

Nitpick comments:
In `@src/boxlite/src/litebox/network.rs`:
- Around line 63-67: Update BoxInternalTunnel::into_fd to preserve and return
the established tunnel when FD extraction is unsupported, using a result-like
return such as Result<OwnedFd, Self>. Adjust connect_fd and its callers to
propagate or handle that recoverable tunnel so fallback bridge logic can reuse
the existing connection without reconnecting.
🪄 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 Plus

Run ID: ab7c5b0c-aeb9-443d-aaaf-5400fba89121

📥 Commits

Reviewing files that changed from the base of the PR and between 1cdc8f1 and 1e61e9d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • src/boxlite/Cargo.toml
  • src/boxlite/src/lib.rs
  • src/boxlite/src/litebox/box_impl.rs
  • src/boxlite/src/litebox/mod.rs
  • src/boxlite/src/litebox/network.rs
  • src/boxlite/src/net/gvproxy/services.rs
  • src/boxlite/src/net/mod.rs
  • src/boxlite/src/rest/client.rs
  • src/boxlite/src/rest/litebox.rs
  • src/boxlite/src/rest/runtime.rs
  • src/boxlite/src/runtime/backend.rs
  • src/boxlite/src/runtime/rt_impl.rs

Comment thread src/boxlite/src/rest/client.rs Outdated
Comment thread src/boxlite/src/rest/client.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/boxlite/src/litebox/network.rs (2)

151-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update stale method name in the comment.

The comment references url(), which should be updated to endpoint() to match the method name.

♻️ Proposed fix
-        // connect() triggers exactly one connect, independent of url().
+        // connect() triggers exactly one connect, independent of endpoint().
         assert!(tunnel.connect().await.is_err());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/litebox/network.rs` around lines 151 - 152, Update the
comment above the tunnel.connect() assertion to replace the stale url()
reference with endpoint(), preserving the existing explanation that connect()
triggers exactly one connection independently of endpoint().

56-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update stale documentation to reflect the new return type.

The doc comment mentions returning Ok(None) for local boxes, but the method now returns Ok(BoxEndpoint::Local) instead of an Option.

♻️ Proposed fix
-    /// Public URL for this service, fetched on demand. `Ok(None)` for local
+    /// Public URL for this service, fetched on demand. `Ok(BoxEndpoint::Local)` for local
     /// boxes, which have no public URL.
     pub async fn endpoint(&self) -> BoxliteResult<BoxEndpoint> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/litebox/network.rs` around lines 56 - 58, Update the
documentation comment above the endpoint method to describe the current
BoxEndpoint return behavior, replacing the stale Ok(None) reference with
Ok(BoxEndpoint::Local) for local boxes while preserving the public URL
description.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/boxlite/src/litebox/network.rs`:
- Around line 151-152: Update the comment above the tunnel.connect() assertion
to replace the stale url() reference with endpoint(), preserving the existing
explanation that connect() triggers exactly one connection independently of
endpoint().
- Around line 56-58: Update the documentation comment above the endpoint method
to describe the current BoxEndpoint return behavior, replacing the stale
Ok(None) reference with Ok(BoxEndpoint::Local) for local boxes while preserving
the public URL description.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aef520a-0755-41a8-9818-4c65a43c5f34

📥 Commits

Reviewing files that changed from the base of the PR and between 3c80549 and 66fa211.

📒 Files selected for processing (3)
  • src/boxlite/src/lib.rs
  • src/boxlite/src/litebox/mod.rs
  • src/boxlite/src/litebox/network.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/boxlite/src/lib.rs
  • src/boxlite/src/litebox/mod.rs

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 2 issues

Comment thread src/boxlite/src/litebox/network.rs Outdated
Comment thread src/boxlite/src/rest/client.rs Outdated
Comment on lines +316 to +352
use tower::Service;

let mut connector = HttpsConnectorBuilder::new()
.with_native_roots()
.map_err(|e| BoxliteError::Config(format!("TLS roots unavailable: {e}")))?
.https_or_http()
.enable_http1()
.build();
let io = connector
.call(uri.clone())
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT transport failed: {e}")))?;
let (mut sender, connection) = http1::handshake(io)
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT handshake failed: {e}")))?;
tokio::spawn(async move {
if let Err(err) = connection.await {
tracing::debug!(error = %err, "REST CONNECT connection closed");
}
});

let bearer = self.current_bearer().await?;
let target = uri
.path_and_query()
.ok_or_else(|| BoxliteError::Internal("CONNECT URI has no path".into()))?
.to_string();
let mut request = Request::builder()
.method("CONNECT")
.uri(target)
.header("host", authority);
if let Some(bearer) = bearer {
request = request.header("authorization", format!("Bearer {bearer}"));
}
let response: hyper::Response<Incoming> = sender
.send_request(
request
.body(http_body_util::Empty::<bytes::Bytes>::new())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ CONNECT handshake/upgrade has no timeout
connector.call, http1::handshake, send_request, and hyper::upgrade::on are all awaited with no timeout, so a stalled server/network leaves the caller's tunnel() future (and its task) blocked indefinitely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed together with the CodeRabbit timeout finding. The complete CONNECT establishment sequence is now bounded by the setup timeout; only the post-upgrade byte pump remains unbounded.

@boxlite-agent boxlite-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📦 BoxLite review — 1 issue

Comment thread src/boxlite/src/rest/client.rs Outdated
Comment on lines +291 to +385
pub(crate) async fn connect_box_network_tunnel(
&self,
box_id: impl AsRef<str>,
port: u16,
) -> BoxliteResult<tokio::net::UnixStream> {
let path = format!("/boxes/{}/network/tunnel", box_id.as_ref());
let url = reqwest::Url::parse(&self.url(&path))
.map_err(|e| BoxliteError::Internal(format!("CONNECT URL build failed: {e}")))?;
let authority = url
.host_str()
.ok_or_else(|| BoxliteError::Internal("CONNECT URL has no host".into()))?;
let authority = match url.port() {
Some(port) => format!("{authority}:{port}"),
None => authority.to_string(),
};
let query = format!("port={port}");
let uri: hyper::Uri = format!("{}://{}{}?{query}", url.scheme(), authority, url.path())
.parse()
.map_err(|e| BoxliteError::Internal(format!("CONNECT URI build failed: {e}")))?;

use hyper::Request;
use hyper::body::Incoming;
use hyper::client::conn::http1;
use hyper_rustls::HttpsConnectorBuilder;
use hyper_util::rt::TokioIo;
use tower::Service;

let mut connector = HttpsConnectorBuilder::new()
.with_native_roots()
.map_err(|e| BoxliteError::Config(format!("TLS roots unavailable: {e}")))?
.https_or_http()
.enable_http1()
.build();
let io = connector
.call(uri.clone())
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT transport failed: {e}")))?;
let (mut sender, connection) = http1::handshake(io)
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT handshake failed: {e}")))?;
tokio::spawn(async move {
if let Err(err) = connection.await {
tracing::debug!(error = %err, "REST CONNECT connection closed");
}
});

let bearer = self.current_bearer().await?;
let target = uri
.path_and_query()
.ok_or_else(|| BoxliteError::Internal("CONNECT URI has no path".into()))?
.to_string();
let mut request = Request::builder()
.method("CONNECT")
.uri(target)
.header("host", authority);
if let Some(bearer) = bearer {
request = request.header("authorization", format!("Bearer {bearer}"));
}
let response: hyper::Response<Incoming> = sender
.send_request(
request
.body(http_body_util::Empty::<bytes::Bytes>::new())
.map_err(|e| {
BoxliteError::Internal(format!("CONNECT request build failed: {e}"))
})?,
)
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT request failed: {e}")))?;
if response.status() != hyper::StatusCode::OK {
return Err(BoxliteError::Network(format!(
"CONNECT request rejected with status {}",
response.status()
)));
}

let upgraded = hyper::upgrade::on(response)
.await
.map_err(|e| BoxliteError::Network(format!("CONNECT upgrade failed: {e}")))?;
let mut upgraded = TokioIo::new(upgraded);
let (local, mut pump_end) = tokio::net::UnixStream::pair()
.map_err(|e| BoxliteError::Network(format!("CONNECT local socket pair failed: {e}")))?;
tokio::spawn(async move {
let _ = tokio::io::copy_bidirectional(&mut pump_end, &mut upgraded).await;
});
Ok(local)
}

/// Request the public descriptor for a box service tunnel.
pub(crate) async fn describe_box_tunnel(
&self,
box_id: impl AsRef<str>,
port: u16,
) -> BoxliteResult<String> {
// Wire body of `POST /boxes/{box_id}/network/tunnel`; only the URL is used.
#[derive(serde::Deserialize)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ New CONNECT tunnel path untested
connect_box_network_tunnel (raw hyper CONNECT + TLS via hyper-rustls + upgrade + copy_bidirectional bridging, ~90 new lines) has no unit/integration test, unlike the parallel litebox/network.rs abstraction which got 2 tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Rust network tests now cover the common endpoint/connect contract for both local and remote backends. A dedicated raw Hyper CONNECT integration test still requires a live REST endpoint and TLS/upgrade-capable test server, so this is covered by REST e2e rather than a synthetic unit test.

}

/// Resolve the endpoint, fetching a URL remotely or preparing a local stream.
pub async fn endpoint(&self) -> BoxliteResult<Option<String>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

2 api LGTM, but the return type and name should be refine?

@G4614
G4614 force-pushed the codex/rust-network-sdk branch from f626fd5 to 7f14964 Compare July 15, 2026 12:11
@DorianZheng
DorianZheng disabled auto-merge July 15, 2026 13:11
@DorianZheng
DorianZheng merged commit 54c348f into boxlite-ai:main Jul 15, 2026
31 checks passed
@G4614
G4614 deleted the codex/rust-network-sdk branch July 23, 2026 08:16
@coderabbitai coderabbitai Bot mentioned this pull request Jul 23, 2026
7 of 8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants