# linkerd2-proxy: the Rust data plane behind the Linkerd service mesh

> A sidecar written in Rust that does mutual TLS, latency-aware load balancing and metrics export for every pod in the cluster. The README undersells it, and the build system is the interesting part.

**linkerd/linkerd2-proxy** — A purpose-built proxy for the Linkerd service mesh. Written in Rust.

- Repository: https://github.com/linkerd/linkerd2-proxy
- Website: https://linkerd.io
- Stars: 2,149 · Forks: 303
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/linkerd-linkerd2-proxy

## What the proxy is responsible for

This repository holds the transparent proxy component of Linkerd 2. The README is explicit that the proxy is heavily influenced by the Linkerd 1.x proxy but is an entirely new codebase in Rust. 2,149 stars and 303 forks, Apache 2.0, hosted under the Cloud Native Computing Foundation, with the last push on 2026-09-28 and 21 open issues.

The feature list is the whole product in seven bullets. Transparent proxying for HTTP, HTTP/2 and arbitrary TCP. Prometheus metrics export for both HTTP and TCP traffic, with no configuration. Transparent WebSocket proxying. Latency-aware layer-7 load balancing, and layer-4 balancing for everything that is not HTTP. Automatic mutual TLS. And an on-demand diagnostic tap API.

Two constraints shape everything else. The proxy is primarily intended to run on Linux in containerized environments such as Kubernetes, though the README allows that it may also work on other Unix-like systems such as macOS. And service discovery comes from DNS plus the Linkerd Destination gRPC API, which means the proxy is not standalone: it expects a control plane to tell it what services exist and which endpoints back them.

That second point is worth pausing on, because "zero config" appears twice in the feature list while the proxy still depends on a running control plane. Both facts are true. Zero config means no sidecar-specific configuration file for mTLS or load balancing, not independence from Linkerd.

## Sixty small crates, and why that matters

The workspace member list in `Cargo.toml` is the most informative file in the repository, and it runs to roughly sixty entries. They are not arbitrary. The names map onto distinct responsibilities:

- `linkerd/app/inbound` and `linkerd/app/outbound`, the two traffic directions
- `linkerd/http/retry`, `linkerd/http/route`, `linkerd/http/metrics`, `linkerd/http/classify`, one concern each
- `linkerd/http/body-eos`, the crate that knows when a request body ends
- `linkerd/meshtls` and `linkerd/meshtls/verifier`, mutual TLS and certificate verification
- `linkerd/pool` and `linkerd/pool/p2c`, connection pooling and the power-of-two-choices balancer
- `linkerd/ewma` and `linkerd/load-biaser`, the latency measurement that drives load balancing
- `linkerd/identity`, `linkerd/proxy/identity-client`, `linkerd/proxy/spire-client`, the two paths to workload identity
- `spiffe-proto`, generated protobuf for SPIFFE identity documents
- `hyper-balance`, the HTTP-specific balancing layer

The README calls these crates out as especially important, naming `linkerd2-proxy` for the executable, `linkerd2-app-integration` for the integration tests, and `linkerd2-app` as a bundle that lets the inbound and outbound crates be run either by the executable or by tests. That last arrangement is the useful part: because the inbound and outbound logic live in libraries rather than inside `main`, the integration tests can drive the real request path directly instead of standing up a process and poking at a socket.

The practical upshot for a reader is that you can understand a narrow slice without reading the proxy. Load balancing, retry policy and mutual TLS each have a crate you can open on its own.

## The build is just-cargo, not cargo

The README recommends a dev container to avoid setting up the development environment by hand, which is an admission that the build has real prerequisites. Three commands are listed for normal work:

- `just build`, which compiles the proxy locally using cargo
- `just test`, which runs unit and integration tests locally
- `just docker`, which builds a container image for testing

Those wrap around `just-cargo`, described as a thin wrapper around `just` and `cargo` that lives in Linkerd's own dev repository. So there are three layers: cargo does the compilation, `just` provides the recipes, and `just-cargo` connects them so a single command works the same locally and inside the dev container.

Reading the `justfile` explains why. The profile defaults to `debug` unless the `RELEASE` environment variable is set, on the stated grounds that development builds are faster. The version string is not static: it defaults to `0.0.0-dev` concatenated with the short git hash, and the vendor field defaults to `whoami` at `hostname`. The docker tag is derived from the branch name with slashes replaced by dots, plus the short hash.

