Skip to content

FuuuOverclocking/hyper-uds

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hyper-uds

Minimal HTTP/1.1-on-UDS RPC framework with SCM_RIGHTS file-descriptor passing.

hyper-uds is not a general-purpose HTTP server. It is a deliberately small framework for building RPC services that run inside a single host, talk over a Unix-domain socket, and need to hand each other open file descriptors as part of their request/response payloads.

It speaks just enough HTTP/1.1 to be debuggable with curl --unix-socket and to feel familiar to anyone who has used hyper, while shedding everything that does not serve the IPC use-case (TLS, h2, chunked encoding, pipelining, multi-host concerns).


Why does this exist?

Two common needs in container/sandbox/agent tooling do not have a clean intersection in the existing ecosystem:

  1. Structured RPC — request routing, headers, status codes, per-request bodies — all things HTTP gives you essentially for free.
  2. Transferring open file descriptors — pipes, memfds, sockets, pidfds — across process boundaries via SCM_RIGHTS ancillary messages on a Unix-domain socket.

Hand-rolled protocols on top of a UDS get fd passing, but you re-invent framing, headers and content negotiation. hyper (or tonic, axum, …) gives you the framing but treats the underlying transport as an opaque byte stream — there is no first-class place to put a received OwnedFd.

hyper-uds keeps the HTTP framing and lets requests/responses carry file descriptors directly, addressed by a logical name through an X-FD header.

Highlights

  • HTTP/1.1 over AF_UNIX — request line, headers, Content-Length body, status line, response headers. Wire-compatible with curl --unix-socket.
  • First-class fd passingRequest::fds() / Response::builder().fd(...). Descriptors are matched to logical names via an X-FD: name1,name2 header so callers do not rely on positional ordering.
  • Direct recvmsg(2) / sendmsg(2) through [rustix], driven by tokio's AsyncFd. No intermediate framing layer touches the bytes.
  • Bounded everything — head size, body size, fds-per-request are all configurable on the [Builder]. The defaults are conservative enough for IPC; raise them deliberately.
  • Single allocation per response head, single Vec<u8> reused across keep-alive iterations. Dominant cost on the hot path is the syscalls themselves.
  • No unsafe in the crate's own logic; unsafe_op_in_unsafe_fn is forbidden at the crate root. The transport relies on rustix and tokio::io::unix::AsyncFd for the unsafe bits.

Non-goals

  • TLS, HTTP/2, HTTP/3, websockets.
  • Chunked transfer encoding (request bodies must declare a Content-Length; responses are always Content-Length-framed).
  • HTTP/1 request pipelining. The dispatcher serializes requests on a connection so that ancillary fds can be unambiguously paired with the request that carried them. See docs/fd-passing.md.
  • TCP / IP transports. Non-AF_UNIX sockets cannot carry SCM_RIGHTS.

Quick start

# Cargo.toml
[dependencies]
hyper-uds = "0.1"
tokio = { version = "1", features = ["rt", "net", "macros"] }
http = "1"
bytes = "1"
use std::os::unix::net::UnixListener as StdUnixListener;

use http::StatusCode;
use hyper_uds::{Builder, Request, Response, service_fn};
use tokio::task::LocalSet;

#[tokio::main(flavor = "current_thread")]
async fn main() -> std::io::Result<()> {
    let _ = std::fs::remove_file("/tmp/hyper-uds.sock");
    let listener = StdUnixListener::bind("/tmp/hyper-uds.sock")?;
    listener.set_nonblocking(true)?;
    let listener = tokio::net::UnixListener::from_std(listener)?;

    let local = LocalSet::new();
    local
        .run_until(async move {
            loop {
                let (stream, _) = listener.accept().await?;
                let std_stream = stream.into_std()?;

                let svc = service_fn(|req: Request| async move {
                    let body = req.into_body();
                    Ok::<_, std::convert::Infallible>(
                        Response::builder()
                            .status(StatusCode::OK)
                            .body(body)
                            .build(),
                    )
                });

                tokio::task::spawn_local(async move {
                    let _ = Builder::new()
                        .keep_alive(true)
                        .serve_connection(std_stream, svc)
                        .await;
                });
            }
            #[allow(unreachable_code)]
            Ok::<_, std::io::Error>(())
        })
        .await
}

Talk to it with curl:

$ curl --unix-socket /tmp/hyper-uds.sock -d "hello" http://localhost/echo
hello

Passing a file descriptor

A client attaches a descriptor as SCM_RIGHTS ancillary data on the same sendmsg that carries the request head, and lists its logical name in the X-FD request header:

POST /open HTTP/1.1
host: localhost
content-length: 0
x-fd: input

(+ ancillary cmsg: SCM_RIGHTS [<fd>])

The handler then sees:

fn handle(mut req: hyper_uds::Request) {
    assert_eq!(req.fd_names(), &["input".to_string()]);
    let fd = req.take_fds().remove(0); // OwnedFd
    // ... use fd ...
}

Responses can attach fds the same way via Response::builder().fd("name", owned_fd).

A working end-to-end example using a hand-rolled recvmsg/sendmsg client lives in tests/e2e.rs.


Documentation

API documentation: cargo doc --open.

Building / testing / benching

# Build
cargo build

# Unit + integration tests (drives a real UDS in tempdir)
cargo test

# Single-connection ping-pong latency benchmark
cargo bench --bench ping_pong
cargo bench --bench ping_pong -- --iters 200000 --warmup 20000

The benchmark reports min / p50 / p90 / p99 / p99.9 / max latencies for 4-byte requests on a single keep-alive connection. See the bench source for what is and isn't included in the measurement.

MSRV

Edition 2024 — currently targets stable Rust ≥ 1.85.

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

No description, website, or topics provided.

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages