Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions .github/workflows/build-mitmproxy-linux.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# SPDX-FileCopyrightText: 2026 The RISE Project
# SPDX-License-Identifier: MIT
---
# This workflow is based on the `build-linux-wheel` job of
# https://github.com/mitmproxy/mitmproxy_rs/blob/v0.12.11/.github/workflows/ci.yml
name: Build mitmproxy-linux wheels (riscv64)

on:
workflow_dispatch:
inputs:
version:
description: 'Version glob to (re)build; empty builds every version of docs/packages/mitmproxy-linux.yaml not released yet'
required: false
default: ''
pull_request:
branches: [main]
paths:
- '.github/workflows/build-mitmproxy-linux.yml'
- 'docs/packages/mitmproxy-linux.yaml'
push:
branches: [main]
paths:
- '.github/workflows/build-mitmproxy-linux.yml'
- 'docs/packages/mitmproxy-linux.yaml'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # to fetch code (actions/checkout)

jobs:
setup:
uses: $/.github/workflows/_setup.yml
with:
package: mitmproxy-linux
version: ${{ inputs.version }}

build_ebpf:
needs: [setup]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# bpf-linker calls LLVM through the shared library the Rust toolchain ships,
# which riscv64 toolchains do not carry, and the newest LLVM packaged for
# riscv64 is older than this release's MSRV emits bitcode for. The object is
# architecture-independent BPF bytecode apart from the bpf_target_arch cfg,
# so it is cross-compiled here and embedded by patch 0001.
name: Cross-compile mitmproxy-linux ${{ matrix.version }} eBPF object
runs-on: ubuntu-latest
timeout-minutes: 60

env:
MITMPROXY_LINUX_VERSION: ${{ matrix.version }}