Cross-compilation is a first-class case rather than an afterthought. There is an `arch` variable defaulting to `amd64` and an `os` variable defaulting to `linux`, and the two combine to select the cargo target, including a `linux-arm64` arm that maps to `aarch64-unknown-linux-musl`. Two linker flags for `AWS_LC_SYS_CFLAGS` are set conditionally for aarch64 GNU and musl targets, which is the AWS-LC crypto library needing an explicit linker. The os variable's own comment says it should be either `linux` or `windows`, so a third target family is anticipated rather than implemented.

## The Dockerfile admits it is for development only

The Dockerfile carries an unusually candid comment at the top: it is intended for development only, so proxy developers can test the proxy in the context of the larger linkerd2 project. That is worth taking at face value, because the rest of the file is built for iteration speed rather than for producing a shippable image.

The final stage copies the built binary into an image supplied by an argument rather than creating a runtime base. The default is an edge release of the Linkerd proxy image, pinned to a specific version, and the comment explains why: the image carries the proxy identity-initialising and linkerd-await wrappers that the binary needs. The entrypoint is therefore not the Rust binary on its own.

The build stage sets `RUSTFLAGS="-D warnings -A deprecated --cfg tokio_unstable"`, which is a meaningful combination. Warnings are errors, so a new compiler warning breaks the build. Deprecated code is explicitly allowed through, which keeps that rule from turning into churn across dependency updates. And the tokio unstable configuration flag is enabled, meaning the proxy uses Tokio internals that are not part of its stable API.

Two details suggest how seriously the build is maintained. Retry and upstream timeouts are both raised to ten, which is a CI-shaped build talking to the network. And a build-time cache mount is attached to the cargo registry directory in both the fetch and build stages, so dependency downloads survive layer rebuilds. Feature flags are threaded through as arguments: when `pprof` is in the feature list the build switches to a debug profile, and when `meshtls-boring` is present the fetch stage installs a Go toolchain, because the boringcrypto path needs one.

## The release tags do not look like release tags

The three most recent releases are named `release/v2.370.0`, `release/v2.369.0` and `release/v2.368.0`, published on 2026-09-23, 2026-09-16 and 2026-09-02. That is a fortnightly cadence across the three, and the `release/` prefix means a plain `git tag` sort or a semver-aware tool pointed at the repository will not pick these up the way it would for a `v2.370.0` tag.

It also means the project versions on a 2.x line where the minor component moves fast. Going from 2.368.0 to 2.370.0 is two weeks of work, so the third digit is a serial rather than a compatibility marker. Do not read anything into 2.370 as a feature tier.

The release bodies read like a dependency-bump log, and that is informative about where maintenance effort goes. The 2.369.0 notes are entirely dependabot activity: the unicode-org group bumped across one directory with eight updates, and zerocopy from 0.8.52 to 0.8.56. The 2.368.0 notes are more substantive and all concern HTTP correctness: emitting only the exact size of unary bodies without trailers, allowing an undefined RPC match in a MatchRoute definition, and adjusting route matching. The 2.370.0 note is a retry fix that avoids replaying unread body data.

That last one is the kind of bug that makes the retry feature worth thinking about before you enable it. Replaying a body the proxy has not finished reading is precisely the failure mode that turns a retry policy into a data corruption policy. Two of the three most recent releases touching body handling and trailers suggests this area has needed attention recently.

## Security posture, and a documentation gap

The README points at `FUZZING.md` in the docs directory and states plainly that the code is tested by way of fuzzing. It also records a third-party audit focused on fuzzing, performed by Ada Logics in 2021, with the full report kept in `docs/reports/`.

That audit is five years old relative to the current release line. It is not a criticism, since audits are snapshots and a project only needs the audit once to have been useful. But it does mean the audit tells you nothing about the code as it exists in the 2.368 to 2.370 range, and the three most recent releases are HTTP body, trailer, route matching and retry changes, which is a class of bug that fuzzing targets rather than unit tests.

There are two files in the tree worth knowing about that the README never mentions. `deny.toml` is cargo-deny configuration, which is how the project enforces dependency licences and checks advisories as part of the build rather than as a periodic audit. And `.checksec/` holds tooling for inspecting the built binary's hardening properties, which is a practice more associated with C projects and notable for appearing in a Rust workspace.

There is also SPIFFE support that the README does not describe. The tree contains a `spiffe-proto` directory for generated identity protobuf, and the workspace has both `linkerd/identity` and `linkerd/proxy/spire-client`, so there appear to be two paths to workload identity rather than one. If SPIFFE matters to your deployment, that is documented in the source and in the Linkerd docs rather than in this README.

## Where to start reading

For understanding the design, start with the workspace member list in `Cargo.toml`. Sixty named crates tell you the decomposition better than any prose could, and the names are descriptive enough that you can guess each one's role from the name alone. Then read `linkerd/app/core` for the shared request path and `linkerd/app/inbound` and `linkerd/app/outbound` for the two directions.

For understanding behaviour, `linkerd/http/classify` is where a response becomes a retryable or non-retryable outcome, and `linkerd/http/body-eos` is where the proxy decides a body is complete. Those two crates together explain most of the behaviour described in the recent release notes.

For building it, do not start with cargo. Install `just-cargo` from Linkerd's dev repository or use the dev container, then run the three documented recipes. The `justfile` is worth reading first because it encodes defaults that are otherwise invisible, including the debug-by-default profile and the git-hash-derived version string.

The diagnostic tap API deserves a mention for anyone running this in anger. It is an on-demand interface for inspecting live traffic through the proxy, which is the kind of feature that saves an afternoon when a mesh is misbehaving and no amount of Prometheus scraping explains why a specific request failed.

## Conclusion

linkerd2-proxy is the component that makes Linkerd a mesh rather than a library. Everything a service mesh promises at the marketing level, automatic mutual TLS, latency-aware balancing, per-request metrics, retries that do not corrupt request bodies, happens in this process. The workspace structure explains a lot about how it is built: roughly sixty small crates, each independently testable, with HTTP concerns split by single responsibility rather than lumped into one proxy crate. What to know before reading the source is that the build is not a plain cargo build. It runs through just-cargo, cross-compiles for a musl target, and depends on a Linkerd base image that already carries the identity and await wrappers. Start with the justfile and the app-inbound and app-outbound crates rather than with the proxy binary.

## FAQ

### What does Linkerd do?

Linkerd is a service mesh. It adds mutual TLS, load balancing, retries, metrics and traffic policy to services in a cluster without requiring application code changes, by injecting a proxy alongside each workload. The linkerd2-proxy repository is the Rust data plane that does that work, one proxy per pod, handling inbound and outbound traffic on the pod's behalf.

### Is Linkerd open source?

Yes. Linkerd is hosted by the Cloud Native Computing Foundation, and linkerd2-proxy is licensed under Apache 2.0 with copyright from 2018. The proxy is the component most likely to be read by engineers, since it is the part that actually handles traffic, and its source is a public Rust workspace.

### What is the difference between linkerd2-proxy and the linkerd2 repository?

The linkerd2 repository holds the control plane: the CLI, the controllers and the Kubernetes manifests that inject the proxy. linkerd2-proxy is the per-pod data plane written in Rust, which performs mutual TLS, load balancing, retries and metrics export. This repository builds one binary that the control plane installs into every meshed pod.

### Do I need a Linkerd control plane to run linkerd2-proxy?

Yes, in practice. The proxy discovers services through DNS and the Linkerd Destination gRPC API, so it expects a control plane to tell it which services exist and which endpoints back them. The zero configuration claim in the README refers to not needing per-proxy configuration files for mTLS or load balancing, not to running without the mesh around it.

### How is the linkerd2-proxy build set up?

It uses just-cargo, a wrapper around just and cargo maintained in Linkerd's dev repository, so the same commands work locally and in the dev container. The three documented recipes are just build, just test and just docker. The justfile defaults to a debug profile unless RELEASE is set, derives the version from the short git hash, and selects cross-compilation targets from arch and os variables.

### What is the tap API for?

It is an on-demand diagnostic interface for inspecting live traffic passing through the proxy. It exists because mesh-level Prometheus metrics tell you what happened in aggregate but not why one specific request failed, and tracing every request through a mesh permanently would be expensive.

## Sources

- [License: Apache-2.0](https://github.com/linkerd/linkerd2-proxy/blob/main/LICENSE)
- [linkerd/linkerd2-proxy on GitHub](https://github.com/linkerd/linkerd2-proxy)
- [Project website](https://linkerd.io)
- [README](https://github.com/linkerd/linkerd2-proxy/blob/main/README.md)
- [Releases](https://github.com/linkerd/linkerd2-proxy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/linkerd-linkerd2-proxy