steps:
- name: Checkout mitmproxy_rs v${{ env.MITMPROXY_LINUX_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: mitmproxy/mitmproxy_rs
ref: v${{ env.MITMPROXY_LINUX_VERSION }}
persist-credentials: false

- name: Install the nightly toolchain and bpf-linker
run: |
rustup toolchain install nightly --profile minimal --component rust-src
cargo install --locked bpf-linker@0.9.15

- name: Build the eBPF object
# The flags aya_build::build_ebpf passes, with bpf_target_arch set for
# the wheel's architecture rather than this runner's.
run: |
env -u RUSTC -u RUSTC_WORKSPACE_WRAPPER \
CARGO_ENCODED_RUSTFLAGS=$'--cfg=bpf_target_arch="riscv64"\x1f-Cdebuginfo=2\x1f-Clink-arg=--btf' \
rustup run nightly cargo build --package mitmproxy-linux-ebpf --bins \
--release --target bpfel-unknown-none -Z build-std=core

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mitmproxy-linux-${{ env.MITMPROXY_LINUX_VERSION }}-ebpf-object
path: target/bpfel-unknown-none/release/mitmproxy-linux
if-no-files-found: error

build_wheel:
needs: [setup, build_ebpf]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
name: Build mitmproxy-linux ${{ matrix.version }} manylinux_riscv64
runs-on: ubuntu-24.04-riscv
timeout-minutes: 1440

env:
MITMPROXY_LINUX_VERSION: ${{ matrix.version }}

steps:
- name: Checkout mitmproxy_rs v${{ env.MITMPROXY_LINUX_VERSION }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: mitmproxy/mitmproxy_rs
ref: v${{ env.MITMPROXY_LINUX_VERSION }}
persist-credentials: false

- name: Checkout python-wheels
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: python-wheels
persist-credentials: false

- name: Patch mitmproxy_rs source
run: git apply python-wheels/patches/mitmproxy-linux/${{ env.MITMPROXY_LINUX_VERSION }}/00*.patch

- name: Download the eBPF object
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: mitmproxy-linux-${{ env.MITMPROXY_LINUX_VERSION }}-ebpf-object
path: ebpf-prebuilt

- name: Stage the licence beside mitmproxy-linux's pyproject.toml
# maturin globs LICEN[CS]E* relative to the pyproject directory, which
# in this monorepo is mitmproxy-linux/ -- so upstream's own aarch64
# wheel ships no licence text at all.
run: cp LICENSE mitmproxy-linux/

# `[tool.maturin] bindings = "bin"`: the wheel is one compiled executable
# with no ABI tag, so a single native build covers every interpreter.
- name: Build wheel
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
command: build
target: riscv64gc-unknown-linux-gnu
args: --release --locked --out dist --manifest-path mitmproxy-linux/Cargo.toml
manylinux: '2_39'
before-script-linux: |
git config --global --add safe.directory "*"

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: mitmproxy-linux-${{ env.MITMPROXY_LINUX_VERSION }}-manylinux_riscv64
path: dist/*.whl
if-no-files-found: error

test_wheel:
name: Test mitmproxy-linux ${{ matrix.version }} on Python ${{ matrix.python-version }}
needs: [setup, build_wheel]
if: needs.setup.outputs.versions != '[]'
runs-on: ubuntu-24.04-riscv
timeout-minutes: 30
env:
MITMPROXY_LINUX_VERSION: ${{ matrix.version }}
# Without this uv would reuse the runner image's system CPython for 3.12
# and download a standalone build for the others.
UV_PYTHON_PREFERENCE: only-managed
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
# The wheel is interpreter-agnostic (bindings = "bin"), so every
# interpreter exercises the same binary.
python-version: ['3.12', '3.13', '3.14', '3.14t']

steps:
- name: Download wheel
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: mitmproxy-linux-${{ env.MITMPROXY_LINUX_VERSION }}-manylinux_riscv64

- name: Install Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
python-version: ${{ matrix.python-version }}
activate-environment: true
enable-cache: false

- name: Install wheel
run: uv pip install --reinstall --no-index --find-links . mitmproxy_linux

# The redirector needs root, a tun device and cgroup eBPF attach, so it
# cannot be driven end to end here (upstream gates that behind its own
# `root-tests` feature). Assert instead that the eBPF object its build.rs
# cross-compiles to bpfel-unknown-none really is embedded in the riscv64
# executable -- loading it is all the binary does.
- name: Test wheel
run: |
set -euo pipefail
cat > verify.py <<'EOF'
import struct
import subprocess
import sys

from mitmproxy_linux import executable_path

exe = executable_path()
assert exe.is_file(), exe
buf = exe.read_bytes()
assert buf[:6] == b"\x7fELF\x02\x01", buf[:6]
(machine,) = struct.unpack_from("<H", buf, 18)
assert machine == 243, f"redirector is not riscv64: e_machine={machine}"

pos = 0
while True:
pos = buf.find(b"\x7fELF\x02\x01\x01", pos + 1)
assert pos > 0, "no embedded eBPF object in the redirector"
if struct.unpack_from("<H", buf, pos + 18)[0] == 247:
break

(shoff,) = struct.unpack_from("<Q", buf, pos + 0x28)
shentsize, shnum, shstrndx = struct.unpack_from("<HHH", buf, pos + 0x3A)
base = pos + shoff
(strtab,) = struct.unpack_from("<Q", buf, base + shstrndx * shentsize + 0x18)
names = []
for i in range(shnum):
(name,) = struct.unpack_from("<I", buf, base + i * shentsize)
start = pos + strtab + name
names.append(buf[start:buf.index(b"\0", start)].decode())
print("eBPF sections:", [n for n in names if n])
assert "cgroup/sock_create" in names, names
assert b"INTERCEPT_CONF" in buf[pos:], "INTERCEPT_CONF map missing"

run = subprocess.run([exe], capture_output=True, text=True, timeout=60)
print(run.stderr, file=sys.stderr)
assert run.returncode != 0, run
assert "usage:" in run.stderr, run.stderr
EOF
python verify.py

publish:
name: Publish mitmproxy-linux ${{ matrix.version }}
needs: [setup, build_wheel, test_wheel]
if: needs.setup.outputs.versions != '[]'
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.setup.outputs.versions) }}
permissions:
contents: write
pull-requests: write
uses: $/.github/workflows/_publish-wheel.yml
secrets:
app-private-key: ${{ secrets.RISEPROJECT_APP_PRIVATE_KEY }}
with:
artifact-pattern: mitmproxy-linux-${{ matrix.version }}-manylinux_riscv64
6 changes: 6 additions & 0 deletions docs/packages/mitmproxy-linux.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package-name: mitmproxy-linux
source-code: https://github.com/mitmproxy/mitmproxy_rs
license: MIT
versions:
- version: 0.12.11
patched: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
From 76383fbf9a42114eb7170532f9608faff8de369e Mon Sep 17 00:00:00 2001
From: Ludovic Henry <git@ludovic.dev>
Date: Sun, 20 Sep 2026 00:22:58 +0000
Subject: [PATCH] mitmproxy-linux: allow a prebuilt eBPF object

`aya_build::build_ebpf` cross-compiles mitmproxy-linux-ebpf to
bpfel-unknown-none through `bpf-linker`, which links against the LLVM that
ships with the Rust toolchain. Rust builds that LLVM as a shared library for
x86_64 and aarch64 only: on riscv64 hosts `librustc_driver-*.so` has LLVM
linked in statically with its symbols hidden and the toolchain carries no
`libLLVM*.so`, so bpf-linker aborts in aya-rustc-llvm-proxy with "unable to
find LLVM shared lib". Building bpf-linker against a system LLVM instead is no
way out either: the newest LLVM packaged for riscv64 is 21.1.8 (Rocky 10.2
AppStream, the manylinux_2_39_riscv64 base), while this workspace's own MSRV of
1.95 is the Rust release that moved to LLVM 22, and linking LLVM 23 bitcode
with LLVM 21 fails with "ERROR llvm: Invalid record".

Take the eBPF object from `ebpf-prebuilt/` at the workspace root when one is
staged there, so the BPF half can be cross-compiled on a host with a usable
bpf-linker while the redirector itself is built natively. The object is plain
BPF bytecode whose only architecture input is `--cfg bpf_target_arch`, which
the ahead-of-time build sets to the target architecture. Without a staged
object both build scripts behave exactly as before.

Upstream-Status: Inappropriate [riscv64 build-environment constraint; upstream's own CI builds on hosts whose Rust toolchain ships a shared libLLVM]
---
mitmproxy-linux-ebpf/build.rs | 15 ++++++++++-----
mitmproxy-linux/build.rs | 27 +++++++++++++++++++++++++++
2 files changed, 37 insertions(+), 5 deletions(-)

diff --git a/mitmproxy-linux-ebpf/build.rs b/mitmproxy-linux-ebpf/build.rs
index 10ad0c2..204a2a3 100644
--- a/mitmproxy-linux-ebpf/build.rs
+++ b/mitmproxy-linux-ebpf/build.rs
@@ -12,9 +12,14 @@ use which::which;
///
/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies
fn main() {
- let bpf_linker = which("bpf-linker").expect(
- "Failed to find `bpf-linker` executable on PATH. \
- Run `cargo install --locked bpf-linker` to install.",
- );
- println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap());
+ // A prebuilt eBPF object staged in `ebpf-prebuilt/` at the workspace root
+ // makes `../mitmproxy-linux/build.rs` skip the cross-compile, so a missing
+ // `bpf-linker` is not an error in that case.
+ match which("bpf-linker") {
+ Ok(bpf_linker) => println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()),
+ Err(err) => println!(
+ "cargo:warning=`bpf-linker` not found on PATH ({err}); \
+ expecting a prebuilt eBPF object"
+ ),
+ }
}
diff --git a/mitmproxy-linux/build.rs b/mitmproxy-linux/build.rs
index 693f4f0..e286729 100644
--- a/mitmproxy-linux/build.rs
+++ b/mitmproxy-linux/build.rs
@@ -1,3 +1,6 @@
+#[cfg(target_os = "linux")]
+use std::{env, fs, path::PathBuf};
+
#[cfg(target_os = "linux")]
use anyhow::{Context as _, anyhow};

@@ -10,6 +13,30 @@ fn main() {}
/// Based on https://github.com/aya-rs/aya-template/blob/main/%7B%7Bproject-name%7D%7D/build.rs
#[cfg(target_os = "linux")]
fn main() -> anyhow::Result<()> {
+ // Use an eBPF object cross-compiled ahead of time, if one is staged in
+ // `ebpf-prebuilt/` at the workspace root. `aya_build::build_ebpf` below
+ // needs `bpf-linker`, which links against the LLVM that ships with the Rust
+ // toolchain and therefore cannot run on a host whose toolchain has no
+ // shared LLVM library.
+ let manifest_dir = PathBuf::from(
+ env::var_os("CARGO_MANIFEST_DIR").ok_or_else(|| anyhow!("CARGO_MANIFEST_DIR not set"))?,
+ );
+ let prebuilt = manifest_dir
+ .parent()
+ .ok_or_else(|| anyhow!("no parent for {}", manifest_dir.display()))?
+ .join("ebpf-prebuilt")
+ .join("mitmproxy-linux");
+ println!("cargo:rerun-if-changed={}", prebuilt.display());
+ if prebuilt.is_file() {
+ let out_dir =
+ PathBuf::from(env::var_os("OUT_DIR").ok_or_else(|| anyhow!("OUT_DIR not set"))?);
+ let dst = out_dir.join("mitmproxy-linux");
+ let _: u64 = fs::copy(&prebuilt, &dst).with_context(|| {
+ format!("failed to copy {} to {}", prebuilt.display(), dst.display())
+ })?;
+ return Ok(());
+ }
+
let cargo_metadata::Metadata { packages, .. } = cargo_metadata::MetadataCommand::new()
.no_deps()
.exec()
Loading