├── .cargo └── config.toml ├── .github ├── FUNDING.yml ├── actions │ └── setup │ │ └── action.yml ├── dependabot.yml ├── python-version.txt ├── scripts │ ├── build-macos-redirector.sh │ ├── pin-versions.py │ └── release └── workflows │ ├── autofix.yml │ ├── ci.yml │ ├── docs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── architecture.png ├── benches ├── .gitignore ├── dns.py ├── local.conf ├── nonlocal.conf ├── openvpnserv.exe ├── plot.py ├── process.rs ├── py_echo_client.py ├── py_echo_server.py ├── wg_echo_client.py └── wg_echo_server.py ├── mitmproxy-contentviews ├── Cargo.toml ├── README.md ├── benches │ └── contentviews.rs ├── src │ ├── hex_dump.rs │ ├── hex_stream.rs │ ├── lib.rs │ ├── msgpack.rs │ ├── protobuf │ │ ├── existing_proto_definitions.rs │ │ ├── mod.rs │ │ ├── proto_to_yaml.rs │ │ ├── raw_to_proto.rs │ │ ├── reencode.rs │ │ ├── view_grpc.rs │ │ ├── view_protobuf.rs │ │ └── yaml_to_pretty.rs │ └── test_inspect_metadata.rs └── testdata │ └── protobuf │ ├── mismatch.proto │ ├── nested.proto │ ├── simple.proto │ ├── simple_package.proto │ └── simple_service.proto ├── mitmproxy-highlight ├── Cargo.toml ├── README.md ├── benches │ └── syntax_highlight.rs └── src │ ├── common.rs │ ├── css.rs │ ├── javascript.rs │ ├── lib.rs │ ├── xml.rs │ └── yaml.rs ├── mitmproxy-linux-ebpf-common ├── Cargo.toml └── src │ └── lib.rs ├── mitmproxy-linux-ebpf ├── .cargo │ └── config.toml ├── Cargo.toml ├── build.rs ├── rust-toolchain.toml └── src │ ├── lib.rs │ └── main.rs ├── mitmproxy-linux ├── Cargo.toml ├── README.md ├── build.rs ├── mitmproxy_linux │ └── __init__.py ├── pyproject.toml └── src │ ├── main.rs │ └── main2.rs ├── mitmproxy-macos ├── .swift-format ├── .swiftlint.yml ├── README.md ├── certificate-truster │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── mitmproxy_macos │ ├── __init__.py │ └── macos-certificate-truster.app │ │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ └── mitmproxy.icns ├── pyproject.toml ├── redirector │ ├── .gitignore │ ├── ExportOptions.plist │ ├── README.md │ ├── ipc │ │ ├── mitmproxy_ipc.pb.swift │ │ └── utils.swift │ ├── macos-redirector.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── macos-redirector.xcscheme │ ├── macos-redirector │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── logo-circle-1024.png │ │ │ │ ├── logo-circle-128.png │ │ │ │ ├── logo-circle-16.png │ │ │ │ ├── logo-circle-256.png │ │ │ │ ├── logo-circle-32.png │ │ │ │ ├── logo-circle-512.png │ │ │ │ └── logo-circle-64.png │ │ │ └── Contents.json │ │ ├── app.swift │ │ └── macos_redirector.entitlements │ └── network-extension │ │ ├── FlowExtensions.swift │ │ ├── Info.plist │ │ ├── InterceptConf.swift │ │ ├── ProcessInfoCache.swift │ │ ├── TransparentProxyProvider.swift │ │ ├── libproc-Bridging-Header.h │ │ ├── main.swift │ │ └── network_extension.entitlements └── version-info.toml ├── mitmproxy-rs ├── .gitignore ├── Cargo.toml ├── README.md ├── mitmproxy_rs │ ├── __init__.py │ ├── __init__.pyi │ ├── _pyinstaller │ │ ├── __init__.py │ │ ├── hook-mitmproxy_linux.py │ │ ├── hook-mitmproxy_macos.py │ │ ├── hook-mitmproxy_rs.py │ │ └── hook-mitmproxy_windows.py │ ├── certs.pyi │ ├── contentviews.pyi │ ├── dns.pyi │ ├── local.pyi │ ├── process_info.pyi │ ├── py.typed │ ├── syntax_highlight.pyi │ ├── tun.pyi │ ├── udp.pyi │ └── wireguard.pyi ├── pyproject.toml ├── pytests │ ├── logger.rs │ └── test_task.rs ├── src │ ├── contentviews.rs │ ├── dns_resolver.rs │ ├── lib.rs │ ├── process_info.rs │ ├── server │ │ ├── base.rs │ │ ├── local_redirector.rs │ │ ├── mod.rs │ │ ├── tun.rs │ │ ├── udp.rs │ │ └── wireguard.rs │ ├── stream.rs │ ├── syntax_highlight.rs │ ├── task.rs │ ├── udp_client.rs │ └── util.rs └── stubtest-allowlist.txt ├── mitmproxy-windows ├── README.md ├── mitmproxy_windows │ ├── LICENSE │ ├── WINDIVERT_VERSION │ ├── WinDivert.dll │ ├── WinDivert.lib │ ├── WinDivert64.sys │ └── __init__.py ├── pyproject.toml └── redirector │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── mitmproxy.ico │ └── src │ ├── main.rs │ └── main2.rs ├── src ├── bin │ └── process-list.rs ├── certificates │ ├── macos.rs │ └── mod.rs ├── dns.rs ├── intercept_conf.rs ├── ipc │ ├── mitmproxy_ipc.proto │ ├── mitmproxy_ipc.rs │ └── mod.rs ├── lib.rs ├── messages.rs ├── network │ ├── core.rs │ ├── icmp.rs │ ├── mod.rs │ ├── task.rs │ ├── tcp.rs │ ├── tests.rs │ ├── udp.rs │ └── virtual_device.rs ├── packet_sources │ ├── linux.rs │ ├── macos.rs │ ├── mod.rs │ ├── tun.rs │ ├── udp.rs │ ├── windows.rs │ └── wireguard.rs ├── processes │ ├── macos_icons.rs │ ├── mod.rs │ ├── nix_list.rs │ ├── windows_icons.rs │ └── windows_list.rs ├── shutdown.rs └── windows │ ├── mod.rs │ └── network.rs └── wireguard-test-client ├── Cargo.toml ├── src └── main.rs └── wireguard_echo_test_server.py /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | WINDIVERT_PATH = { value = "mitmproxy-windows/mitmproxy_windows/", relative = true } 3 | 4 | [target.x86_64-apple-darwin] 5 | rustflags = [ 6 | "-C", "link-arg=-undefined", 7 | "-C", "link-arg=dynamic_lookup", 8 | ] 9 | 10 | [target.aarch64-apple-darwin] 11 | rustflags = [ 12 | "-C", "link-arg=-undefined", 13 | "-C", "link-arg=dynamic_lookup", 14 | ] 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [decathorpe, mhils] 2 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: 'setup' 2 | description: 'checkout, setup rust and python' 3 | inputs: 4 | rust-version: 5 | description: 'Rust version' 6 | required: true 7 | default: 'stable' 8 | extra-targets: 9 | description: 'Extra Rust targets' 10 | toolchain-args: 11 | description: 'Extra args for `rustup toolchain`' 12 | runs: 13 | using: "composite" 14 | steps: 15 | - uses: mhils/workflows/setup-python@3c9fc8f5b40cb8f3f1b81163f3f195cad843b09a # v18.0 # PyO3 wants recent Python on Windows. 16 | - run: rustup toolchain install ${{ inputs.rust-version }} --profile minimal ${{ inputs.toolchain-args }} 17 | shell: bash 18 | - if: runner.os == 'Linux' 19 | run: rustup toolchain install nightly --component rust-src 20 | shell: bash 21 | - run: rustup default ${{ inputs.rust-version }} 22 | shell: bash 23 | - if: inputs.extra-targets 24 | run: rustup target add ${{ inputs.extra-targets }} 25 | shell: bash 26 | - uses: mhils/workflows/rust-cache@5b6540d578f48644ffa5e955cedadc81034cb7d8 # v19.0 27 | with: 28 | key: ${{ inputs.rust-version }} # proxy value for glibc version 29 | - if: runner.os == 'Linux' 30 | run: cargo install --locked bpf-linker 31 | shell: bash 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: cargo 12 | directory: "/" 13 | schedule: 14 | interval: "monthly" 15 | groups: 16 | # If a dependency matches more than one rule, it's included in the first group that it matches. 17 | # Any outdated dependencies that do not match a rule are updated in individual pull requests. 18 | windows: 19 | patterns: 20 | - "windows" 21 | dependencies: 22 | patterns: 23 | - "*" 24 | -------------------------------------------------------------------------------- /.github/python-version.txt: -------------------------------------------------------------------------------- 1 | 3.12 2 | -------------------------------------------------------------------------------- /.github/scripts/build-macos-redirector.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eou pipefail 4 | 5 | if [ -n "${APPLE_ID-}" ]; then 6 | echo "Signing keys available, building signed binary..." 7 | 8 | APPLE_TEAM_ID=S8XHQB96PW 9 | 10 | # Install provisioning profiles 11 | mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles 12 | echo -n "$APPLE_PROVISIONING_PROFILE_APP" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/app.provisionprofile 13 | echo -n "$APPLE_PROVISIONING_PROFILE_EXT" | base64 --decode -o ~/Library/MobileDevice/Provisioning\ Profiles/ext.provisionprofile 14 | 15 | # Create temporary keychain 16 | KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain 17 | security create-keychain -p "app-signing" $KEYCHAIN_PATH 18 | security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 19 | security unlock-keychain -p "app-signing" $KEYCHAIN_PATH 20 | # Import certificate to keychain 21 | security import <(echo -n "$APPLE_CERTIFICATE") -A -k $KEYCHAIN_PATH 22 | security list-keychain -s $KEYCHAIN_PATH 23 | 24 | mkdir build 25 | xcodebuild \ 26 | -scheme macos-redirector \ 27 | -archivePath build/macos-redirector.xcarchive \ 28 | OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_PATH" \ 29 | archive 30 | xcodebuild \ 31 | -exportArchive \ 32 | -archivePath build/macos-redirector.xcarchive \ 33 | -exportOptionsPlist ./ExportOptions.plist \ 34 | -exportPath ./build 35 | 36 | # Notarize 37 | # https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow 38 | xcrun notarytool store-credentials "AC_PASSWORD" \ 39 | --keychain "$KEYCHAIN_PATH" \ 40 | --apple-id "$APPLE_ID" \ 41 | --team-id "$APPLE_TEAM_ID" \ 42 | --password "$APPLE_APP_PASSWORD" 43 | ditto -c -k --keepParent "./build/Mitmproxy Redirector.app" "./build/Mitmproxy Redirector.zip" 44 | xcrun notarytool submit \ 45 | "./build/Mitmproxy Redirector.zip" \ 46 | --keychain "$KEYCHAIN_PATH" \ 47 | --keychain-profile "AC_PASSWORD" \ 48 | --wait 49 | xcrun stapler staple "./build/Mitmproxy Redirector.app" 50 | 51 | mkdir -p dist 52 | tar --create --file "./dist/Mitmproxy Redirector.app.tar" --cd "./build" "Mitmproxy Redirector.app" 53 | else 54 | echo "Signing keys not available, building unsigned binary..." 55 | xcodebuild -scheme macos-redirector CODE_SIGNING_ALLOWED="NO" build 56 | mkdir -p dist 57 | touch "dist/Mitmproxy Redirector.app.tar" 58 | fi 59 | -------------------------------------------------------------------------------- /.github/scripts/pin-versions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import tomllib 4 | from pathlib import Path 5 | 6 | with open("Cargo.toml", "rb") as f: 7 | version = tomllib.load(f)["workspace"]["package"]["version"] 8 | 9 | pyproject_toml = Path("mitmproxy-rs/pyproject.toml") 10 | contents = pyproject_toml.read_text() 11 | contents = ( 12 | contents 13 | .replace(f"mitmproxy_windows", f"mitmproxy_windows=={version}") 14 | .replace(f"mitmproxy_linux", f"mitmproxy_linux=={version}") 15 | .replace(f"mitmproxy_macos", f"mitmproxy_macos=={version}") 16 | ) 17 | pyproject_toml.write_text(contents) 18 | -------------------------------------------------------------------------------- /.github/scripts/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -u 2 | import logging 3 | 4 | from releasetool import branch 5 | from releasetool import get_next_dev_version 6 | from releasetool import git_commit 7 | from releasetool import git_push 8 | from releasetool import git_tag 9 | from releasetool import status_check 10 | from releasetool import update_changelog 11 | from releasetool import update_rust_version 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | if __name__ == "__main__": 16 | status_check() 17 | update_changelog() 18 | update_rust_version() 19 | git_commit() 20 | git_tag() 21 | 22 | if branch == "main": 23 | update_rust_version(version=get_next_dev_version()) 24 | git_commit(message="reopen main for development") 25 | 26 | git_push() 27 | logger.info("✅ All done. 🥳") 28 | -------------------------------------------------------------------------------- /.github/workflows/autofix.yml: -------------------------------------------------------------------------------- 1 | name: autofix.ci 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | MSRV: "1.85" # Minimum Supported Rust Version 15 | 16 | jobs: 17 | protobuf: 18 | runs-on: macos-latest 19 | steps: 20 | - uses: mhils/workflows/checkout@5b6540d578f48644ffa5e955cedadc81034cb7d8 # v18.0 21 | - run: brew install swift-protobuf 22 | - run: cargo install protoc-gen-prost 23 | - run: protoc --proto_path=./src/ipc/ mitmproxy_ipc.proto 24 | --prost_out=./src/ipc/ 25 | --prost_opt="bytes=data" 26 | --swift_out=./mitmproxy-macos/redirector/ipc 27 | - run: cargo fmt --all 28 | - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef 29 | 30 | rustfmt: 31 | runs-on: ${{ matrix.os }} 32 | strategy: 33 | matrix: 34 | include: 35 | - os: windows-latest 36 | - os: macos-latest 37 | - os: ubuntu-latest 38 | steps: 39 | - uses: mhils/workflows/checkout@5b6540d578f48644ffa5e955cedadc81034cb7d8 # v18.0 40 | - uses: ./.github/actions/setup 41 | with: 42 | rust-version: ${{ env.MSRV }} 43 | toolchain-args: --component rustfmt --component clippy 44 | 45 | # We could run clippy on mitmproxy-linux-ebpf with 46 | # cargo +nightly clippy --workspace -- -C panic=abort -Zpanic_abort_tests 47 | # but that means we'd use nightly clippy, which may change its behavior (and thus break CI). 48 | # So we rather exempt mitmproxy-linux-ebpf from clippy lints. 49 | - run: cargo clippy --fix --allow-dirty --workspace --exclude mitmproxy-linux-ebpf 50 | - run: cargo fmt --all 51 | - run: git checkout src/ipc/mitmproxy_ipc.rs 52 | 53 | - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef 54 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | # security: restrict permissions for CI jobs. 10 | permissions: 11 | contents: read 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | # Build the documentation and upload the static HTML files as an artifact. 19 | build: 20 | runs-on: ubuntu-latest 21 | steps: 22 | # TODO: This should ideally just reuse the main CI artifacts. 23 | - uses: mhils/workflows/checkout@5b6540d578f48644ffa5e955cedadc81034cb7d8 # v18.0 24 | - uses: ./.github/actions/setup 25 | - uses: install-pinned/maturin-with-zig@42dc7eb111721cfc7d889e6588c18f72f6ea3dc0 26 | - uses: install-pinned/mypy@75779f141592e4909d64e13f8a1861f06aa9cd8d 27 | - uses: install-pinned/pdoc@69ba59f9699df21e1026110af4ec6b10a98cf5cd 28 | 29 | - run: maturin build --features docs,pyo3/extension-module 30 | working-directory: ./mitmproxy-rs 31 | - run: pip install --no-index --no-dependencies --find-links target/wheels/ mitmproxy_rs 32 | 33 | - run: stubtest --allowlist mitmproxy-rs/stubtest-allowlist.txt --mypy-config-file mitmproxy-rs/pyproject.toml mitmproxy_rs 34 | 35 | - run: pdoc -o docs/ mitmproxy_rs 36 | 37 | - uses: actions/upload-pages-artifact@v3 38 | with: 39 | path: docs/ 40 | 41 | # Deploy the artifact to GitHub pages. 42 | # This is a separate job so that only actions/deploy-pages has the necessary permissions. 43 | deploy: 44 | if: github.ref == 'refs/heads/main' 45 | needs: build 46 | runs-on: ubuntu-latest 47 | permissions: 48 | pages: write 49 | id-token: write 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | steps: 54 | - id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version (major.minor.patch)' 8 | required: true 9 | type: string 10 | skip-branch-status-check: 11 | description: 'Skip CI status check.' 12 | default: false 13 | required: false 14 | type: boolean 15 | 16 | permissions: {} 17 | 18 | jobs: 19 | release: 20 | environment: deploy 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | token: ${{ secrets.GH_PUSH_TOKEN }} # this token works to push to the protected main branch. 26 | persist-credentials: true # needed by release tool 27 | - uses: mhils/releasetool@v1 28 | - run: ./.github/scripts/release 29 | env: 30 | PROJECT_VERSION: ${{ inputs.version }} 31 | STATUS_CHECK_SKIP_GIT: ${{ inputs.skip-branch-status-check }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /dist/ 3 | /out/ 4 | /target/ 5 | /venv/ 6 | /docs/ 7 | __pycache__/ 8 | *.xcuserstate 9 | editable.marker 10 | *.DS_Store 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Development Setup 4 | 5 | To get started hacking on mitmproxy_rs, please [install mitmproxy as described 6 | in the main mitmproxy repository](https://github.com/mitmproxy/mitmproxy/blob/main/CONTRIBUTING.md#development-setup) 7 | and [install the latest Rust release](https://www.rust-lang.org/tools/install). Make sure that you have mitmproxy's 8 | virtualenv activated and then run the following: 9 | 10 | ```shell 11 | git clone https://github.com/mitmproxy/mitmproxy_rs.git 12 | cd mitmproxy_rs/mitmproxy-rs 13 | maturin develop 14 | ``` 15 | 16 | mitmproxy now uses your locally-compiled version of `mitmproxy_rs`. **After applying any changes to the Rust code, 17 | re-run `maturin develop` and restart mitmproxy** for changes to apply. 18 | 19 | 20 | ## Testing 21 | 22 | If you've followed the procedure above, you can run the basic test suite as follows: 23 | 24 | ```shell 25 | cargo test 26 | ``` 27 | 28 | Please ensure that all patches are accompanied by matching changes in the test suite. 29 | 30 | 31 | ## Code Style 32 | 33 | The format for Rust code is enforced by `cargo fmt`. 34 | Pull requests will be automatically fixed by CI. 35 | 36 | 37 | ## Introspecting the tokio runtime 38 | 39 | The asynchronous runtime can be introspected using `tokio-console` if the crate 40 | was built with the `tracing` feature: 41 | 42 | ```shell 43 | tokio-console http://localhost:6669 44 | ``` 45 | 46 | There should be no task that is busy when the program is idle, i.e. there should 47 | be no busy waiting. 48 | 49 | 50 | ## Release Process 51 | 52 | If you are the current maintainer of mitmproxy_rs, 53 | you can perform the following steps to ship a release: 54 | 55 | 1. Make sure that CI is passing without errors. 56 | 2. Make sure that CHANGELOG.md is up-to-date with all entries in the "Unreleased" section. 57 | 3. Invoke the release workflow from the GitHub UI: https://github.com/mitmproxy/mitmproxy_rs/actions/workflows/release.yml 58 | 4. The spawned workflow run will require manual deploy confirmation on GitHub: https://github.com/mitmproxy/mitmproxy/actions 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "mitmproxy-contentviews", 5 | "mitmproxy-highlight", 6 | "mitmproxy-rs", 7 | "mitmproxy-linux", 8 | "mitmproxy-linux-ebpf", 9 | "mitmproxy-linux-ebpf-common", 10 | "mitmproxy-macos/certificate-truster", 11 | "mitmproxy-windows/redirector", 12 | "wireguard-test-client", 13 | ] 14 | default-members = [ 15 | ".", 16 | "mitmproxy-contentviews", 17 | "mitmproxy-highlight", 18 | "mitmproxy-rs", 19 | "mitmproxy-linux", 20 | "mitmproxy-linux-ebpf-common", 21 | "mitmproxy-macos/certificate-truster", 22 | "mitmproxy-windows/redirector", 23 | "wireguard-test-client", 24 | ] 25 | 26 | [workspace.package] 27 | authors = [ 28 | "Fabio Valentini ", 29 | "Maximilian Hils ", 30 | ] 31 | version = "0.13.0-dev" 32 | publish = false 33 | repository = "https://github.com/mitmproxy/mitmproxy-rs" 34 | edition = "2021" 35 | rust-version = "1.85" # MSRV 36 | 37 | [workspace.dependencies] 38 | aya = { version = "0.13.0", default-features = false } 39 | aya-ebpf = { version = "0.1.1", default-features = false } 40 | aya-log = { version = "0.2.1", default-features = false } 41 | aya-log-ebpf = { version = "0.1.1", default-features = false } 42 | tun = { version = "0.7.22" } 43 | 44 | [workspace.lints.clippy] 45 | large_futures = "deny" 46 | 47 | [package] 48 | name = "mitmproxy" 49 | license = "MIT" 50 | authors.workspace = true 51 | version.workspace = true 52 | repository.workspace = true 53 | edition.workspace = true 54 | rust-version.workspace = true 55 | publish.workspace = true 56 | 57 | [dependencies] 58 | anyhow = { version = "1.0.97", features = ["backtrace"] } 59 | log = "0.4.27" 60 | pretty-hex = "0.4.1" 61 | smoltcp = "0.12" 62 | tokio = { version = "1.45.1", features = ["macros", "net", "rt-multi-thread", "sync", "time", "io-util", "process"] } 63 | boringtun = { version = "0.6", default-features = false } 64 | console-subscriber = { version = "0.4.1", optional = true } 65 | image = { version = "0.25.6", default-features = false, features = ["png", "tiff"] } 66 | prost = "0.13.5" 67 | tokio-util = { version = "0.7.14", features = ["codec"] } 68 | futures-util = { version = "0.3.31", features = ["sink"] } 69 | lru_time_cache = "0.11.11" 70 | internet-packet = { version = "0.2.3", features = ["smoltcp"] } 71 | data-encoding = "2.8.0" 72 | hickory-resolver = "0.25.2" 73 | socket2 = "0.5.10" 74 | 75 | [patch.crates-io] 76 | # tokio = { path = "../tokio/tokio" } 77 | boringtun = { git = 'https://github.com/cloudflare/boringtun', rev = 'e3252d9c4f4c8fc628995330f45369effd4660a1' } 78 | 79 | [target.'cfg(windows)'.dependencies.windows] 80 | version = "0.61.1" 81 | features = [ 82 | "Win32_Foundation", 83 | "Win32_Graphics_Dwm", 84 | "Win32_Graphics_Gdi", 85 | "Win32_Networking_WinSock", 86 | "Win32_NetworkManagement_IpHelper", 87 | "Win32_Storage_FileSystem", 88 | "Win32_System_LibraryLoader", 89 | "Win32_System_ProcessStatus", 90 | "Win32_System_Threading", 91 | "Win32_UI_Shell", 92 | "Win32_UI_WindowsAndMessaging", 93 | ] 94 | 95 | [target.'cfg(target_os = "macos")'.dependencies] 96 | security-framework = "3.2.0" 97 | nix = { version = "0.30.1", default-features = false, features = ["fs"] } 98 | core-graphics = "0.25" 99 | core-foundation = "0.10" 100 | cocoa = "0.26" 101 | objc = "0.2" 102 | sysinfo = "0.35.1" 103 | 104 | [target.'cfg(target_os = "linux")'.dependencies] 105 | tun = { workspace = true, features = ["async"] } 106 | tempfile = "3.20.0" 107 | sysinfo = "0.35.1" 108 | 109 | [dev-dependencies] 110 | env_logger = "0.11" 111 | rand = "0.9" 112 | criterion = "0.6.0" 113 | hickory-server = "0.25.2" 114 | 115 | [[bench]] 116 | name = "process" 117 | harness = false 118 | 119 | [profile.release] 120 | codegen-units = 1 121 | lto = true 122 | opt-level = 3 123 | 124 | [features] 125 | tracing = ["console-subscriber"] 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Fabio Valentini and Maximilian Hils 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy_rs 2 | 3 | [![autofix.ci: enabled](https://shields.mitmproxy.org/badge/autofix.ci-yes-success?logo=data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZmZmIiB2aWV3Qm94PSIwIDAgMTI4IDEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCB0cmFuc2Zvcm09InNjYWxlKDAuMDYxLC0wLjA2MSkgdHJhbnNsYXRlKC0yNTAsLTE3NTApIiBkPSJNMTMyNSAtMzQwcS0xMTUgMCAtMTY0LjUgMzIuNXQtNDkuNSAxMTQuNXEwIDMyIDUgNzAuNXQxMC41IDcyLjV0NS41IDU0djIyMHEtMzQgLTkgLTY5LjUgLTE0dC03MS41IC01cS0xMzYgMCAtMjUxLjUgNjJ0LTE5MSAxNjl0LTkyLjUgMjQxcS05MCAxMjAgLTkwIDI2NnEwIDEwOCA0OC41IDIwMC41dDEzMiAxNTUuNXQxODguNSA4MXExNSA5OSAxMDAuNSAxODAuNXQyMTcgMTMwLjV0MjgyLjUgNDlxMTM2IDAgMjU2LjUgLTQ2IHQyMDkgLTEyNy41dDEyOC41IC0xODkuNXExNDkgLTgyIDIyNyAtMjEzLjV0NzggLTI5OS41cTAgLTEzNiAtNTggLTI0NnQtMTY1LjUgLTE4NC41dC0yNTYuNSAtMTAzLjVsLTI0MyAtMzAwdi01MnEwIC0yNyAzLjUgLTU2LjV0Ni41IC01Ny41dDMgLTUycTAgLTg1IC00MS41IC0xMTguNXQtMTU3LjUgLTMzLjV6TTEzMjUgLTI2MHE3NyAwIDk4IDE0LjV0MjEgNTcuNXEwIDI5IC0zIDY4dC02LjUgNzN0LTMuNSA0OHY2NGwyMDcgMjQ5IHEtMzEgMCAtNjAgNS41dC01NCAxMi41bC0xMDQgLTEyM3EtMSAzNCAtMiA2My41dC0xIDU0LjVxMCA2OSA5IDEyM2wzMSAyMDBsLTExNSAtMjhsLTQ2IC0yNzFsLTIwNSAyMjZxLTE5IC0xNSAtNDMgLTI4LjV0LTU1IC0yNi41bDIxOSAtMjQydi0yNzZxMCAtMjAgLTUuNSAtNjB0LTEwLjUgLTc5dC01IC01OHEwIC00MCAzMCAtNTMuNXQxMDQgLTEzLjV6TTEyNjIgNjE2cS0xMTkgMCAtMjI5LjUgMzQuNXQtMTkzLjUgOTYuNWw0OCA2NCBxNzMgLTU1IDE3MC41IC04NXQyMDQuNSAtMzBxMTM3IDAgMjQ5IDQ1LjV0MTc5IDEyMXQ2NyAxNjUuNWg4MHEwIC0xMTQgLTc3LjUgLTIwNy41dC0yMDggLTE0OXQtMjg5LjUgLTU1LjV6TTgwMyA1OTVxODAgMCAxNDkgMjkuNXQxMDggNzIuNWwyMjEgLTY3bDMwOSA4NnE0NyAtMzIgMTA0LjUgLTUwdDExNy41IC0xOHE5MSAwIDE2NSAzOHQxMTguNSAxMDMuNXQ0NC41IDE0Ni41cTAgNzYgLTM0LjUgMTQ5dC05NS41IDEzNHQtMTQzIDk5IHEtMzcgMTA3IC0xMTUuNSAxODMuNXQtMTg2IDExNy41dC0yMzAuNSA0MXEtMTAzIDAgLTE5Ny41IC0yNnQtMTY5IC03Mi41dC0xMTcuNSAtMTA4dC00MyAtMTMxLjVxMCAtMzQgMTQuNSAtNjIuNXQ0MC41IC01MC41bC01NSAtNTlxLTM0IDI5IC01NCA2NS41dC0yNSA4MS41cS04MSAtMTggLTE0NSAtNzB0LTEwMSAtMTI1LjV0LTM3IC0xNTguNXEwIC0xMDIgNDguNSAtMTgwLjV0MTI5LjUgLTEyM3QxNzkgLTQ0LjV6Ii8+PC9zdmc+)](https://autofix.ci) 4 | [![Continuous Integration Status](https://github.com/mitmproxy/mitmproxy_rs/workflows/CI/badge.svg?branch=main)](https://github.com/mitmproxy/mitmproxy_rs/actions?query=branch%3Amain) 5 | [![Latest Version](https://shields.mitmproxy.org/pypi/v/mitmproxy-rs.svg)](https://pypi.python.org/pypi/mitmproxy-wireguard) 6 | [![Supported Python versions](https://shields.mitmproxy.org/pypi/pyversions/mitmproxy.svg)](https://pypi.python.org/pypi/mitmproxy) 7 | ![PyPI - Wheel](https://shields.mitmproxy.org/pypi/wheel/mitmproxy-rs) 8 | 9 | 10 | This repository contains mitmproxy's Rust bits, most notably: 11 | 12 | - WireGuard Mode: The ability to proxy any device that can be configured as a WireGuard client. 13 | - Local Redirect Mode: The ability to proxy arbitrary macOS or Windows applications by name or pid. 14 | 15 | 16 | ## Contributing 17 | 18 | [![Dev Guide](https://shields.mitmproxy.org/badge/docs-CONTRIBUTING.md-blue)](https://github.com/mitmproxy/mitmproxy_rs/blob/main/CONTRIBUTING.md) 19 | [![dev documentation](https://shields.mitmproxy.org/badge/docs-Python%20API-blue.svg)](https://mitmproxy.github.io/mitmproxy_rs/) 20 | 21 | ### Structure 22 | 23 | - [`src/`](./src): The `mitmproxy` crate containing most of the "meat". 24 | - [`mitmproxy-contentviews/`](./mitmproxy-contentviews): 25 | Pretty-printers for (HTTP) message bodies. 26 | - [`mitmproxy-highlight/`](./mitmproxy-highlight): 27 | Syntax highlighting backend for mitmproxy and mitmdump. 28 | - [`mitmproxy-rs/`](./mitmproxy-rs): The `mitmproxy-rs` Python package, 29 | which provides Python bindings for the Rust crate using [PyO3](https://pyo3.rs/). 30 | Source and binary distributions are available [on PyPI](https://pypi.org/project/mitmproxy-rs/). 31 | - [`mitmproxy-macos/`](./mitmproxy-macos): The `mitmproxy-macos` Python package, which 32 | contains a macOS Network Extension to transparently intercept macOS traffic. 33 | Only a binary distribution is available [on PyPI](https://pypi.org/project/mitmproxy-macos/) 34 | due to code signing and notarization requirements. 35 | - [`mitmproxy-windows/`](./mitmproxy-windows): The `mitmproxy-windows` Python package, which 36 | contains the Windows traffic redirector based on [WinDivert](https://github.com/basil00/WinDivert). 37 | Only a binary distribution is available [on PyPI](https://pypi.org/project/mitmproxy-windows/) 38 | due to build complexity. 39 | - [`mitmproxy-linux/`](./mitmproxy-linux): The `mitmproxy-linux` Python package, which 40 | contains the Linux traffic redirector based on [Aya](https://aya-rs.dev/). 41 | Source and binary distributions are available [on PyPI](https://pypi.org/project/mitmproxy-linux/). 42 | - [`mitmproxy-linux-ebpf/`](./mitmproxy-linux-ebpf): The eBPF program embedded in `mitmproxy-linux`. 43 | - [`mitmproxy-linux-ebpf-common/`](./mitmproxy-linux-ebpf-common): Data structures shared by user space and eBPF. 44 | 45 | ### Architecture 46 | 47 | ![library architecture](architecture.png) 48 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/architecture.png -------------------------------------------------------------------------------- /benches/.gitignore: -------------------------------------------------------------------------------- 1 | /*.csv 2 | /*.png 3 | /*.json 4 | -------------------------------------------------------------------------------- /benches/dns.py: -------------------------------------------------------------------------------- 1 | import mitmproxy_rs 2 | import asyncio 3 | import socket 4 | 5 | async def main(): 6 | resolver = mitmproxy_rs.DnsResolver(use_hosts_file=False, name_servers=['8.8.8.8']) 7 | 8 | async def lookup(host: str): 9 | try: 10 | r = await resolver.lookup_ip(host) 11 | except socket.gaierror as e: 12 | print(f"{host=} {e=}") 13 | else: 14 | print(f"{host=} {r=}") 15 | 16 | await lookup("example.com.") 17 | await lookup("nxdomain.mitmproxy.org.") 18 | await lookup("no-a-records.mitmproxy.org.") 19 | 20 | print(f"{mitmproxy_rs.get_system_dns_servers()=}") 21 | 22 | 23 | asyncio.run(main()) 24 | -------------------------------------------------------------------------------- /benches/local.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | PrivateKey = qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk= 3 | Address = 10.0.0.1/32 4 | MTU = 1420 5 | 6 | [Peer] 7 | PublicKey = mitmV5Wo7pRJrHNAKhZEI0nzqqeO8u4fXG+zUbZEXA0= 8 | AllowedIPs = 10.0.0.0/24 9 | Endpoint = 127.0.0.1:51820 10 | -------------------------------------------------------------------------------- /benches/nonlocal.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | PrivateKey = qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk= 3 | Address = 10.0.0.2/32 4 | 5 | [Peer] 6 | PublicKey = mitmV5Wo7pRJrHNAKhZEI0nzqqeO8u4fXG+zUbZEXA0= 7 | AllowedIPs = 10.0.0.0/24 8 | Endpoint = 192.168.86.134:51820 9 | -------------------------------------------------------------------------------- /benches/openvpnserv.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/benches/openvpnserv.exe -------------------------------------------------------------------------------- /benches/process.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | #[cfg(any(windows, target_os = "macos"))] 3 | use mitmproxy::processes; 4 | 5 | #[allow(unused_variables)] 6 | fn criterion_benchmark(c: &mut Criterion) { 7 | #[cfg(target_os = "macos")] 8 | let test_executable = 9 | std::path::PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder"); 10 | #[cfg(windows)] 11 | let test_executable = { 12 | let mut test_executable = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 13 | test_executable.push("benches\\openvpnserv.exe"); 14 | test_executable 15 | }; 16 | 17 | #[cfg(any(windows, target_os = "macos"))] 18 | { 19 | c.bench_function("active_executables", |b| { 20 | b.iter(processes::active_executables) 21 | }); 22 | 23 | c.bench_function("visible_windows", |b| { 24 | b.iter(processes::bench::visible_windows) 25 | }); 26 | 27 | c.bench_function("get_png", |b| { 28 | b.iter(|| { 29 | processes::bench::IconCache::default() 30 | .get_png(test_executable.clone()) 31 | .unwrap(); 32 | }) 33 | }); 34 | 35 | let mut cache = processes::bench::IconCache::default(); 36 | cache.get_png(test_executable.clone()).unwrap(); 37 | c.bench_function("get_png (cached)", |b| { 38 | b.iter(|| { 39 | cache.get_png(test_executable.clone()).unwrap(); 40 | }) 41 | }); 42 | } 43 | 44 | #[cfg(windows)] 45 | { 46 | let pid = std::process::id(); 47 | 48 | c.bench_function("is_critical", |b| { 49 | b.iter(|| processes::bench::get_is_critical(pid)) 50 | }); 51 | c.bench_function("get_process_name", |b| { 52 | b.iter(|| processes::bench::get_process_name(pid)) 53 | }); 54 | c.bench_function("enumerate_pids", |b| { 55 | b.iter(processes::bench::enumerate_pids) 56 | }); 57 | c.bench_function("get_display_name", |b| { 58 | b.iter(|| processes::bench::get_display_name(&test_executable)) 59 | }); 60 | } 61 | 62 | #[cfg(target_os = "macos")] 63 | { 64 | c.bench_function("tiff_data_for_executable", |b| { 65 | b.iter(|| processes::bench::tiff_data_for_executable(&test_executable).unwrap()) 66 | }); 67 | 68 | let tiff = processes::bench::tiff_data_for_executable(&test_executable).unwrap(); 69 | c.bench_function("tiff_to_png", |b| { 70 | b.iter(|| { 71 | processes::bench::tiff_to_png(&tiff); 72 | }) 73 | }); 74 | } 75 | } 76 | 77 | criterion_group!(benches, criterion_benchmark); 78 | criterion_main!(benches); 79 | -------------------------------------------------------------------------------- /benches/py_echo_client.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import json 4 | import pprint 5 | import timeit 6 | 7 | 8 | # generate unique test packets with the given length 9 | def gen_data(pnum: int, psize: int) -> list[bytes]: 10 | packets = [] 11 | 12 | for i in range(pnum): 13 | packet = (f"{i:04d}".encode() * (psize // 4 + 1))[:psize] 14 | assert len(packet) == psize 15 | packets.append(packet) 16 | 17 | return packets 18 | 19 | 20 | async def work(host: str, port: int, packets: list[bytes]): 21 | r, w = await asyncio.open_connection(host, port) 22 | 23 | bytes_back = [] 24 | 25 | for packet in packets: 26 | w.write(packet) 27 | await w.drain() 28 | 29 | recv_len = 0 30 | recv_bytes = [] 31 | 32 | while recv_len != len(packet): 33 | read = await r.read(4096) 34 | recv_bytes.extend(read) 35 | recv_len += len(read) 36 | 37 | bytes_back.append(bytes(recv_bytes)) 38 | 39 | w.close() 40 | await w.wait_closed() 41 | 42 | try: 43 | assert packets == bytes_back 44 | 45 | except AssertionError: 46 | bytes_sent = sum(map(len, packets)) 47 | bytes_received = sum(map(len, bytes_back)) 48 | 49 | pprint.pprint(packets) 50 | pprint.pprint(bytes_back) 51 | 52 | print(f"Bytes Sent: {bytes_sent}") 53 | print(f"Bytes Received: {bytes_received}") 54 | print(f"Difference: {bytes_sent - bytes_received}") 55 | 56 | raise 57 | 58 | 59 | def main(): 60 | parser = argparse.ArgumentParser() 61 | parser.add_argument("name", choices=["local", "nonlocal"]) 62 | parser.add_argument("host", default="0.0.0.0") 63 | parser.add_argument("port", default=51820) 64 | args = parser.parse_args() 65 | 66 | # host = "192.168.86.134" 67 | name = args.name 68 | host = args.host 69 | port = args.port 70 | 71 | reps = 10 72 | numbs = [1000, 2000, 5000, 10000, 20000, 50000, 100000] 73 | sizes = [1000] 74 | 75 | x = list() 76 | ys = dict() 77 | 78 | for numb in numbs: 79 | x.extend([numb] * reps) 80 | 81 | for size in sizes: 82 | ys[size] = list() 83 | 84 | for numb in numbs: 85 | data = gen_data(numb, size) 86 | timer = timeit.Timer(lambda: asyncio.run(work(host, port, data), debug=True), "gc.enable()") 87 | 88 | print(f"Packet number: {numb}") 89 | print(f"Packet size: {size} bytes") 90 | 91 | times = timer.repeat(reps, number=1) 92 | ys[size].extend(times) 93 | 94 | print() 95 | 96 | with open(f"py_data_{name}.json", "w") as file: 97 | json.dump(dict(x=x, ys=ys, sizes=sizes), file, indent=4) 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /benches/py_echo_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import signal 4 | import sys 5 | import time 6 | 7 | 8 | LOG_FORMAT = "[%(asctime)s %(levelname)-5s %(name)s] %(message)s" 9 | TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 10 | 11 | try: 12 | from rich import print 13 | from rich.logging import RichHandler 14 | 15 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT, handlers=[RichHandler()]) 16 | except ImportError: 17 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT) 18 | 19 | logging.Formatter.convert = time.gmtime 20 | logger = logging.getLogger() 21 | logger.setLevel(logging.DEBUG) 22 | 23 | 24 | async def main(): 25 | server = await asyncio.start_server(handle_connection, "0.0.0.0", 51820) 26 | 27 | def stop(*_): 28 | server.close() 29 | signal.signal(signal.SIGINT, lambda *_: sys.exit()) 30 | 31 | signal.signal(signal.SIGINT, stop) 32 | 33 | await server.wait_closed() 34 | 35 | 36 | async def handle_connection(r: asyncio.StreamReader, w: asyncio.StreamWriter): 37 | logger.debug(f"Connection established: {r} / {w}") 38 | 39 | while True: 40 | data = await r.read(4096) 41 | 42 | # check if the connection was closed 43 | if len(data) == 0: 44 | break 45 | 46 | w.write(data) 47 | await w.drain() 48 | 49 | w.close() 50 | logger.debug(f"Connection closed: {r} / {w}") 51 | 52 | 53 | if __name__ == "__main__": 54 | asyncio.run(main(), debug=True) 55 | -------------------------------------------------------------------------------- /benches/wg_echo_client.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import json 4 | import pprint 5 | import timeit 6 | 7 | 8 | # generate unique test packets with the given length 9 | def gen_data(pnum: int, psize: int) -> list[bytes]: 10 | packets = [] 11 | 12 | for i in range(pnum): 13 | packet = (f"{i:04d}".encode() * (psize // 4 + 1))[:psize] 14 | assert len(packet) == psize 15 | packets.append(packet) 16 | 17 | return packets 18 | 19 | 20 | async def work(host: str, port: int, packets: list[bytes]): 21 | r, w = await asyncio.open_connection(host, port) 22 | 23 | bytes_back = [] 24 | 25 | for packet in packets: 26 | w.write(packet) 27 | await w.drain() 28 | 29 | recv_len = 0 30 | recv_bytes = [] 31 | 32 | while recv_len != len(packet): 33 | read = await r.read(4096) 34 | recv_bytes.extend(read) 35 | recv_len += len(read) 36 | 37 | bytes_back.append(bytes(recv_bytes)) 38 | 39 | w.close() 40 | await w.wait_closed() 41 | 42 | try: 43 | assert packets == bytes_back 44 | 45 | except AssertionError: 46 | bytes_sent = sum(map(len, packets)) 47 | bytes_received = sum(map(len, bytes_back)) 48 | 49 | pprint.pprint(packets) 50 | pprint.pprint(bytes_back) 51 | 52 | print(f"Bytes Sent: {bytes_sent}") 53 | print(f"Bytes Received: {bytes_received}") 54 | print(f"Difference: {bytes_sent - bytes_received}") 55 | 56 | raise 57 | 58 | 59 | def main(): 60 | parser = argparse.ArgumentParser() 61 | parser.add_argument("name", choices=["local", "nonlocal"]) 62 | parser.add_argument("host", default="10.0.0.42") 63 | parser.add_argument("port", default=1234) 64 | args = parser.parse_args() 65 | 66 | name = args.name 67 | host = args.host 68 | port = args.port 69 | 70 | reps = 10 71 | numbs = [1000, 2000, 5000, 10000, 20000, 50000, 100000] 72 | sizes = [1000] 73 | 74 | x = list() 75 | ys = dict() 76 | 77 | for numb in numbs: 78 | x.extend([numb] * reps) 79 | 80 | for size in sizes: 81 | ys[size] = list() 82 | 83 | for numb in numbs: 84 | data = gen_data(numb, size) 85 | timer = timeit.Timer(lambda: asyncio.run(work(host, port, data), debug=True), "gc.enable()") 86 | 87 | print(f"Packet number: {numb}") 88 | print(f"Packet size: {size} bytes") 89 | 90 | times = timer.repeat(reps, number=1) 91 | ys[size].extend(times) 92 | 93 | print() 94 | 95 | with open(f"wg_data_{name}.json", "w") as file: 96 | json.dump(dict(x=x, ys=ys, sizes=sizes), file, indent=4) 97 | 98 | 99 | if __name__ == "__main__": 100 | main() 101 | -------------------------------------------------------------------------------- /benches/wg_echo_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import signal 4 | import sys 5 | import time 6 | 7 | import mitmproxy_rs 8 | 9 | 10 | LOG_FORMAT = "[%(asctime)s %(levelname)-5s %(name)s] %(message)s" 11 | TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 12 | 13 | try: 14 | from rich import print 15 | from rich.logging import RichHandler 16 | 17 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT, handlers=[RichHandler()]) 18 | except ImportError: 19 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT) 20 | 21 | logging.Formatter.convert = time.gmtime 22 | logger = logging.getLogger() 23 | logger.setLevel(logging.DEBUG) 24 | 25 | 26 | # (private key, public key) 27 | server_keypair = ( 28 | "EG47ZWjYjr+Y97TQ1A7sVl7Xn3mMWDnvjU/VxU769ls=", 29 | "mitmV5Wo7pRJrHNAKhZEI0nzqqeO8u4fXG+zUbZEXA0=", 30 | ) 31 | client_keypair = ( 32 | "qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk=", 33 | "Test1sbpTFmJULgSlJ5hJ1RdzsXWrl3Mg7k9UTN//jE=", 34 | ) 35 | 36 | 37 | async def main(): 38 | server = await mitmproxy_rs.start_wireguard_server( 39 | "0.0.0.0", 40 | 51820, 41 | server_keypair[0], 42 | [client_keypair[1]], 43 | handle_connection, 44 | receive_datagram, 45 | ) 46 | 47 | print( 48 | f""" 49 | ------------------------------------------------------------ 50 | [Interface] 51 | PrivateKey = {client_keypair[0]} 52 | Address = 10.0.0.1/32 53 | MTU = 1420 54 | 55 | [Peer] 56 | PublicKey = {server_keypair[1]} 57 | AllowedIPs = 10.0.0.0/24 58 | Endpoint = 127.0.0.1:51820 59 | ------------------------------------------------------------ 60 | """ 61 | ) 62 | 63 | def stop(*_): 64 | server.close() 65 | signal.signal(signal.SIGINT, lambda *_: sys.exit()) 66 | 67 | signal.signal(signal.SIGINT, stop) 68 | 69 | await server.wait_closed() 70 | 71 | 72 | async def handle_connection(rw: mitmproxy_rs.TcpStream): 73 | logger.debug(f"Connection established: {rw}") 74 | 75 | while True: 76 | data = await rw.read(4096) 77 | 78 | # check if the connection was closed 79 | if len(data) == 0: 80 | break 81 | 82 | rw.write(data) 83 | await rw.drain() 84 | 85 | rw.close() 86 | logger.debug(f"Connection closed: {rw}") 87 | 88 | 89 | def receive_datagram(_data, _src_addr, _dst_addr): 90 | pass 91 | 92 | 93 | if __name__ == "__main__": 94 | asyncio.run(main(), debug=True) 95 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy-contentviews" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [dependencies] 15 | anyhow = { version = "1.0.97", features = ["backtrace"] } 16 | log = "0.4.27" 17 | data-encoding = "2.8.0" 18 | pretty-hex = "0.4.1" 19 | mitmproxy-highlight = { path = "../mitmproxy-highlight" } 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_yaml = "0.9" 22 | rmp-serde = "1.1" 23 | protobuf = "3.7.2" 24 | regex = "1.10.3" 25 | flate2 = "1.0" 26 | protobuf-parse = "3.7" 27 | 28 | [dev-dependencies] 29 | criterion = "0.6.0" 30 | 31 | [[bench]] 32 | name = "contentviews" 33 | harness = false 34 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-contentviews 2 | 3 | This crate contains various contentviews for mitmproxy, 4 | exposed to Python via [mitmproxy-rs/src/contentviews.rs]. 5 | For additional documentation, see https://docs.mitmproxy.org/dev/addons/contentviews/. 6 | 7 | [mitmproxy-rs/src/contentviews.rs]: ../mitmproxy-rs/src/contentviews.rs 8 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/benches/contentviews.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use mitmproxy_contentviews::{test::TestMetadata, MsgPack, Prettify, Protobuf, Reencode}; 3 | use std::hint::black_box; 4 | 5 | fn criterion_benchmark(c: &mut Criterion) { 6 | c.bench_function("protobuf-prettify", |b| { 7 | b.iter(|| { 8 | Protobuf.prettify(black_box(b"\n\x13gRPC testing server\x12\x07\n\x05Index\x12\x07\n\x05Empty\x12\x0c\n\nDummyUnary\x12\x0f\n\rSpecificError\x12\r\n\x0bRandomError\x12\x0e\n\x0cHeadersUnary\x12\x11\n\x0fNoResponseUnary"), &TestMetadata::default()).unwrap() 9 | }) 10 | }); 11 | 12 | c.bench_function("protobuf-reencode", |b| { 13 | b.iter(|| { 14 | Protobuf.reencode( 15 | black_box("1: gRPC testing server\n2:\n- 1: Index\n- 1: Empty\n- 1: DummyUnary\n- 1: SpecificError\n- 1: RandomError\n- 1: HeadersUnary\n- 1: NoResponseUnary\n"), 16 | &TestMetadata::default() 17 | ).unwrap() 18 | }) 19 | }); 20 | 21 | const TEST_MSGPACK: &[u8] = &[ 22 | 0x83, // map with 3 elements 23 | 0xa4, 0x6e, 0x61, 0x6d, 0x65, // "name" 24 | 0xa8, 0x4a, 0x6f, 0x68, 0x6e, 0x20, 0x44, 0x6f, 0x65, // "John Doe" 25 | 0xa3, 0x61, 0x67, 0x65, // "age" 26 | 0x1e, // 30 27 | 0xa4, 0x74, 0x61, 0x67, 0x73, // "tags" 28 | 0x92, // array with 2 elements 29 | 0xa9, 0x64, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, // "developer" 30 | 0xa4, 0x72, 0x75, 0x73, 0x74, // "rust" 31 | ]; 32 | c.bench_function("msgpack-prettify", |b| { 33 | b.iter(|| { 34 | MsgPack 35 | .prettify(black_box(TEST_MSGPACK), &TestMetadata::default()) 36 | .unwrap() 37 | }) 38 | }); 39 | 40 | c.bench_function("msgpack-reencode", |b| { 41 | b.iter(|| { 42 | MsgPack 43 | .reencode( 44 | black_box( 45 | "\ 46 | name: John Doe\n\ 47 | age: 30\n\ 48 | tags:\n\ 49 | - developer\n\ 50 | - rust\n\ 51 | ", 52 | ), 53 | &TestMetadata::default(), 54 | ) 55 | .unwrap() 56 | }) 57 | }); 58 | } 59 | 60 | criterion_group!(benches, criterion_benchmark); 61 | criterion_main!(benches); 62 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/hex_dump.rs: -------------------------------------------------------------------------------- 1 | use crate::hex_stream::is_binary; 2 | use crate::{Metadata, Prettify}; 3 | use pretty_hex::{HexConfig, PrettyHex}; 4 | 5 | pub struct HexDump; 6 | 7 | impl Prettify for HexDump { 8 | fn name(&self) -> &'static str { 9 | "Hex Dump" 10 | } 11 | 12 | fn prettify(&self, data: &[u8], _metadata: &dyn Metadata) -> anyhow::Result { 13 | Ok(format!( 14 | "{:?}", 15 | data.hex_conf(HexConfig { 16 | title: false, 17 | ascii: true, 18 | width: 16, 19 | group: 4, 20 | chunk: 1, 21 | max_bytes: usize::MAX, 22 | display_offset: 0, 23 | }) 24 | )) 25 | } 26 | 27 | fn render_priority(&self, data: &[u8], _metadata: &dyn Metadata) -> f32 { 28 | if is_binary(data) { 29 | 0.5 30 | } else { 31 | 0.0 32 | } 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | use crate::test::TestMetadata; 40 | 41 | #[test] 42 | fn prettify_simple() { 43 | let result = HexDump.prettify(b"abcd", &TestMetadata::default()).unwrap(); 44 | assert_eq!( 45 | result, 46 | "0000: 61 62 63 64 abcd" 47 | ); 48 | } 49 | 50 | #[test] 51 | fn prettify_empty() { 52 | let result = HexDump.prettify(b"", &TestMetadata::default()).unwrap(); 53 | assert_eq!(result, ""); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/hex_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{Metadata, Prettify, Reencode}; 2 | use anyhow::{Context, Result}; 3 | 4 | pub struct HexStream; 5 | 6 | pub(crate) fn is_binary(data: &[u8]) -> bool { 7 | if data.is_empty() { 8 | return false; 9 | } 10 | let ratio = data 11 | .iter() 12 | .take(100) 13 | .filter(|&&b| b < 9 || (13 < b && b < 32) || b > 126) 14 | .count() as f64 15 | / data.len().min(100) as f64; 16 | 17 | ratio > 0.3 18 | } 19 | 20 | impl Prettify for HexStream { 21 | fn name(&self) -> &'static str { 22 | "Hex Stream" 23 | } 24 | 25 | fn prettify(&self, data: &[u8], _metadata: &dyn Metadata) -> Result { 26 | Ok(data_encoding::HEXLOWER.encode(data)) 27 | } 28 | 29 | fn render_priority(&self, data: &[u8], _metadata: &dyn Metadata) -> f32 { 30 | if is_binary(data) { 31 | 0.4 32 | } else { 33 | 0.0 34 | } 35 | } 36 | } 37 | 38 | impl Reencode for HexStream { 39 | fn reencode(&self, data: &str, _metadata: &dyn Metadata) -> Result> { 40 | let data = data.trim_end_matches(['\n', '\r']); 41 | if data.len() % 2 != 0 { 42 | anyhow::bail!("Invalid hex string: uneven number of characters"); 43 | } 44 | data_encoding::HEXLOWER_PERMISSIVE 45 | .decode(data.as_bytes()) 46 | .context("Invalid hex string") 47 | } 48 | } 49 | 50 | #[cfg(test)] 51 | mod tests { 52 | use super::*; 53 | use crate::test::TestMetadata; 54 | 55 | #[test] 56 | fn test_hex_stream() { 57 | let result = HexStream 58 | .prettify(b"foo", &TestMetadata::default()) 59 | .unwrap(); 60 | assert_eq!(result, "666f6f"); 61 | } 62 | 63 | #[test] 64 | fn test_hex_stream_empty() { 65 | let result = HexStream.prettify(b"", &TestMetadata::default()).unwrap(); 66 | assert_eq!(result, ""); 67 | } 68 | 69 | #[test] 70 | fn test_hex_stream_reencode() { 71 | let data = "666f6f"; 72 | let result = HexStream.reencode(data, &TestMetadata::default()).unwrap(); 73 | assert_eq!(result, b"foo"); 74 | } 75 | 76 | #[test] 77 | fn test_hex_stream_reencode_with_newlines() { 78 | let data = "666f6f\r\n"; 79 | let result = HexStream.reencode(data, &TestMetadata::default()).unwrap(); 80 | assert_eq!(result, b"foo"); 81 | } 82 | 83 | #[test] 84 | fn test_hex_stream_reencode_uneven_chars() { 85 | let data = "666f6"; 86 | let result = HexStream.reencode(data, &TestMetadata::default()); 87 | assert!(result.is_err()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod hex_dump; 2 | mod hex_stream; 3 | mod msgpack; 4 | mod protobuf; 5 | mod test_inspect_metadata; 6 | 7 | pub use hex_dump::HexDump; 8 | pub use hex_stream::HexStream; 9 | pub use msgpack::MsgPack; 10 | pub use protobuf::Protobuf; 11 | pub use protobuf::GRPC; 12 | pub use test_inspect_metadata::TestInspectMetadata; 13 | 14 | use anyhow::Result; 15 | use mitmproxy_highlight::Language; 16 | 17 | use serde::Serialize; 18 | use std::path::Path; 19 | 20 | pub trait Metadata { 21 | /// The HTTP `content-type` of this message. 22 | fn content_type(&self) -> Option<&str>; 23 | /// Get an HTTP header value by name. 24 | /// `name` is case-insensitive. 25 | fn get_header(&self, name: &str) -> Option; 26 | /// Get the path from the flow's request. 27 | fn get_path(&self) -> Option<&str> { 28 | None 29 | } 30 | /// Check if this is an HTTP request. 31 | fn is_http_request(&self) -> bool { 32 | false 33 | } 34 | /// Get the protobuf definitions for this message. 35 | fn protobuf_definitions(&self) -> Option<&Path> { 36 | None 37 | } 38 | } 39 | 40 | /// See https://docs.mitmproxy.org/dev/api/mitmproxy/contentviews.html 41 | /// for API details. 42 | pub trait Prettify: Send + Sync { 43 | /// The name for this contentview, e.g. `gRPC` or `Protobuf`. 44 | /// Favor brevity. 45 | fn name(&self) -> &str; 46 | 47 | fn instance_name(&self) -> String { 48 | self.name().to_lowercase().replace(" ", "_") 49 | } 50 | 51 | /// The syntax highlighting that should be applied to the prettified output. 52 | /// This is useful for contentviews that prettify to JSON or YAML. 53 | fn syntax_highlight(&self) -> Language { 54 | Language::None 55 | } 56 | 57 | /// Pretty-print `data`. 58 | fn prettify(&self, data: &[u8], metadata: &dyn Metadata) -> Result; 59 | 60 | /// Render priority - typically a float between 0 and 1 for builtin views. 61 | #[allow(unused_variables)] 62 | fn render_priority(&self, data: &[u8], metadata: &dyn Metadata) -> f32 { 63 | 0.0 64 | } 65 | } 66 | 67 | pub trait Reencode: Send + Sync { 68 | fn reencode(&self, data: &str, metadata: &dyn Metadata) -> Result>; 69 | } 70 | 71 | // no cfg(test) gate because it's used in benchmarks as well 72 | pub mod test { 73 | use super::*; 74 | 75 | #[derive(Default, Serialize)] 76 | pub struct TestMetadata { 77 | pub content_type: Option, 78 | pub headers: std::collections::HashMap, 79 | pub protobuf_definitions: Option, 80 | pub path: Option, 81 | pub is_http_request: bool, 82 | } 83 | 84 | impl TestMetadata { 85 | pub fn with_content_type(mut self, content_type: &str) -> Self { 86 | self.content_type = Some(content_type.to_string()); 87 | self 88 | } 89 | 90 | pub fn with_header(mut self, name: &str, value: &str) -> Self { 91 | self.headers.insert(name.to_lowercase(), value.to_string()); 92 | self 93 | } 94 | 95 | pub fn with_path(mut self, path: &str) -> Self { 96 | self.path = Some(path.to_string()); 97 | self 98 | } 99 | 100 | pub fn with_protobuf_definitions>(mut self, definitions: P) -> Self { 101 | self.protobuf_definitions = Some(definitions.as_ref().to_path_buf()); 102 | self 103 | } 104 | 105 | pub fn with_is_http_request(mut self, is_http_request: bool) -> Self { 106 | self.is_http_request = is_http_request; 107 | self 108 | } 109 | } 110 | 111 | impl Metadata for TestMetadata { 112 | fn content_type(&self) -> Option<&str> { 113 | self.content_type.as_deref() 114 | } 115 | 116 | fn get_header(&self, name: &str) -> Option { 117 | self.headers.get(name).cloned() 118 | } 119 | 120 | fn get_path(&self) -> Option<&str> { 121 | self.path.as_deref() 122 | } 123 | 124 | fn protobuf_definitions(&self) -> Option<&Path> { 125 | self.protobuf_definitions.as_deref() 126 | } 127 | 128 | fn is_http_request(&self) -> bool { 129 | self.is_http_request 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/protobuf/mod.rs: -------------------------------------------------------------------------------- 1 | mod existing_proto_definitions; 2 | mod proto_to_yaml; 3 | mod raw_to_proto; 4 | mod reencode; 5 | mod view_grpc; 6 | mod view_protobuf; 7 | mod yaml_to_pretty; 8 | 9 | pub use view_grpc::GRPC; 10 | pub use view_protobuf::Protobuf; 11 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/protobuf/proto_to_yaml.rs: -------------------------------------------------------------------------------- 1 | use crate::protobuf::view_protobuf::tags; 2 | /// Parsed protobuf message => YAML value 3 | use protobuf::descriptor::field_descriptor_proto::Type; 4 | use protobuf::descriptor::field_descriptor_proto::Type::{ 5 | TYPE_BYTES, TYPE_FIXED32, TYPE_FIXED64, TYPE_UINT64, 6 | }; 7 | use protobuf::reflect::{ReflectFieldRef, ReflectValueRef}; 8 | use protobuf::MessageDyn; 9 | use serde_yaml::value::TaggedValue; 10 | use serde_yaml::{Mapping, Number, Value}; 11 | use std::ops::Deref; 12 | 13 | pub(super) fn message_to_yaml(message: &dyn MessageDyn) -> Value { 14 | let mut ret = Mapping::new(); 15 | 16 | for field in message.descriptor_dyn().fields() { 17 | let is_unknown_field = field.name().starts_with("unknown_field_"); 18 | let key = if is_unknown_field { 19 | Value::from(field.number()) 20 | } else { 21 | Value::from(field.name()) 22 | }; 23 | let field_type = field 24 | .proto() 25 | .type_ 26 | .map(|t| t.enum_value_or(TYPE_BYTES)) 27 | .unwrap_or(TYPE_BYTES); 28 | 29 | let value = match field.get_reflect(message) { 30 | ReflectFieldRef::Optional(x) => { 31 | if let Some(x) = x.value() { 32 | value_to_yaml(x, field_type, is_unknown_field) 33 | } else { 34 | continue; 35 | } 36 | } 37 | ReflectFieldRef::Repeated(x) => { 38 | if x.is_empty() { 39 | continue; 40 | } 41 | Value::Sequence( 42 | x.into_iter() 43 | .map(|x| value_to_yaml(x, field_type, is_unknown_field)) 44 | .collect(), 45 | ) 46 | } 47 | ReflectFieldRef::Map(x) => { 48 | if x.is_empty() { 49 | continue; 50 | } 51 | Value::Mapping( 52 | x.into_iter() 53 | .map(|(k, v)| { 54 | ( 55 | value_to_yaml(k, field_type, is_unknown_field), 56 | value_to_yaml(v, field_type, is_unknown_field), 57 | ) 58 | }) 59 | .collect(), 60 | ) 61 | } 62 | }; 63 | ret.insert(key, value); 64 | } 65 | Value::Mapping(ret) 66 | } 67 | 68 | fn value_to_yaml(x: ReflectValueRef, field_type: Type, is_unknown: bool) -> Value { 69 | match x { 70 | ReflectValueRef::U32(x) => tag_number(Number::from(x), field_type, is_unknown), 71 | ReflectValueRef::U64(x) => tag_number(Number::from(x), field_type, is_unknown), 72 | ReflectValueRef::I32(x) => Value::Number(Number::from(x)), 73 | ReflectValueRef::I64(x) => Value::Number(Number::from(x)), 74 | ReflectValueRef::F32(x) => Value::Number(Number::from(x)), 75 | ReflectValueRef::F64(x) => Value::Number(Number::from(x)), 76 | ReflectValueRef::Bool(x) => Value::from(x), 77 | ReflectValueRef::String(x) => Value::from(x), 78 | ReflectValueRef::Bytes(x) => Value::Tagged(Box::new(TaggedValue { 79 | tag: tags::BINARY.clone(), 80 | value: Value::String(data_encoding::HEXLOWER.encode(x)), 81 | })), 82 | ReflectValueRef::Enum(descriptor, i) => descriptor 83 | .value_by_number(i) 84 | .map(|v| Value::String(v.name().to_string())) 85 | .unwrap_or_else(|| Value::Number(Number::from(i))), 86 | ReflectValueRef::Message(m) => message_to_yaml(m.deref()), 87 | } 88 | } 89 | 90 | fn tag_number(number: Number, field_type: Type, is_unknown: bool) -> Value { 91 | if !is_unknown { 92 | return Value::Number(number); 93 | } 94 | match field_type { 95 | TYPE_UINT64 => Value::Tagged(Box::new(TaggedValue { 96 | tag: tags::VARINT.clone(), 97 | value: Value::Number(number), 98 | })), 99 | TYPE_FIXED64 => Value::Tagged(Box::new(TaggedValue { 100 | tag: tags::FIXED64.clone(), 101 | value: Value::Number(number), 102 | })), 103 | TYPE_FIXED32 => Value::Tagged(Box::new(TaggedValue { 104 | tag: tags::FIXED32.clone(), 105 | value: Value::Number(number), 106 | })), 107 | _ => Value::Number(number), 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/protobuf/yaml_to_pretty.rs: -------------------------------------------------------------------------------- 1 | /// YAML value => prettified text 2 | use crate::protobuf::view_protobuf::tags; 3 | use regex::Captures; 4 | use std::fmt::{Display, Formatter}; 5 | 6 | /// Collect all representations of a number and output the "best" one as the YAML value 7 | /// and the rest as comments. 8 | struct NumReprs(Vec<(&'static str, String)>); 9 | 10 | impl NumReprs { 11 | fn new(k: &'static str, v: impl ToString) -> Self { 12 | let mut inst = Self(Vec::with_capacity(3)); 13 | inst.push(k, v); 14 | inst 15 | } 16 | fn push(&mut self, k: &'static str, v: impl ToString) { 17 | self.0.push((k, v.to_string())); 18 | } 19 | } 20 | 21 | impl Display for NumReprs { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 23 | // We first sort by t.len(), which is a hack to make sure that sint is not used 24 | // as the main representation. 25 | let (min_typ, min_val) = self 26 | .0 27 | .iter() 28 | .min_by_key(|(t, v)| (t.len(), v.len())) 29 | .unwrap(); 30 | let mut i = self.0.iter().filter(|(t, _)| t != min_typ); 31 | 32 | write!(f, "{}", min_val)?; 33 | if let Some((t, v)) = i.next() { 34 | write!(f, " # {}: {}", t, v)?; 35 | } 36 | for (t, v) in i { 37 | write!(f, ", {}: {}", t, v)?; 38 | } 39 | Ok(()) 40 | } 41 | } 42 | 43 | // Helper method to apply regex replacements to the YAML output 44 | pub(super) fn apply_replacements(yaml_str: &str) -> anyhow::Result { 45 | // Replace !fixed32 tags with comments showing float and i32 interpretations 46 | let with_fixed32 = tags::FIXED32_RE.replace_all(yaml_str, |caps: &Captures| { 47 | let value = caps[1].parse::().unwrap_or_default(); 48 | let mut repr = NumReprs::new("u32", value); 49 | 50 | let float_value = f32::from_bits(value); 51 | if !float_value.is_nan() && float_value.abs() > 0.0000001 { 52 | repr.push("f32", format_float(float_value)); 53 | } 54 | 55 | if value.leading_zeros() == 0 { 56 | repr.push("i32", value as i32); 57 | } 58 | format!("{} {}", *tags::FIXED32, repr) 59 | }); 60 | 61 | // Replace !fixed64 tags with comments showing double and i64 interpretations 62 | let with_fixed64 = tags::FIXED64_RE.replace_all(&with_fixed32, |caps: &Captures| { 63 | let value = caps[1].parse::().unwrap_or_default(); 64 | let mut repr = NumReprs::new("u64", value); 65 | 66 | let double_value = f64::from_bits(value); 67 | if !double_value.is_nan() && double_value.abs() > 0.0000001 { 68 | repr.push("f64", format_float(double_value)); 69 | } 70 | 71 | if value.leading_zeros() == 0 { 72 | repr.push("i64", value as i64); 73 | } 74 | format!("{} {}", *tags::FIXED64, repr) 75 | }); 76 | 77 | // Replace !varint tags with comments showing signed interpretation if different 78 | let with_varint = tags::VARINT_RE.replace_all(&with_fixed64, |caps: &Captures| { 79 | let value = caps[1].parse::().unwrap_or_default(); 80 | let mut repr = NumReprs::new("u64", value); 81 | 82 | if value.leading_zeros() == 0 { 83 | repr.push("i64", value as i64); 84 | // We only show u64 and i64 reprs if the leading bit is a 1. 85 | // It could technically be zigzag, but the odds are quite low. 86 | } else { 87 | repr.push("!sint", decode_zigzag64(value)); 88 | } 89 | 90 | repr.to_string() 91 | }); 92 | 93 | Ok(with_varint.to_string()) 94 | } 95 | 96 | /// Ensure that floating point numbers have a ".0" component so that we roundtrip. 97 | fn format_float(val: T) -> String { 98 | let mut ret = format!("{:.}", val); 99 | if !ret.contains(".") { 100 | ret.push_str(".0"); 101 | } 102 | ret 103 | } 104 | 105 | // Decode a zigzag-encoded 64-bit integer 106 | fn decode_zigzag64(n: u64) -> i64 { 107 | ((n >> 1) as i64) ^ (-((n & 1) as i64)) 108 | } 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use super::*; 113 | 114 | #[test] 115 | fn test_format_float() { 116 | assert_eq!(format_float(1.2345), "1.2345"); 117 | assert_eq!(format_float(0f32), "0.0"); 118 | assert_eq!(format_float(-1f64), "-1.0"); 119 | } 120 | 121 | #[test] 122 | fn test_decode_zigzag64() { 123 | assert_eq!(decode_zigzag64(0), 0); 124 | assert_eq!(decode_zigzag64(1), -1); 125 | assert_eq!(decode_zigzag64(2), 1); 126 | assert_eq!(decode_zigzag64(3), -2); 127 | assert_eq!(decode_zigzag64(0xfffffffe), 0x7fffffff); 128 | assert_eq!(decode_zigzag64(0xffffffff), -0x80000000); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/src/test_inspect_metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::test::TestMetadata; 2 | use crate::{Metadata, Prettify}; 3 | use anyhow::Context; 4 | use std::collections::HashMap; 5 | use std::path::Path; 6 | 7 | /// Contentview used for internal testing to ensure that the 8 | /// Python accessors in mitmproxy-rs all work properly. 9 | pub struct TestInspectMetadata; 10 | 11 | impl Prettify for TestInspectMetadata { 12 | fn name(&self) -> &'static str { 13 | "Inspect Metadata (test only)" 14 | } 15 | 16 | fn instance_name(&self) -> String { 17 | "_test_inspect_metadata".to_string() 18 | } 19 | 20 | fn prettify(&self, _data: &[u8], metadata: &dyn Metadata) -> anyhow::Result { 21 | let mut headers = HashMap::new(); 22 | if let Some(host) = metadata.get_header("host") { 23 | headers.insert("host".to_string(), host); 24 | } 25 | let meta = TestMetadata { 26 | content_type: metadata.content_type().map(str::to_string), 27 | headers, 28 | path: metadata.get_path().map(str::to_string), 29 | is_http_request: metadata.is_http_request(), 30 | protobuf_definitions: metadata.protobuf_definitions().map(Path::to_path_buf), 31 | }; 32 | // JSON would be nicer to consume on the Python side, 33 | // but let's not add dependencies for this. 34 | serde_yaml::to_string(&meta).context("Failed to convert to YAML") 35 | } 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | 42 | #[test] 43 | fn prettify_simple() { 44 | let result = TestInspectMetadata 45 | .prettify(b"", &TestMetadata::default()) 46 | .unwrap(); 47 | assert_eq!( 48 | result, 49 | "content_type: null\nheaders: {}\nprotobuf_definitions: null\npath: null\nis_http_request: false\n" 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/testdata/protobuf/mismatch.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message TestMessage { 4 | string example = 1; 5 | } 6 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/testdata/protobuf/nested.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example.nested; 4 | 5 | service Service { 6 | rpc Method(Empty) returns (Response) {} 7 | } 8 | 9 | message Empty {} 10 | 11 | message Response { 12 | message Nested { 13 | int32 example = 1; 14 | } 15 | int32 example = 1; 16 | Nested nested = 2; 17 | } 18 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/testdata/protobuf/simple.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message TestMessage { 4 | int32 example = 1; 5 | } 6 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/testdata/protobuf/simple_package.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package example.simple; 4 | 5 | service Other { 6 | rpc Method(Empty) returns (Response) {} 7 | } 8 | 9 | service Service { 10 | // This endpoint 11 | rpc Method(Empty) returns (Response) {} 12 | } 13 | 14 | message Empty {} 15 | 16 | message Response { 17 | int32 example = 1; 18 | } 19 | -------------------------------------------------------------------------------- /mitmproxy-contentviews/testdata/protobuf/simple_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service Other { 4 | rpc Method(Empty) returns (Response) {} 5 | } 6 | 7 | service Service { 8 | // This endpoint 9 | rpc Method(Empty) returns (Response) {} 10 | } 11 | 12 | message Empty {} 13 | 14 | message Response { 15 | int32 example = 1; 16 | } 17 | -------------------------------------------------------------------------------- /mitmproxy-highlight/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy-highlight" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [dependencies] 15 | anyhow = { version = "1.0.97", features = ["backtrace"] } 16 | tree-sitter = "0.25.5" 17 | tree-sitter-css = "0.23.2" 18 | tree-sitter-highlight = "0.25.5" 19 | tree-sitter-javascript = "0.23.1" 20 | tree-sitter-xml = "0.7.0" 21 | tree-sitter-yaml = "0.7.1" 22 | 23 | [dev-dependencies] 24 | criterion = "0.6.0" 25 | 26 | [[bench]] 27 | name = "syntax_highlight" 28 | harness = false 29 | -------------------------------------------------------------------------------- /mitmproxy-highlight/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-highlight 2 | 3 | This crate contains the syntax highlighting backend for mitmproxy and mitmdump, 4 | utilizing [tree-sitter]. 5 | This functionality is exposed to Python via [mitmproxy-rs/src/syntax_highlight.rs]. 6 | 7 | [tree-sitter]: https://tree-sitter.github.io/tree-sitter/ 8 | [mitmproxy-rs/src/syntax_highlight.rs]: ../mitmproxy-rs/src/syntax_highlight.rs -------------------------------------------------------------------------------- /mitmproxy-highlight/benches/syntax_highlight.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use mitmproxy_highlight::Language; 3 | use std::hint::black_box; 4 | 5 | fn criterion_benchmark(c: &mut Criterion) { 6 | c.bench_function("syntax_highlight small", |b| { 7 | b.iter(|| { 8 | Language::Xml 9 | .highlight(black_box( 10 | br#" 11 | 12 | 13 | 14 | 15 | 16 | Bootstrap demo 17 | 18 | 19 |

Hello, world!

20 | 21 | "#, 22 | )) 23 | .unwrap() 24 | }) 25 | }); 26 | 27 | let data = "x".repeat(8096); 28 | c.bench_function("syntax_highlight xml", |b| { 29 | b.iter(|| Language::Xml.highlight(black_box(data.as_bytes())).unwrap()) 30 | }); 31 | 32 | // tree_sitter_html is faster, but not by orders of magnitude. 33 | /* 34 | let mut config = HighlightConfiguration::new( 35 | tree_sitter_html::LANGUAGE.into(), 36 | "", 37 | tree_sitter_html::HIGHLIGHTS_QUERY, 38 | "", 39 | "" 40 | ).unwrap(); 41 | let names = config.names().iter().map(|x| x.to_string()).collect::>(); 42 | let tags = names.iter().map(|_| Tag::Text).collect::>(); 43 | config.configure(&names); 44 | 45 | c.bench_function("syntax_highlight html", |b| { 46 | b.iter(|| { 47 | common::highlight( 48 | &config, 49 | &tags, 50 | data.as_bytes(), 51 | ) 52 | }) 53 | }); 54 | */ 55 | } 56 | 57 | criterion_group!(benches, criterion_benchmark); 58 | criterion_main!(benches); 59 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/common.rs: -------------------------------------------------------------------------------- 1 | use super::{Chunk, Tag}; 2 | use anyhow::{Context, Result}; 3 | use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter}; 4 | 5 | pub fn highlight( 6 | config: &HighlightConfiguration, 7 | tags: &[Tag], 8 | input: &[u8], 9 | ) -> Result> { 10 | let mut highlighter = Highlighter::new(); 11 | let highlights = highlighter 12 | .highlight(config, input, None, |_| None) 13 | .context("failed to highlight")?; 14 | 15 | let mut chunks: Vec = Vec::new(); 16 | let mut tag: Tag = Tag::Text; 17 | 18 | for event in highlights { 19 | let event = event.context("highlighter failure")?; 20 | match event { 21 | HighlightEvent::Source { start, end } => { 22 | let contents = String::from_utf8_lossy(&input[start..end]); 23 | match chunks.last_mut() { 24 | Some(x) if x.0 == tag || contents.trim_ascii().is_empty() => { 25 | x.1.push_str(&contents); 26 | } 27 | _ => chunks.push((tag, contents.to_string())), 28 | } 29 | } 30 | HighlightEvent::HighlightStart(s) => { 31 | tag = tags[s.0]; 32 | } 33 | HighlightEvent::HighlightEnd => { 34 | tag = Tag::Text; 35 | } 36 | } 37 | } 38 | Ok(chunks) 39 | } 40 | 41 | #[cfg(test)] 42 | pub(super) fn test_tags_ok( 43 | language: tree_sitter::Language, 44 | highlights_query: &str, 45 | names: &[&str], 46 | tags: &[Tag], 47 | ) { 48 | assert_eq!(names.len(), tags.len()); 49 | let config = HighlightConfiguration::new(language, "", highlights_query, "", "").unwrap(); 50 | for &tag in names { 51 | assert!( 52 | config.names().iter().any(|name| name.contains(tag)), 53 | "Invalid tag: {},\nAllowed tags: {:?}", 54 | tag, 55 | config.names() 56 | ); 57 | } 58 | } 59 | 60 | #[allow(unused)] 61 | #[cfg(test)] 62 | pub(super) fn debug(language: tree_sitter::Language, highlights_query: &str, input: &[u8]) { 63 | let mut highlighter = Highlighter::new(); 64 | let mut config = HighlightConfiguration::new(language, "", highlights_query, "", "").unwrap(); 65 | let names = config 66 | .names() 67 | .iter() 68 | .map(|name| name.to_string()) 69 | .collect::>(); 70 | config.configure(&names); 71 | let highlights = highlighter 72 | .highlight(&config, input, None, |_| None) 73 | .unwrap(); 74 | 75 | let mut tag: &str = ""; 76 | for event in highlights { 77 | match event.unwrap() { 78 | HighlightEvent::Source { start, end } => { 79 | let contents = &input[start..end]; 80 | println!( 81 | "{}: {:?}", 82 | tag, 83 | String::from_utf8_lossy(contents).to_string().as_str() 84 | ); 85 | } 86 | HighlightEvent::HighlightStart(s) => { 87 | tag = &names[s.0]; 88 | } 89 | HighlightEvent::HighlightEnd => { 90 | tag = ""; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/css.rs: -------------------------------------------------------------------------------- 1 | use super::{common, Chunk, Tag}; 2 | use anyhow::Result; 3 | use std::sync::LazyLock; 4 | use tree_sitter_css::HIGHLIGHTS_QUERY; 5 | use tree_sitter_css::LANGUAGE; 6 | use tree_sitter_highlight::HighlightConfiguration; 7 | 8 | const NAMES: &[&str] = &[ 9 | "tag", // body 10 | "property", // font-size 11 | "variable", // --foo-bar 12 | "function", // calc() 13 | "number", // 42 14 | "string", // "foo" 15 | "comment", // /* comment */ 16 | ]; 17 | const TAGS: &[Tag] = &[ 18 | Tag::Name, 19 | Tag::Boolean, // we only have one "Name", so this is a workaround. 20 | Tag::Text, 21 | Tag::Text, 22 | Tag::Number, 23 | Tag::String, 24 | Tag::Comment, 25 | ]; 26 | 27 | static CONFIG: LazyLock = LazyLock::new(|| { 28 | let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") 29 | .expect("failed to build syntax highlighter"); 30 | config.configure(NAMES); 31 | config 32 | }); 33 | 34 | pub fn highlight(input: &[u8]) -> Result> { 35 | common::highlight(&CONFIG, TAGS, input) 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | 42 | #[ignore] 43 | #[test] 44 | fn debug() { 45 | common::debug( 46 | LANGUAGE.into(), 47 | HIGHLIGHTS_QUERY, 48 | b"p > span { color: red; font-size: 42px; content: \"foo\"; margin: var(--foo) } /* foo */", 49 | ); 50 | } 51 | 52 | #[test] 53 | fn test_tags_ok() { 54 | common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); 55 | } 56 | 57 | #[test] 58 | fn test_highlight() { 59 | let input = b"\ 60 | p > span { \n\ 61 | color: red;\n\ 62 | font-size: 42px;\n\ 63 | content: \"foo\";\n\ 64 | margin: var(--foo);\n\ 65 | }\n\ 66 | /* foo */\n\ 67 | "; 68 | let chunks = highlight(input).unwrap(); 69 | assert_eq!( 70 | chunks, 71 | vec![ 72 | (Tag::Name, "p".to_string()), 73 | (Tag::Text, " > ".to_string()), 74 | (Tag::Name, "span".to_string()), 75 | (Tag::Text, " { \n".to_string()), 76 | (Tag::Boolean, "color".to_string()), 77 | (Tag::Text, ": red;\n".to_string()), 78 | (Tag::Boolean, "font-size".to_string()), 79 | (Tag::Text, ": ".to_string()), 80 | (Tag::Number, "42px".to_string()), 81 | (Tag::Text, ";\n".to_string()), 82 | (Tag::Boolean, "content".to_string()), 83 | (Tag::Text, ": ".to_string()), 84 | (Tag::String, "\"foo\"".to_string()), 85 | (Tag::Text, ";\n".to_string()), 86 | (Tag::Boolean, "margin".to_string()), 87 | (Tag::Text, ": var(--foo);\n}\n".to_string()), 88 | (Tag::Comment, "/* foo */\n".to_string()), 89 | ] 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/javascript.rs: -------------------------------------------------------------------------------- 1 | use super::{common, Chunk, Tag}; 2 | use anyhow::Result; 3 | use std::sync::LazyLock; 4 | use tree_sitter_highlight::HighlightConfiguration; 5 | use tree_sitter_javascript::HIGHLIGHT_QUERY as HIGHLIGHTS_QUERY; 6 | use tree_sitter_javascript::LANGUAGE; 7 | 8 | const NAMES: &[&str] = &[ 9 | "keyword", // let 10 | "function", // *function* () { 11 | "variable", // let *foo* = ... 12 | "property", // foo.*bar* = ... 13 | "constant", // *true* 14 | "string", // "string" 15 | "number", // 42 16 | "comment", // /* comments */ 17 | ]; 18 | const TAGS: &[Tag] = &[ 19 | Tag::Name, 20 | Tag::Text, 21 | Tag::Text, 22 | Tag::Text, 23 | Tag::Boolean, 24 | Tag::String, 25 | Tag::Number, 26 | Tag::Comment, 27 | ]; 28 | 29 | static CONFIG: LazyLock = LazyLock::new(|| { 30 | let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") 31 | .expect("failed to build syntax highlighter"); 32 | config.configure(NAMES); 33 | config 34 | }); 35 | 36 | pub fn highlight(input: &[u8]) -> Result> { 37 | common::highlight(&CONFIG, TAGS, input) 38 | } 39 | 40 | #[cfg(test)] 41 | mod tests { 42 | use super::*; 43 | 44 | #[ignore] 45 | #[test] 46 | fn debug() { 47 | common::debug( 48 | LANGUAGE.into(), 49 | HIGHLIGHTS_QUERY, 50 | b"function foo() { let bar = true && 42 && 'qux'; foo.bar = 42; } // comment", 51 | ); 52 | } 53 | 54 | #[test] 55 | fn test_tags_ok() { 56 | common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); 57 | } 58 | 59 | #[test] 60 | fn test_highlight() { 61 | let input = b"\ 62 | function foo() {\n\ 63 | let bar = true && 42 && 'qux';\n\ 64 | } // comment\n\ 65 | "; 66 | let chunks = highlight(input).unwrap(); 67 | assert_eq!( 68 | chunks, 69 | vec![ 70 | (Tag::Name, "function ".to_string()), 71 | (Tag::Text, "foo() {\n".to_string()), 72 | (Tag::Name, "let ".to_string()), 73 | (Tag::Text, "bar = ".to_string()), 74 | (Tag::Boolean, "true".to_string()), 75 | (Tag::Text, " && ".to_string()), 76 | (Tag::Number, "42".to_string()), 77 | (Tag::Text, " && ".to_string()), 78 | (Tag::String, "'qux'".to_string()), 79 | (Tag::Text, ";\n} ".to_string()), 80 | (Tag::Comment, "// comment\n".to_string()), 81 | ] 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::bail; 2 | use std::str::FromStr; 3 | 4 | pub mod common; 5 | mod css; 6 | mod javascript; 7 | mod xml; 8 | mod yaml; 9 | 10 | pub type Chunk = (Tag, String); 11 | 12 | pub enum Language { 13 | Css, 14 | JavaScript, 15 | Xml, 16 | Yaml, 17 | None, 18 | Error, 19 | } 20 | 21 | impl Language { 22 | pub fn highlight(&self, input: &[u8]) -> anyhow::Result> { 23 | match self { 24 | Language::Css => css::highlight(input), 25 | Language::JavaScript => javascript::highlight(input), 26 | Language::Xml => xml::highlight(input), 27 | Language::Yaml => yaml::highlight(input), 28 | Language::None => Ok(vec![( 29 | Tag::Text, 30 | String::from_utf8_lossy(input).to_string(), 31 | )]), 32 | Language::Error => Ok(vec![( 33 | Tag::Error, 34 | String::from_utf8_lossy(input).to_string(), 35 | )]), 36 | } 37 | } 38 | 39 | pub const VALUES: [Self; 6] = [ 40 | Self::Css, 41 | Self::JavaScript, 42 | Self::Xml, 43 | Self::Yaml, 44 | Self::None, 45 | Self::Error, 46 | ]; 47 | 48 | pub const fn as_str(&self) -> &'static str { 49 | match self { 50 | Self::Css => "css", 51 | Self::JavaScript => "javascript", 52 | Self::Xml => "xml", 53 | Self::Yaml => "yaml", 54 | Self::None => "none", 55 | Self::Error => "error", 56 | } 57 | } 58 | } 59 | 60 | impl FromStr for Language { 61 | type Err = anyhow::Error; 62 | 63 | fn from_str(s: &str) -> Result { 64 | Ok(match s { 65 | "css" => Language::Css, 66 | "javascript" => Language::JavaScript, 67 | "xml" => Language::Xml, 68 | "yaml" => Language::Yaml, 69 | "none" => Language::None, 70 | "error" => Language::Error, 71 | other => bail!("Unsupported language: {other}"), 72 | }) 73 | } 74 | } 75 | 76 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] 77 | pub enum Tag { 78 | Text, // Text that shouldn't be emphasized. 79 | Name, // A tag, such as an HTML tag or a YAML key. 80 | String, // A string value. 81 | Number, // A number value. 82 | Boolean, // A boolean value. 83 | Comment, // A comment. 84 | Error, // An error value. 85 | } 86 | 87 | impl Tag { 88 | pub const VALUES: [Self; 7] = [ 89 | Self::Text, 90 | Self::Name, 91 | Self::String, 92 | Self::Number, 93 | Self::Boolean, 94 | Self::Comment, 95 | Self::Error, 96 | ]; 97 | 98 | pub fn as_str(&self) -> &'static str { 99 | match self { 100 | Tag::Text => "", 101 | Tag::Name => "name", 102 | Tag::String => "string", 103 | Tag::Number => "number", 104 | Tag::Boolean => "boolean", 105 | Tag::Comment => "comment", 106 | Tag::Error => "error", 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/xml.rs: -------------------------------------------------------------------------------- 1 | use super::common; 2 | use super::{Chunk, Tag}; 3 | use anyhow::Result; 4 | use std::sync::LazyLock; 5 | use tree_sitter_highlight::HighlightConfiguration; 6 | use tree_sitter_xml::{LANGUAGE_XML as LANGUAGE, XML_HIGHLIGHT_QUERY as HIGHLIGHTS_QUERY}; 7 | 8 | const NAMES: &[&str] = &[ 9 | "tag", //
10 | "property", // class or style 11 | "operator", // equal sign between class and value 12 | "comment", // 13 | "punctuation", 14 | "markup", 15 | ]; 16 | const TAGS: &[Tag] = &[ 17 | Tag::Name, //
18 | Tag::Name, // class or style 19 | Tag::Name, // equal sign between class and value 20 | Tag::Comment, // 21 | Tag::Name, // punctuation 22 | Tag::Text, // markup 23 | ]; 24 | 25 | static CONFIG: LazyLock = LazyLock::new(|| { 26 | let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") 27 | .expect("failed to build syntax highlighter"); 28 | config.configure(NAMES); 29 | config 30 | }); 31 | 32 | pub fn highlight(input: &[u8]) -> Result> { 33 | common::highlight(&CONFIG, TAGS, input) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[ignore] 41 | #[test] 42 | fn debug() { 43 | common::debug( 44 | LANGUAGE.into(), 45 | HIGHLIGHTS_QUERY, 46 | b"
Hello
", 47 | ); 48 | } 49 | 50 | #[test] 51 | fn test_tags_ok() { 52 | common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); 53 | } 54 | 55 | #[test] 56 | fn test_highlight() { 57 | let input = b"
Hello
"; 58 | let chunks = highlight(input).unwrap(); 59 | assert_eq!( 60 | chunks, 61 | vec![ 62 | (Tag::Name, "
".to_string()), 65 | (Tag::Text, "Hello".to_string()), 66 | (Tag::Name, "
".to_string()), 67 | (Tag::Comment, "".to_string()) 68 | ] 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mitmproxy-highlight/src/yaml.rs: -------------------------------------------------------------------------------- 1 | use super::common; 2 | use super::{Chunk, Tag}; 3 | use anyhow::Result; 4 | use std::sync::LazyLock; 5 | use tree_sitter_highlight::HighlightConfiguration; 6 | use tree_sitter_yaml::{HIGHLIGHTS_QUERY, LANGUAGE}; 7 | 8 | const NAMES: &[&str] = &[ 9 | "boolean", // YAML booleans 10 | "string", // YAML strings 11 | "number", // YAML numbers 12 | "comment", // # comment 13 | "type", // !fixed32 type annotations 14 | "property", // key: 15 | ]; 16 | const TAGS: &[Tag] = &[ 17 | Tag::Boolean, 18 | Tag::String, 19 | Tag::Number, 20 | Tag::Comment, 21 | Tag::Name, 22 | Tag::Name, 23 | ]; 24 | 25 | static CONFIG: LazyLock = LazyLock::new(|| { 26 | let mut config = HighlightConfiguration::new(LANGUAGE.into(), "", HIGHLIGHTS_QUERY, "", "") 27 | .expect("failed to build syntax highlighter"); 28 | config.configure(NAMES); 29 | config 30 | }); 31 | 32 | pub fn highlight(input: &[u8]) -> Result> { 33 | common::highlight(&CONFIG, TAGS, input) 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | 40 | #[test] 41 | fn test_tags_ok() { 42 | common::test_tags_ok(LANGUAGE.into(), HIGHLIGHTS_QUERY, NAMES, TAGS); 43 | } 44 | 45 | #[test] 46 | fn test_highlight() { 47 | let input = b"\ 48 | string: \"value\"\n\ 49 | bool: true\n\ 50 | number: !fixed32 42 # comment\n\ 51 | "; 52 | let chunks = highlight(input).unwrap(); 53 | assert_eq!( 54 | chunks, 55 | vec![ 56 | (Tag::Name, "string".to_string()), 57 | (Tag::Text, ": ".to_string()), 58 | (Tag::String, "\"value\"\n".to_string()), 59 | (Tag::Name, "bool".to_string()), 60 | (Tag::Text, ": ".to_string()), 61 | (Tag::Boolean, "true\n".to_string()), 62 | (Tag::Name, "number".to_string()), 63 | (Tag::Text, ": ".to_string()), 64 | (Tag::Name, "!fixed32 ".to_string()), 65 | (Tag::Number, "42 ".to_string()), 66 | (Tag::Comment, "# comment\n".to_string()), 67 | ] 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy-linux-ebpf-common" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | # aya-ebpf currently does not compile on Windows. 12 | [target.'cfg(target_os = "linux")'.dependencies] 13 | aya-ebpf = { workspace = true } 14 | 15 | [lib] 16 | path = "src/lib.rs" 17 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | // aya-ebpf currently does not compile on Windows. 4 | #[cfg(target_os = "linux")] 5 | use aya_ebpf::TASK_COMM_LEN; 6 | #[cfg(not(target_os = "linux"))] 7 | const TASK_COMM_LEN: usize = 16; 8 | 9 | type Pid = u32; 10 | 11 | pub const INTERCEPT_CONF_LEN: u32 = 20; 12 | 13 | #[derive(Copy, Clone, Debug)] 14 | #[repr(C)] 15 | pub enum Pattern { 16 | Pid(Pid), 17 | Process([u8; TASK_COMM_LEN]), 18 | } 19 | 20 | #[derive(Copy, Clone, Debug)] 21 | #[repr(C)] 22 | pub enum Action { 23 | None, 24 | Include(Pattern), 25 | Exclude(Pattern), 26 | } 27 | 28 | impl Pattern { 29 | pub fn matches(&self, command: Option<&[u8; TASK_COMM_LEN]>, pid: Pid) -> bool { 30 | match self { 31 | Pattern::Pid(p) => pid == *p, 32 | Pattern::Process(process) => { 33 | let Some(command) = command else { 34 | return false; 35 | }; 36 | // `command == process` inexplicably causes BPF verifier errors on Ubuntu 22.04, 37 | // (it works on 24.04+), so we do a manual strcmp dance. 38 | for i in 0..16 { 39 | let curr = command[i]; 40 | if curr != process[i] { 41 | return false; 42 | } 43 | if curr == 0 { 44 | break; 45 | } 46 | } 47 | true 48 | } 49 | } 50 | } 51 | } 52 | 53 | impl From<&str> for Action { 54 | fn from(value: &str) -> Self { 55 | let value = value.trim(); 56 | if let Some(value) = value.strip_prefix('!') { 57 | Action::Exclude(Pattern::from(value)) 58 | } else { 59 | Action::Include(Pattern::from(value)) 60 | } 61 | } 62 | } 63 | 64 | impl From<&str> for Pattern { 65 | fn from(value: &str) -> Self { 66 | let value = value.trim(); 67 | match value.parse::() { 68 | Ok(pid) => Pattern::Pid(pid), 69 | Err(_) => { 70 | let mut val = [0u8; TASK_COMM_LEN]; 71 | let src = value.as_bytes(); 72 | // This silently truncates to TASK_COMM_LEN - 1 bytes, 73 | // bpf_get_current_comm always puts a null byte at the end. 74 | let len = core::cmp::min(TASK_COMM_LEN - 1, src.len()); 75 | val[..len].copy_from_slice(&src[..len]); 76 | Pattern::Process(val) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # We have this so that one doesn't need to manually pass 2 | # --target=bpfel-unknown-none -Z build-std=core when running cargo 3 | # check/build/doc etc. 4 | # 5 | # NB: this file gets loaded only if you run cargo from this directory, it's 6 | # ignored if you run from the workspace root. See 7 | # https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure 8 | [build] 9 | target = ["bpfeb-unknown-none", "bpfel-unknown-none"] 10 | 11 | [unstable] 12 | build-std = ["core"] 13 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy-linux-ebpf" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [dependencies] 12 | mitmproxy-linux-ebpf-common = { path = "../mitmproxy-linux-ebpf-common" } 13 | 14 | aya-ebpf = { workspace = true } 15 | aya-log-ebpf = { workspace = true } 16 | 17 | [build-dependencies] 18 | which = "7.0.2" 19 | 20 | [[bin]] 21 | name = "mitmproxy-linux" 22 | path = "src/main.rs" 23 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/build.rs: -------------------------------------------------------------------------------- 1 | use which::which; 2 | 3 | /// Building this crate has an undeclared dependency on the `bpf-linker` binary. This would be 4 | /// better expressed by [artifact-dependencies][bindeps] but issues such as 5 | /// https://github.com/rust-lang/cargo/issues/12385 make their use impractical for the time being. 6 | /// 7 | /// This file implements an imperfect solution: it causes cargo to rebuild the crate whenever the 8 | /// mtime of `which bpf-linker` changes. Note that possibility that a new bpf-linker is added to 9 | /// $PATH ahead of the one used as the cache key still exists. Solving this in the general case 10 | /// would require rebuild-if-changed-env=PATH *and* rebuild-if-changed={every-directory-in-PATH} 11 | /// which would likely mean far too much cache invalidation. 12 | /// 13 | /// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies 14 | fn main() { 15 | let bpf_linker = which("bpf-linker").expect( 16 | "Failed to find `bpf-linker` executable on PATH. \ 17 | Run `cargo install --locked bpf-linker` to install.", 18 | ); 19 | println!("cargo:rerun-if-changed={}", bpf_linker.to_str().unwrap()); 20 | } 21 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rust-src"] 4 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | // This file exists to enable the library target. 4 | -------------------------------------------------------------------------------- /mitmproxy-linux-ebpf/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use aya_ebpf::macros::{cgroup_sock, map}; 5 | use aya_ebpf::maps::Array; 6 | use aya_ebpf::programs::SockContext; 7 | use aya_ebpf::EbpfContext; 8 | use aya_log_ebpf::debug; 9 | use mitmproxy_linux_ebpf_common::{Action, INTERCEPT_CONF_LEN}; 10 | 11 | #[no_mangle] 12 | static INTERFACE_ID: u32 = 0; 13 | 14 | #[map] 15 | static INTERCEPT_CONF: Array = Array::with_max_entries(INTERCEPT_CONF_LEN, 0); 16 | 17 | #[cgroup_sock(sock_create)] 18 | pub fn cgroup_sock_create(ctx: SockContext) -> i32 { 19 | if should_intercept(&ctx) { 20 | debug!(&ctx, "intercepting in sock_create"); 21 | let interface_id = unsafe { core::ptr::read_volatile(&INTERFACE_ID) }; 22 | unsafe { 23 | (*ctx.sock).bound_dev_if = interface_id; 24 | } 25 | } 26 | 1 27 | } 28 | 29 | pub fn should_intercept(ctx: &SockContext) -> bool { 30 | let command = ctx.command().ok(); 31 | let pid = ctx.pid(); 32 | 33 | let mut intercept = matches!(INTERCEPT_CONF.get(0), Some(Action::Exclude(_))); 34 | for i in 0..INTERCEPT_CONF_LEN { 35 | match INTERCEPT_CONF.get(i) { 36 | Some(Action::Include(pattern)) => { 37 | intercept = intercept || pattern.matches(command.as_ref(), pid); 38 | } 39 | Some(Action::Exclude(pattern)) => { 40 | intercept = intercept && !pattern.matches(command.as_ref(), pid); 41 | } 42 | _ => { 43 | break; 44 | } 45 | } 46 | } 47 | intercept 48 | } 49 | 50 | #[cfg(not(test))] 51 | #[panic_handler] 52 | fn panic(_info: &core::panic::PanicInfo) -> ! { 53 | loop {} 54 | } 55 | -------------------------------------------------------------------------------- /mitmproxy-linux/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy-linux" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [[bin]] 15 | name = "mitmproxy-linux-redirector" 16 | path = "src/main.rs" 17 | 18 | [target.'cfg(target_os = "linux")'.dependencies] 19 | mitmproxy = { path = "../" } 20 | mitmproxy-linux-ebpf-common = { path = "../mitmproxy-linux-ebpf-common"} 21 | tun = { workspace = true, features = ["async"] } 22 | aya = { workspace = true } 23 | aya-log = { workspace = true } 24 | tokio = { version = "1.45", features = ["macros", "net", "rt-multi-thread", "sync", "io-util", "signal"] } 25 | anyhow = { version = "1.0.97", features = ["backtrace"] } 26 | log = "0.4.27" 27 | env_logger = "0.11.5" 28 | prost = "0.13.5" 29 | internet-packet = { version = "0.2.0", features = ["checksums"] } 30 | libc = "0.2.170" 31 | const-sha1 = "0.3.0" 32 | 33 | [target.'cfg(target_os = "linux")'.build-dependencies] 34 | anyhow = { version = "1.0.97", features = ["backtrace"] } 35 | aya-build = "0.1.2" 36 | mitmproxy-linux-ebpf = { path = "../mitmproxy-linux-ebpf" } 37 | 38 | [target.'cfg(target_os = "linux")'.dev-dependencies] 39 | hex = "0.4.3" 40 | 41 | [features] 42 | root-tests = [] 43 | -------------------------------------------------------------------------------- /mitmproxy-linux/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-linux 2 | 3 | This package contains the following precompiled binaries for Linux: 4 | 5 | - `mitmproxy-linux-redirector`: A Rust executable that redirects traffic to mitmproxy via eBPF. 6 | 7 | 8 | ## Build Dependencies 9 | 10 | This package requires the following software to build (via https://aya-rs.dev/book/start/development/#prerequisites): 11 | 12 | - Rust nightly. 13 | - [bpf-linker]: `cargo install --locked bpf-linker` 14 | 15 | ## Redirector Development Setup 16 | 17 | 1. Install build dependencies (see above). 18 | 2. Install mitmproxy_linux as editable: `pip install -e .` 19 | 3. Remove `$VIRTUAL_ENV/bin/mitmproxy-linux-redirector` 20 | 4. Run something along the lines of `mitmdump --mode local:curl`. 21 | You should see a `Development mode: Compiling mitmproxy-linux-redirector...` message. 22 | 23 | 24 | [bpf-linker]: https://github.com/aya-rs/bpf-linker 25 | -------------------------------------------------------------------------------- /mitmproxy-linux/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | use anyhow::{anyhow, Context as _}; 3 | 4 | #[cfg(target_os = "linux")] 5 | use aya_build::cargo_metadata; 6 | 7 | #[cfg(not(target_os = "linux"))] 8 | fn main() {} 9 | 10 | /// Based on https://github.com/aya-rs/aya-template/blob/main/%7B%7Bproject-name%7D%7D/build.rs 11 | #[cfg(target_os = "linux")] 12 | fn main() -> anyhow::Result<()> { 13 | let cargo_metadata::Metadata { packages, .. } = cargo_metadata::MetadataCommand::new() 14 | .no_deps() 15 | .exec() 16 | .context("MetadataCommand::exec")?; 17 | let ebpf_package = packages 18 | .into_iter() 19 | .find(|cargo_metadata::Package { name, .. }| name == "mitmproxy-linux-ebpf") 20 | .ok_or_else(|| anyhow!("mitmproxy-linux-ebpf package not found"))?; 21 | aya_build::build_ebpf([ebpf_package]) 22 | } 23 | -------------------------------------------------------------------------------- /mitmproxy-linux/mitmproxy_linux/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import sysconfig 3 | from pathlib import Path 4 | 5 | 6 | def executable_path() -> Path: 7 | """ 8 | Return the Path for mitmproxy-linux-redirector. 9 | 10 | For PyInstaller binaries this is the bundled executable, 11 | for wheels this is the file in the package, 12 | for development setups this may invoke cargo to build it. 13 | """ 14 | 15 | if getattr(sys, 'frozen', False) and (pyinstaller_dir := getattr(sys, '_MEIPASS')): 16 | return Path(pyinstaller_dir) / "mitmproxy-linux-redirector" 17 | else: 18 | here = Path(__file__).parent.absolute() 19 | scripts = Path(sysconfig.get_path("scripts")).absolute() 20 | exe = scripts / "mitmproxy-linux-redirector" 21 | 22 | # Development path: This should never happen with precompiled wheels. 23 | if not exe.exists() and (here / "../Cargo.toml").exists(): 24 | import logging 25 | import subprocess 26 | 27 | logger = logging.getLogger(__name__) 28 | logger.warning("Development mode: Compiling mitmproxy-linux-redirector...") 29 | 30 | # Build Redirector 31 | subprocess.run(["cargo", "build"], cwd=here.parent, check=True) 32 | target_debug = here.parent.parent / "target/debug" 33 | 34 | logger.warning("Development mode: Using target/debug/linux-redirector...") 35 | exe = target_debug / "mitmproxy-linux-redirector" 36 | 37 | return exe 38 | -------------------------------------------------------------------------------- /mitmproxy-linux/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.7,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mitmproxy_linux" 7 | requires-python = ">=3.12" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | dynamic = ["version"] 14 | 15 | [tool.maturin] 16 | bindings = "bin" 17 | -------------------------------------------------------------------------------- /mitmproxy-linux/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "linux")] 2 | include!("main2.rs"); 3 | 4 | #[cfg(not(target_os = "linux"))] 5 | pub fn main() { 6 | panic!("The Linux redirector works on Linux only."); 7 | } 8 | -------------------------------------------------------------------------------- /mitmproxy-macos/.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "indentation": { 4 | "spaces": 4 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /mitmproxy-macos/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - redirector/ipc/mitmproxy_ipc.pb.swift 3 | disabled_rules: 4 | - identifier_name 5 | - trailing_whitespace 6 | - line_length 7 | - closure_parameter_position 8 | opt_in_rules: 9 | - unhandled_throwing_task 10 | -------------------------------------------------------------------------------- /mitmproxy-macos/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-macos 2 | 3 | This package contains the following precompiled binaries for macOS: 4 | 5 | - `macos-certificate-truster.app`: A helper app written in Rust to mark the mitmproxy CA as trusted. 6 | - `Mitmproxy Redirector.app`: The app bundle that sets up and hosts the network extension for redirecting traffic. 7 | 8 | ## Redirector Development Setup 9 | 10 | The macOS Network System Extension needs to be signed and notarized during development. 11 | You need to reconfigure the XCode project to use your own (paid) Apple Developer Account. 12 | 13 | - Clicking "Build" in XCode should automatically install `/Applications/Mitmproxy Redirector.app`. 14 | - Run mitmproxy with an `MITMPROXY_KEEP_REDIRECTOR=1` env var to keep the development version. 15 | mitmproxy should start with "Using existing mitmproxy redirector app." 16 | - ⚠️ Bump the network extension version on changes, otherwise existing installations will not be replaced 17 | on upgrade, see https://github.com/mitmproxy/mitmproxy_rs/pull/227. 18 | -------------------------------------------------------------------------------- /mitmproxy-macos/certificate-truster/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macos-certificate-truster" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [target.'cfg(target_os = "macos")'.dependencies] 12 | security-framework = "3.2.0" 13 | -------------------------------------------------------------------------------- /mitmproxy-macos/certificate-truster/README.md: -------------------------------------------------------------------------------- 1 | # Why we have to create a Bundle App to add and trust the certificate? 2 | 3 | This minimal bundle app is to overcome the limitations of macOS in automating the mitmproxy certificate trust process. This app will operate without any actual user interaction or window display, except for the possible popup asking for permission to unlock the keychain. By bypassing the GUI restrictions, this solution ensures smoother and automated certificate management on MacOS systems. 4 | 5 | Read the [Special Consideration](https://developer.apple.com/documentation/security/1399119-sectrustsettingssettrustsettings#1819554) paragraph on official Apple Documentation for more info. 6 | -------------------------------------------------------------------------------- /mitmproxy-macos/certificate-truster/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | use security_framework::{ 3 | item::{ItemClass, ItemSearchOptions, Reference, SearchResult}, 4 | trust_settings::{Domain, TrustSettings}, 5 | }; 6 | 7 | #[cfg(target_os = "macos")] 8 | fn main() { 9 | if let SearchResult::Ref(Reference::Certificate(cert)) = ItemSearchOptions::new() 10 | .class(ItemClass::certificate()) 11 | .load_refs(true) 12 | .label("mitmproxy") 13 | .search() 14 | .unwrap() 15 | .first() 16 | .unwrap() 17 | { 18 | TrustSettings::new(Domain::Admin) 19 | .set_trust_settings_always(cert) 20 | .unwrap(); 21 | } 22 | } 23 | 24 | #[cfg(not(target_os = "macos"))] 25 | fn main() { 26 | panic!("The macOS certificate truster works on macOS only."); 27 | } 28 | -------------------------------------------------------------------------------- /mitmproxy-macos/mitmproxy_macos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/mitmproxy_macos/__init__.py -------------------------------------------------------------------------------- /mitmproxy-macos/mitmproxy_macos/macos-certificate-truster.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | mitmproxy-certificate-truster 7 | CFBundleIconFile 8 | mitmproxy 9 | CFBundleExecutable 10 | macos-certificate-truster 11 | CFBundleIdentifier 12 | org.mitmproxy.certificate-truster 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | mitmproxy-certificate-truster 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 20230512.103042 23 | CSResourcesFileMapped 24 | 25 | LSRequiresCarbon 26 | 27 | NSHighResolutionCapable 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /mitmproxy-macos/mitmproxy_macos/macos-certificate-truster.app/Contents/Resources/mitmproxy.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/mitmproxy_macos/macos-certificate-truster.app/Contents/Resources/mitmproxy.icns -------------------------------------------------------------------------------- /mitmproxy-macos/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mitmproxy-macos" 7 | dynamic = ["version"] 8 | license = "MIT" 9 | requires-python = ">=3.12" 10 | readme = "README.md" 11 | 12 | [project.urls] 13 | Source = "https://github.com/mitmproxy/mitmproxy-rs" 14 | 15 | [tool.hatch.build] 16 | only-include = ["mitmproxy_macos"] 17 | 18 | [tool.hatch.version] 19 | path = "./version-info.toml" 20 | pattern = "version = \"(?P.+?)\"" 21 | 22 | [tool.hatch.build.force-include] 23 | # "../target/release/macos-certificate-truster" = "mitmproxy_macos/macos-certificate-truster.app/Contents/MacOS/macos-certificate-truster" 24 | "./redirector/dist/Mitmproxy Redirector.app.tar" = "mitmproxy_macos/Mitmproxy Redirector.app.tar" 25 | "../Cargo.toml" = "./version-info.toml" 26 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | build/ -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/ExportOptions.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | destination 6 | export 7 | method 8 | developer-id 9 | provisioningProfiles 10 | 11 | org.mitmproxy.macos-redirector 12 | Mitmproxy Redirector 13 | org.mitmproxy.macos-redirector.network-extension 14 | Mitmproxy Redirector Network Extension 15 | 16 | signingCertificate 17 | 89AACB17FA34CC93BE764A1024FAB6346155A439 18 | signingStyle 19 | manual 20 | teamID 21 | S8XHQB96PW 22 | 23 | 24 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/README.md: -------------------------------------------------------------------------------- 1 | # Mitmproxy Redirector for macOS 2 | 3 | - `macos-redirector`: The app bundle that sets up and hosts the network extension. 4 | - `network-extension`: The network extension that redirects traffic. 5 | - `ipc`: Inter-process protobuf communication between proxy (Rust) and redirector (Swift). 6 | 7 | 8 | ## High-Level Overview 9 | 10 | When starting transparent interception on macOS, the following things happen: 11 | 12 | 1. mitmproxy-rs' `start_local_redirector` copies the redirector application into `/Applications`, 13 | which is a prerequisite for installing system extensions. 14 | 2. mitmproxy-rs opens a unix socket listener. 15 | 2. mitmproxy-rs starts the `macos-redirector` app, passing the unix socket as an argument. 16 | 3. The macos-redirector app installs system extension and sets up the transparent proxy configuration. 17 | 4. As a result the network extension is started by the OS. It immediately opens a unix socket to mitmproxy-rs acting as a control channel. 18 | 5. mitmproxy-rs will pass the intercept spec (describing which apps to intercept) to the network extension. 19 | 6. The network extension receives all new TCP/UDP flows on the system. 20 | If the intercept spec matches, it intercepts the connection, opens a new unix socket to mitmproxy-rs and copies over all message contents. 21 | Using a separate unix socket per flow ensure that each connection has its own dedicated buffers + backpressure. 22 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/ipc/utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Network 3 | import NetworkExtension 4 | import SwiftProtobuf 5 | 6 | extension UInt32 { 7 | var data: Data { 8 | var int = self 9 | return Data(bytes: &int, count: MemoryLayout.size) 10 | } 11 | } 12 | 13 | enum IpcError: Error { 14 | case incompleteRead 15 | case connectionCancelled 16 | } 17 | 18 | extension NWConnection { 19 | /// Async wrapper to establish a connection and wait for NWConnection.State.ready 20 | func establish() async throws { 21 | let orig_handler = self.stateUpdateHandler 22 | defer { 23 | self.stateUpdateHandler = orig_handler 24 | } 25 | try await withCheckedThrowingContinuation { continuation in 26 | self.stateUpdateHandler = { state in 27 | log.info("stateUpdate: \(String(describing: state), privacy: .public)") 28 | switch state { 29 | case .ready: 30 | continuation.resume() 31 | case .waiting(let err): 32 | continuation.resume(with: .failure(err)) 33 | case .failed(let err): 34 | continuation.resume(with: .failure(err)) 35 | case .cancelled: 36 | continuation.resume(with: .failure(IpcError.connectionCancelled)) 37 | default: 38 | break 39 | } 40 | } 41 | self.start(queue: DispatchQueue.global()) 42 | } 43 | } 44 | 45 | func send(ipc message: SwiftProtobuf.Message) async throws { 46 | let data = try message.serializedData() 47 | var to_send = Data(capacity: data.count + 4) 48 | to_send.append(UInt32(data.count).bigEndian.data) 49 | to_send.append(data) 50 | assert(to_send.count == data.count + 4) 51 | 52 | try await withCheckedThrowingContinuation { 53 | (continuation: CheckedContinuation) -> Void in 54 | self.send( 55 | content: to_send, 56 | completion: .contentProcessed({ error in 57 | if let err = error { 58 | continuation.resume(throwing: err) 59 | } else { 60 | continuation.resume() 61 | } 62 | })) 63 | } 64 | } 65 | 66 | func receive(ipc: T.Type) async throws -> T? { 67 | return try await withCheckedThrowingContinuation { 68 | (continuation: CheckedContinuation) -> Void in 69 | receive( 70 | minimumIncompleteLength: 4, maximumLength: 4, 71 | completion: { len_buf, _, _, _ in 72 | guard 73 | let len_buf = len_buf, 74 | len_buf.count == 4 75 | else { 76 | if len_buf == nil { 77 | return continuation.resume(returning: nil) 78 | } else { 79 | return continuation.resume(throwing: IpcError.incompleteRead) 80 | } 81 | } 82 | let len = len_buf[...].reduce(Int(0)) { $0 << 8 + Int($1) } 83 | 84 | self.receive(minimumIncompleteLength: len, maximumLength: len) { 85 | data, _, _, _ in 86 | guard 87 | let data = data, 88 | data.count == len 89 | else { 90 | return continuation.resume(throwing: IpcError.incompleteRead) 91 | } 92 | do { 93 | let message = try T(contiguousBytes: data) 94 | continuation.resume(returning: message) 95 | } catch { 96 | continuation.resume(throwing: error) 97 | } 98 | } 99 | }) 100 | } 101 | } 102 | } 103 | 104 | extension MitmproxyIpc_Address { 105 | init(endpoint: NWHostEndpoint) { 106 | self.init() 107 | self.host = endpoint.hostname 108 | self.port = UInt32(endpoint.port)! 109 | } 110 | } 111 | 112 | extension NWHostEndpoint { 113 | convenience init(address: MitmproxyIpc_Address) { 114 | self.init(hostname: address.host, port: String(address.port)) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-protobuf", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-protobuf.git", 7 | "state" : { 8 | "revision" : "ce20dc083ee485524b802669890291c0d8090170", 9 | "version" : "1.22.1" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector.xcodeproj/xcshareddata/xcschemes/macos-redirector.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 60 | 64 | 65 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-circle-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "logo-circle-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "logo-circle-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "logo-circle-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "logo-circle-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "logo-circle-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "logo-circle-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "logo-circle-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "logo-circle-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "logo-circle-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-1024.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-128.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-16.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-256.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-32.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-512.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/AppIcon.appiconset/logo-circle-64.png -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/macos-redirector/macos_redirector.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | app-proxy-provider-systemextension 8 | 9 | com.apple.developer.system-extension.install 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSSystemExtensionUsageDescription 6 | Redirect traffic to mitmproxy 7 | NetworkExtension 8 | 9 | NEProviderClasses 10 | 11 | com.apple.networkextension.app-proxy 12 | $(PRODUCT_MODULE_NAME).TransparentProxyProvider 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/InterceptConf.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Action { 4 | case include(Pattern) 5 | case exclude(Pattern) 6 | 7 | init(from string: String) throws { 8 | if string.hasPrefix("!") { 9 | self = .exclude(Pattern(from: String(string.dropFirst()))) 10 | } else { 11 | self = .include(Pattern(from: string)) 12 | } 13 | } 14 | } 15 | 16 | enum Pattern { 17 | case pid(UInt32) 18 | case process(String) 19 | 20 | init(from string: String) { 21 | if let pid = UInt32(string) { 22 | self = .pid(pid) 23 | } else { 24 | self = .process(string) 25 | } 26 | } 27 | 28 | func matches(_ processInfo: ProcessInfo) -> Bool { 29 | switch self { 30 | case .pid(let pid): 31 | return processInfo.pid == pid 32 | case .process(let name): 33 | if let processName = processInfo.path { 34 | return processName.contains(name) 35 | } else { 36 | return false 37 | } 38 | } 39 | } 40 | } 41 | 42 | 43 | /// The intercept spec decides whether a TCP/UDP flow should be intercepted or not. 44 | class InterceptConf { 45 | 46 | private var defaultAction: Bool 47 | private var actions: [Action] 48 | 49 | init(defaultAction: Bool, actions: [Action]) { 50 | self.defaultAction = defaultAction 51 | self.actions = actions 52 | } 53 | 54 | convenience init(from ipc: MitmproxyIpc_InterceptConf) throws { 55 | let actions = try ipc.actions.map { try Action(from: $0) } 56 | let defaultAction = ipc.actions[0].hasPrefix("!") 57 | self.init(defaultAction: defaultAction, actions: actions) 58 | } 59 | 60 | /// Mirrored after the Rust implementation 61 | func shouldIntercept(_ processInfo: ProcessInfo) -> Bool { 62 | var intercept = self.defaultAction 63 | 64 | for action in actions { 65 | switch action { 66 | case .include(let pattern): 67 | intercept = intercept || pattern.matches(processInfo) 68 | case .exclude(let pattern): 69 | intercept = intercept && !pattern.matches(processInfo) 70 | } 71 | } 72 | 73 | return intercept 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/ProcessInfoCache.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ProcessInfo { 4 | var pid: UInt32 5 | var path: String? 6 | } 7 | 8 | let PROC_PIDPATHINFO_MAXSIZE = UInt32(MAXPATHLEN * 4) 9 | 10 | /// An audit token -> (pid, process path) lookup cache. 11 | class ProcessInfoCache { 12 | private static var cache: [Data: ProcessInfo] = [:] 13 | 14 | static func getInfo(fromAuditToken tokenData: Data?) -> ProcessInfo? { 15 | guard let tokenData = tokenData 16 | else { return nil } 17 | 18 | if let cached = cache[tokenData] { 19 | return cached 20 | } 21 | 22 | // Data -> audit_token_t 23 | guard tokenData.count == MemoryLayout.size 24 | else { return nil } 25 | let token = tokenData.withUnsafeBytes { buf in 26 | buf.baseAddress!.assumingMemoryBound(to: audit_token_t.self).pointee 27 | } 28 | 29 | let pid = audit_token_to_pid(token) 30 | 31 | // pid -> path 32 | let pathBuffer = UnsafeMutablePointer.allocate( 33 | capacity: Int(PROC_PIDPATHINFO_MAXSIZE)) 34 | defer { 35 | pathBuffer.deallocate() 36 | } 37 | let path: String? 38 | if proc_pidpath(pid, pathBuffer, PROC_PIDPATHINFO_MAXSIZE) > 0 { 39 | path = String(cString: pathBuffer) 40 | } else { 41 | path = nil 42 | } 43 | 44 | let procInfo = ProcessInfo(pid: UInt32(pid), path: path) 45 | cache[tokenData] = procInfo 46 | return procInfo 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/libproc-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #ifndef Header_h 2 | #define Header_h 3 | 4 | #include 5 | 6 | #endif /* Header_h */ 7 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NetworkExtension 3 | import OSLog 4 | 5 | let log = Logger(subsystem: "org.mitmproxy.macos-redirector", category: "extension") 6 | let networkExtensionIdentifier = "org.mitmproxy.macos-redirector.network-extension" 7 | 8 | autoreleasepool { 9 | let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String 10 | log.error("starting mitmproxy redirector \(version, privacy: .public) system extension") 11 | log.debug("debug-level logging active") 12 | NEProvider.startSystemExtensionMode() 13 | } 14 | 15 | dispatchMain() 16 | -------------------------------------------------------------------------------- /mitmproxy-macos/redirector/network-extension/network_extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.networking.networkextension 6 | 7 | app-proxy-provider-systemextension 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /mitmproxy-macos/version-info.toml: -------------------------------------------------------------------------------- 1 | ../Cargo.toml -------------------------------------------------------------------------------- /mitmproxy-rs/.gitignore: -------------------------------------------------------------------------------- 1 | mitmproxy_rs/WinDivert* 2 | mitmproxy_rs/windows-redirector.exe 3 | mitmproxy_rs/*.pyd 4 | mitmproxy_rs/*.so 5 | mitmproxy_rs/*.app 6 | -------------------------------------------------------------------------------- /mitmproxy-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitmproxy_rs" 3 | license = "MIT" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [lints] 12 | workspace = true 13 | 14 | [lib] 15 | name = "mitmproxy_rs" 16 | crate-type = ["lib", "cdylib"] 17 | 18 | [dependencies] 19 | mitmproxy = { path = "../" } 20 | mitmproxy-highlight = { path = "../mitmproxy-highlight" } 21 | mitmproxy-contentviews = { path = "../mitmproxy-contentviews" } 22 | anyhow = { version = "1.0.97", features = ["backtrace"] } 23 | data-encoding = "2.8.0" 24 | log = "0.4.27" 25 | pyo3 = { version = "0.25", features = ["abi3", "abi3-py312", "anyhow"] } 26 | pyo3-async-runtimes = { version = "0.25", features = ["tokio-runtime", "testing", "attributes"] } 27 | pyo3-log = "0.12" 28 | rand_core = { version = "0.6.4", features = ["getrandom"] } # https://github.com/dalek-cryptography/curve25519-dalek/issues/731 29 | tokio = { version = "1.45", features = ["macros", "net", "rt-multi-thread", "sync"] } 30 | boringtun = "0.6" 31 | tar = "0.4.44" 32 | console-subscriber = { version = "0.4.1", optional = true } 33 | 34 | [target.'cfg(target_os = "linux")'.dependencies] 35 | nix = { version = "0.30.1", features = ["user"] } 36 | 37 | [dev-dependencies] 38 | env_logger = "0.11" 39 | 40 | [features] 41 | tracing = ["console-subscriber"] 42 | docs = [] 43 | 44 | [[test]] 45 | name = "test_task" 46 | path = "pytests/test_task.rs" 47 | harness = false 48 | -------------------------------------------------------------------------------- /mitmproxy-rs/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy_rs 2 | 3 | This package contains mitmproxy's Rust bits. 4 | 5 | [![dev documentation](https://shields.mitmproxy.org/badge/docs-Python%20API-blue.svg)](https://mitmproxy.github.io/mitmproxy_rs/) 6 | 7 | https://github.com/mitmproxy/mitmproxy_rs 8 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import types 3 | 4 | from .mitmproxy_rs import * 5 | 6 | __doc__ = mitmproxy_rs.__doc__ 7 | if hasattr(mitmproxy_rs, "__all__"): 8 | __all__ = mitmproxy_rs.__all__ 9 | 10 | # Hacky workaround for https://github.com/PyO3/pyo3/issues/759 11 | for k, v in vars(mitmproxy_rs).items(): 12 | if isinstance(v, types.ModuleType): 13 | sys.modules[f"mitmproxy_rs.{k}"] = v 14 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/__init__.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Literal 4 | from typing import final, overload, TypeVar 5 | from . import certs, contentviews, dns, local, process_info, tun, udp, wireguard, syntax_highlight 6 | 7 | T = TypeVar("T") 8 | 9 | # TCP / UDP 10 | 11 | @final 12 | class Stream: 13 | async def read(self, n: int) -> bytes: ... 14 | def write(self, data: bytes): ... 15 | async def drain(self) -> None: ... 16 | def write_eof(self): ... 17 | def close(self): ... 18 | def is_closing(self) -> bool: ... 19 | async def wait_closed(self) -> None: ... 20 | @overload 21 | def get_extra_info( 22 | self, name: Literal["transport_protocol"], default: None = None 23 | ) -> Literal["tcp", "udp"]: ... 24 | @overload 25 | def get_extra_info( 26 | self, name: Literal["transport_protocol"], default: T 27 | ) -> Literal["tcp", "udp"] | T: ... 28 | @overload 29 | def get_extra_info( 30 | self, 31 | name: Literal[ 32 | "peername", "sockname", "original_src", "original_dst", "remote_endpoint" 33 | ], 34 | default: None = None, 35 | ) -> tuple[str, int]: ... 36 | @overload 37 | def get_extra_info( 38 | self, 39 | name: Literal[ 40 | "peername", "sockname", "original_src", "original_dst", "remote_endpoint" 41 | ], 42 | default: T, 43 | ) -> tuple[str, int] | T: ... 44 | @overload 45 | def get_extra_info(self, name: Literal["pid"], default: None = None) -> int: ... 46 | @overload 47 | def get_extra_info(self, name: Literal["pid"], default: T) -> int | T: ... 48 | @overload 49 | def get_extra_info( 50 | self, name: Literal["process_name"], default: None = None 51 | ) -> str: ... 52 | @overload 53 | def get_extra_info(self, name: Literal["process_name"], default: T) -> str | T: ... 54 | @overload 55 | def get_extra_info(self, name: str, default: Any) -> Any: ... 56 | def __repr__(self) -> str: ... 57 | 58 | __all__ = [ 59 | "certs", 60 | "contentviews", 61 | "dns", 62 | "local", 63 | "process_info", 64 | "syntax_highlight", 65 | "tun", 66 | "udp", 67 | "wireguard", 68 | "Stream", 69 | ] 70 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/_pyinstaller/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | here = Path(__file__).parent.absolute() 4 | 5 | 6 | def hook_dirs() -> list[str]: 7 | return [str(here)] 8 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/_pyinstaller/hook-mitmproxy_linux.py: -------------------------------------------------------------------------------- 1 | import sysconfig 2 | import os.path 3 | 4 | binaries = [ 5 | (os.path.join(sysconfig.get_path("scripts"), "mitmproxy-linux-redirector"), ".") 6 | ] 7 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/_pyinstaller/hook-mitmproxy_macos.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("mitmproxy_macos") 4 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/_pyinstaller/hook-mitmproxy_rs.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from PyInstaller.utils.hooks import collect_data_files 3 | 4 | datas = collect_data_files("mitmproxy_rs") 5 | 6 | hiddenimports = [] 7 | 8 | match platform.system(): 9 | case "Darwin": 10 | hiddenimports.append("mitmproxy_macos") 11 | case "Windows": 12 | hiddenimports.append("mitmproxy_windows") 13 | case "Linux": 14 | hiddenimports.append("mitmproxy_linux") 15 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/_pyinstaller/hook-mitmproxy_windows.py: -------------------------------------------------------------------------------- 1 | from PyInstaller.utils.hooks import collect_data_files 2 | 3 | datas = collect_data_files("mitmproxy_windows") 4 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/certs.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | def add_cert(pem: str) -> None: ... 4 | def remove_cert() -> None: ... 5 | 6 | __all__ = [ 7 | "add_cert", 8 | "remove_cert", 9 | ] 10 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/contentviews.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ClassVar, final, Literal 4 | 5 | class Contentview: 6 | name: ClassVar[str] 7 | 8 | syntax_highlight: ClassVar[Literal["xml", "yaml", "none", "error"]] 9 | 10 | def prettify(self, data: bytes, metadata) -> str: 11 | pass 12 | 13 | def render_priority(self, data: bytes, metadata) -> float: 14 | pass 15 | 16 | @final 17 | class InteractiveContentview(Contentview): 18 | def reencode(self, data: str, metadata) -> bytes: 19 | pass 20 | 21 | _test_inspect_metadata: Contentview 22 | hex_dump: Contentview 23 | hex_stream: InteractiveContentview 24 | msgpack: InteractiveContentview 25 | protobuf: InteractiveContentview 26 | grpc: InteractiveContentview 27 | 28 | __all__ = [ 29 | "Contentview", 30 | "InteractiveContentview", 31 | "hex_dump", 32 | "hex_stream", 33 | "msgpack", 34 | "protobuf", 35 | "grpc", 36 | "_test_inspect_metadata", 37 | ] 38 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/dns.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import final 3 | 4 | @final 5 | class DnsResolver: 6 | def __init__( 7 | self, *, name_servers: list[str] | None = None, use_hosts_file: bool = True 8 | ) -> None: ... 9 | async def lookup_ip(self, host: str) -> list[str]: ... 10 | async def lookup_ipv4(self, host: str) -> list[str]: ... 11 | async def lookup_ipv6(self, host: str) -> list[str]: ... 12 | 13 | def get_system_dns_servers() -> list[str]: ... 14 | 15 | __all__ = [ 16 | "DnsResolver", 17 | "get_system_dns_servers", 18 | ] 19 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/local.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Awaitable, Callable 4 | from typing import final 5 | from . import Stream 6 | 7 | async def start_local_redirector( 8 | handle_tcp_stream: Callable[[Stream], Awaitable[None]], 9 | handle_udp_stream: Callable[[Stream], Awaitable[None]], 10 | ) -> LocalRedirector: ... 11 | @final 12 | class LocalRedirector: 13 | @staticmethod 14 | def describe_spec(spec: str) -> None: ... 15 | def set_intercept(self, spec: str) -> None: ... 16 | def close(self) -> None: ... 17 | async def wait_closed(self) -> None: ... 18 | @staticmethod 19 | def unavailable_reason() -> str | None: ... 20 | 21 | __all__ = [ 22 | "start_local_redirector", 23 | "LocalRedirector", 24 | ] 25 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/process_info.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from pathlib import Path 3 | from typing import final 4 | 5 | def active_executables() -> list[Process]: ... 6 | def executable_icon(path: Path | str) -> bytes: ... 7 | @final 8 | class Process: 9 | @property 10 | def executable(self) -> str: ... 11 | @property 12 | def display_name(self) -> str: ... 13 | @property 14 | def is_visible(self) -> bool: ... 15 | @property 16 | def is_system(self) -> bool: ... 17 | 18 | __all__ = [ 19 | "active_executables", 20 | "executable_icon", 21 | "Process", 22 | ] 23 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-rs/mitmproxy_rs/py.typed -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/syntax_highlight.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Literal 4 | 5 | 6 | def highlight(text: str, language: Literal["css", "javascript", "xml", "yaml", "none", "error"]) -> list[tuple[str, str]]: 7 | pass 8 | 9 | def languages() -> list[str]: 10 | pass 11 | 12 | def tags() -> list[str]: 13 | pass 14 | 15 | __all__ = [ 16 | "highlight", 17 | "languages", 18 | "tags", 19 | ] 20 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/tun.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Awaitable, Callable 4 | from typing import final 5 | from . import Stream 6 | 7 | async def create_tun_interface( 8 | handle_tcp_stream: Callable[[Stream], Awaitable[None]], 9 | handle_udp_stream: Callable[[Stream], Awaitable[None]], 10 | tun_name: str | None = None, 11 | ) -> TunInterface: ... 12 | @final 13 | class TunInterface: 14 | def tun_name(self) -> str: ... 15 | def close(self) -> None: ... 16 | async def wait_closed(self) -> None: ... 17 | def __repr__(self) -> str: ... 18 | @staticmethod 19 | def unavailable_reason() -> str | None: ... 20 | 21 | __all__ = [ 22 | "create_tun_interface", 23 | "TunInterface", 24 | ] 25 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/udp.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Awaitable, Callable 4 | from typing import final 5 | from . import Stream 6 | 7 | async def start_udp_server( 8 | host: str, 9 | port: int, 10 | handle_udp_stream: Callable[[Stream], Awaitable[None]], 11 | ) -> UdpServer: ... 12 | @final 13 | class UdpServer: 14 | def getsockname(self) -> tuple[str, int]: ... 15 | def close(self) -> None: ... 16 | async def wait_closed(self) -> None: ... 17 | def __repr__(self) -> str: ... 18 | 19 | async def open_udp_connection( 20 | host: str, 21 | port: int, 22 | *, 23 | local_addr: tuple[str, int] | None = None, 24 | ) -> Stream: ... 25 | 26 | __all__ = [ 27 | "start_udp_server", 28 | "UdpServer", 29 | "open_udp_connection", 30 | ] 31 | -------------------------------------------------------------------------------- /mitmproxy-rs/mitmproxy_rs/wireguard.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Awaitable, Callable 4 | from typing import final 5 | from . import Stream 6 | 7 | def genkey() -> str: ... 8 | def pubkey(private_key: str) -> str: ... 9 | async def start_wireguard_server( 10 | host: str, 11 | port: int, 12 | private_key: str, 13 | peer_public_keys: list[str], 14 | handle_tcp_stream: Callable[[Stream], Awaitable[None]], 15 | handle_udp_stream: Callable[[Stream], Awaitable[None]], 16 | ) -> WireGuardServer: ... 17 | @final 18 | class WireGuardServer: 19 | def getsockname(self) -> tuple[str, int]: ... 20 | def close(self) -> None: ... 21 | async def wait_closed(self) -> None: ... 22 | def __repr__(self) -> str: ... 23 | 24 | __all__ = [ 25 | "genkey", 26 | "pubkey", 27 | "start_wireguard_server", 28 | "WireGuardServer", 29 | ] 30 | -------------------------------------------------------------------------------- /mitmproxy-rs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1,<2"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "mitmproxy_rs" 7 | dynamic = ["version"] 8 | requires-python = ">=3.12" 9 | classifiers = [ 10 | "Programming Language :: Rust", 11 | "Programming Language :: Python :: Implementation :: CPython", 12 | "Programming Language :: Python :: Implementation :: PyPy", 13 | "Programming Language :: Python :: 3 :: Only", 14 | "Programming Language :: Python :: 3.12", 15 | "Programming Language :: Python :: 3.13", 16 | "Development Status :: 5 - Production/Stable", 17 | ] 18 | 19 | dependencies = [ 20 | "mitmproxy_windows; os_name == 'nt'", 21 | "mitmproxy_linux; sys_platform == 'linux'", 22 | "mitmproxy_macos; sys_platform == 'darwin'", 23 | ] 24 | 25 | [tool.black] 26 | line-length = 140 27 | include = '\.pyi?$' 28 | 29 | [project.entry-points.pyinstaller40] 30 | hook-dirs = "mitmproxy_rs._pyinstaller:hook_dirs" 31 | 32 | [tool.mypy] 33 | exclude = [ 34 | 'mitmproxy_rs/_pyinstaller' 35 | ] 36 | 37 | # https://pyo3.rs/v0.22.2/faq.html?highlight=cargo%20test#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror 38 | [tool.maturin] 39 | features = ["pyo3/extension-module"] 40 | -------------------------------------------------------------------------------- /mitmproxy-rs/pytests/logger.rs: -------------------------------------------------------------------------------- 1 | use log::{LevelFilter, Log, Metadata, Record}; 2 | use std::sync::LazyLock; 3 | use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; 4 | use tokio::sync::{mpsc, Mutex, MutexGuard}; 5 | 6 | /// A logger for tests to ensure that log statements are made. 7 | pub struct TestLogger { 8 | tx: UnboundedSender, 9 | rx: Mutex>, 10 | buf: Mutex>, 11 | } 12 | impl Log for TestLogger { 13 | fn enabled(&self, _metadata: &Metadata) -> bool { 14 | true 15 | } 16 | 17 | fn log(&self, record: &Record) { 18 | self.tx.send(format!("{}", record.args())).unwrap() 19 | } 20 | 21 | fn flush(&self) {} 22 | } 23 | impl TestLogger { 24 | /// Wait for a log line to appear. If the log message already appeared, 25 | /// we return immediately. 26 | pub async fn wait_for(&self, needle: &str) { 27 | let mut buf = self.buf.lock().await; 28 | if buf.iter().any(|m| m.contains(needle)) { 29 | return; 30 | } 31 | 32 | let mut rx = self.rx.lock().await; 33 | while let Some(m) = rx.recv().await { 34 | let done = m.contains(needle); 35 | buf.push(m); 36 | if done { 37 | break; 38 | } 39 | } 40 | } 41 | 42 | /// Get a copy of all log lines so far. 43 | pub async fn logs(&self) -> Vec { 44 | let mut buf = self.buf.lock().await; 45 | let mut rx = self.rx.lock().await; 46 | while let Ok(m) = rx.try_recv() { 47 | buf.push(m); 48 | } 49 | buf.clone() 50 | } 51 | 52 | /// Clear log buffer. 53 | pub async fn clear(&self) { 54 | while let Ok(x) = self.rx.lock().await.try_recv() { 55 | drop(x); 56 | } 57 | self.buf.lock().await.clear(); 58 | } 59 | } 60 | static _LOGGER: LazyLock> = LazyLock::new(|| { 61 | let (tx, rx) = mpsc::unbounded_channel(); 62 | let logger = Box::leak(Box::new(TestLogger { 63 | tx, 64 | rx: Mutex::new(rx), 65 | buf: Mutex::new(vec![]), 66 | })); 67 | log::set_logger(logger).expect("cannot set logger"); 68 | log::set_max_level(LevelFilter::Debug); 69 | Mutex::new(logger) 70 | }); 71 | 72 | /// Initialize the logger. 73 | /// pyo3_async_runtimes tests all run in parallel in the same runtime, so we use a mutex to ensure 74 | /// that only one test that uses TestLogger runs at the same time. 75 | pub async fn setup_logger() -> MutexGuard<'static, &'static TestLogger> { 76 | let logger = _LOGGER.lock().await; 77 | logger.clear().await; 78 | logger 79 | } 80 | -------------------------------------------------------------------------------- /mitmproxy-rs/pytests/test_task.rs: -------------------------------------------------------------------------------- 1 | mod logger; 2 | 3 | mod tests { 4 | use std::ffi::CString; 5 | use std::future::Future; 6 | 7 | use mitmproxy::messages::{ConnectionId, TransportEvent, TunnelInfo}; 8 | use mitmproxy_rs::task::PyInteropTask; 9 | use pyo3::prelude::*; 10 | use pyo3::types::PyDict; 11 | 12 | use crate::logger::setup_logger; 13 | 14 | use mitmproxy::shutdown; 15 | use tokio::sync::mpsc; 16 | 17 | #[pyo3_async_runtimes::tokio::test] 18 | async fn test_handler_invalid_signature() -> PyResult<()> { 19 | let logger = setup_logger().await; 20 | _test_task_error_handling( 21 | "async def handler(): pass", 22 | logger.wait_for("Failed to spawn connection handler"), 23 | ) 24 | .await?; 25 | logger.wait_for("shutting down").await; 26 | Ok(()) 27 | } 28 | 29 | #[pyo3_async_runtimes::tokio::test] 30 | async fn test_handler_runtime_error() -> PyResult<()> { 31 | let logger = setup_logger().await; 32 | _test_task_error_handling( 33 | "async def handler(stream): raise RuntimeError('task failed successfully')", 34 | logger.wait_for("RuntimeError: task failed successfully"), 35 | ) 36 | .await?; 37 | logger.wait_for("shutting down").await; 38 | Ok(()) 39 | } 40 | 41 | #[pyo3_async_runtimes::tokio::test] 42 | async fn test_handler_cancelled() -> PyResult<()> { 43 | let logger = setup_logger().await; 44 | _test_task_error_handling( 45 | "async def handler(stream): import asyncio; asyncio.current_task().cancel()", 46 | async {}, 47 | ) 48 | .await?; 49 | logger.wait_for("shutting down").await; 50 | assert!(!logger 51 | .logs() 52 | .await 53 | .into_iter() 54 | .any(|l| l.contains("exception"))); 55 | Ok(()) 56 | } 57 | 58 | async fn _test_task_error_handling(code: &str, verify: F) -> PyResult<()> 59 | where 60 | F: Future, 61 | { 62 | let (command_tx, _command_rx) = mpsc::unbounded_channel(); 63 | let (event_tx, event_rx) = mpsc::channel(1); 64 | let (shutdown_tx, shutdown_rx) = shutdown::channel(); 65 | 66 | let (tcp_handler, udp_handler) = Python::with_gil(|py| { 67 | let locals = PyDict::new(py); 68 | let code = CString::new(code).unwrap(); 69 | py.run(&code, None, Some(&locals)).unwrap(); 70 | let handler = locals.get_item("handler").unwrap().unwrap().unbind(); 71 | (handler.clone_ref(py), handler) 72 | }); 73 | 74 | let task = PyInteropTask::new(command_tx, event_rx, tcp_handler, udp_handler, shutdown_rx)?; 75 | let task = tokio::spawn(task.run()); 76 | 77 | event_tx 78 | .send(TransportEvent::ConnectionEstablished { 79 | connection_id: ConnectionId::unassigned_udp(), 80 | src_addr: "127.0.0.1:51232".parse()?, 81 | dst_addr: "127.0.0.1:53".parse()?, 82 | tunnel_info: TunnelInfo::None, 83 | command_tx: None, 84 | }) 85 | .await 86 | .unwrap(); 87 | // ensure previous event is processed. 88 | let _ = event_tx.reserve().await.unwrap(); 89 | 90 | verify.await; 91 | 92 | shutdown_tx.send(()).unwrap(); 93 | task.await.unwrap()?; 94 | Ok(()) 95 | } 96 | } 97 | 98 | #[pyo3_async_runtimes::tokio::main] 99 | async fn main() -> pyo3::PyResult<()> { 100 | pyo3_async_runtimes::testing::main().await 101 | } 102 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/process_info.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | #[cfg(any(windows, target_os = "macos"))] 4 | use anyhow::Context; 5 | use pyo3::prelude::*; 6 | #[cfg(any(windows, target_os = "macos"))] 7 | use pyo3::IntoPyObjectExt; 8 | 9 | #[cfg(any(windows, target_os = "macos", target_os = "linux"))] 10 | use mitmproxy::processes; 11 | 12 | #[pyclass(module = "mitmproxy_rs.process_info", frozen)] 13 | pub struct Process(mitmproxy::processes::ProcessInfo); 14 | 15 | #[pymethods] 16 | impl Process { 17 | /// Absolute path for the executable. 18 | #[getter] 19 | fn executable(&self) -> &Path { 20 | &self.0.executable 21 | } 22 | /// Process name suitable for display in the UI. 23 | #[getter] 24 | fn display_name(&self) -> &str { 25 | &self.0.display_name 26 | } 27 | /// `True` if the process has a visible window, `False` otherwise. 28 | /// This information is useful when sorting the process list. 29 | #[getter] 30 | fn is_visible(&self) -> bool { 31 | self.0.is_visible 32 | } 33 | /// `True` if the process is a system process, `False` otherwise. 34 | /// This information is useful to hide noise in the process list. 35 | #[getter] 36 | fn is_system(&self) -> bool { 37 | self.0.is_system 38 | } 39 | fn __repr__(&self) -> String { 40 | format!( 41 | "Process(executable={:?}, display_name={:?}, is_visible={}, is_system={})", 42 | self.executable(), 43 | self.display_name(), 44 | self.is_visible(), 45 | self.is_system(), 46 | ) 47 | } 48 | } 49 | 50 | /// Return a list of all running executables. 51 | /// Note that this groups multiple processes by executable name. 52 | /// 53 | /// *Availability: Windows, macOS, Linux* 54 | #[pyfunction] 55 | pub fn active_executables() -> PyResult> { 56 | #[cfg(any(windows, target_os = "macos", target_os = "linux"))] 57 | { 58 | processes::active_executables() 59 | .map(|p| p.into_iter().map(Process).collect()) 60 | .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(format!("{}", e))) 61 | } 62 | #[cfg(not(any(windows, target_os = "macos", target_os = "linux")))] 63 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 64 | "active_executables not supported on the current OS", 65 | )) 66 | } 67 | 68 | /// Get a PNG icon for an executable path. 69 | /// 70 | /// *Availability: Windows, macOS* 71 | #[pyfunction] 72 | #[allow(unused_variables)] 73 | pub fn executable_icon(py: Python<'_>, path: PathBuf) -> PyResult { 74 | #[cfg(any(windows, target_os = "macos"))] 75 | { 76 | let mut icon_cache = processes::ICON_CACHE.lock().unwrap(); 77 | icon_cache 78 | .get_png(path) 79 | .context("failed to get image")? 80 | .into_py_any(py) 81 | } 82 | #[cfg(not(any(windows, target_os = "macos")))] 83 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 84 | "executable_icon is only available on Windows", 85 | )) 86 | } 87 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/server/base.rs: -------------------------------------------------------------------------------- 1 | use crate::task::PyInteropTask; 2 | 3 | use anyhow::Result; 4 | 5 | use mitmproxy::packet_sources::{PacketSourceConf, PacketSourceTask}; 6 | use mitmproxy::shutdown::shutdown_task; 7 | use pyo3::prelude::*; 8 | 9 | use mitmproxy::shutdown; 10 | use tokio::sync::mpsc; 11 | use tokio::sync::watch; 12 | use tokio::task::JoinSet; 13 | 14 | #[derive(Debug)] 15 | pub struct Server { 16 | shutdown_done: shutdown::Receiver, 17 | start_shutdown: Option>, 18 | } 19 | 20 | impl Server { 21 | pub fn close(&mut self) { 22 | if let Some(trigger) = self.start_shutdown.take() { 23 | log::debug!("Shutting down."); 24 | trigger.send(()).ok(); 25 | } 26 | } 27 | 28 | pub fn wait_closed<'py>(&self, py: Python<'py>) -> PyResult> { 29 | let mut receiver = self.shutdown_done.clone(); 30 | pyo3_async_runtimes::tokio::future_into_py(py, async move { 31 | receiver.recv().await; 32 | Ok(()) 33 | }) 34 | } 35 | } 36 | 37 | impl Server { 38 | /// Set up and initialize a new WireGuard server. 39 | pub async fn init( 40 | packet_source_conf: T, 41 | py_tcp_handler: PyObject, 42 | py_udp_handler: PyObject, 43 | ) -> Result<(Self, T::Data)> 44 | where 45 | T: PacketSourceConf, 46 | { 47 | let typ = packet_source_conf.name(); 48 | log::debug!("Initializing {} ...", typ); 49 | 50 | // Channel used to notify Python land of incoming connections. 51 | let (transport_events_tx, transport_events_rx) = mpsc::channel(256); 52 | // Channel used to send data and ask for packets. 53 | // This needs to be unbounded because write() is not async. 54 | let (transport_commands_tx, transport_commands_rx) = mpsc::unbounded_channel(); 55 | // Channel used to trigger graceful shutdown 56 | let (shutdown_start_tx, shutdown_start_rx) = shutdown::channel(); 57 | 58 | let (packet_source_task, data) = packet_source_conf 59 | .build( 60 | transport_events_tx, 61 | transport_commands_rx, 62 | shutdown_start_rx.clone(), 63 | ) 64 | .await?; 65 | 66 | // initialize Python interop task 67 | let py_task = PyInteropTask::new( 68 | transport_commands_tx, 69 | transport_events_rx, 70 | py_tcp_handler, 71 | py_udp_handler, 72 | shutdown_start_rx, 73 | )?; 74 | 75 | // spawn tasks 76 | let mut tasks = JoinSet::new(); 77 | tasks.spawn(async move { packet_source_task.run().await }); 78 | tasks.spawn(async move { py_task.run().await }); 79 | 80 | let (shutdown_done_tx, shutdown_done_rx) = shutdown::channel(); 81 | tokio::spawn(shutdown_task(tasks, shutdown_done_tx)); 82 | 83 | log::debug!("{} successfully initialized.", typ); 84 | 85 | Ok(( 86 | Server { 87 | shutdown_done: shutdown_done_rx, 88 | start_shutdown: Some(shutdown_start_tx), 89 | }, 90 | data, 91 | )) 92 | } 93 | } 94 | 95 | impl Drop for Server { 96 | fn drop(&mut self) { 97 | self.close() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | mod base; 2 | mod local_redirector; 3 | mod tun; 4 | mod udp; 5 | mod wireguard; 6 | 7 | pub use local_redirector::{start_local_redirector, LocalRedirector}; 8 | pub use tun::{create_tun_interface, TunInterface}; 9 | pub use udp::{start_udp_server, UdpServer}; 10 | pub use wireguard::{start_wireguard_server, WireGuardServer}; 11 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/server/tun.rs: -------------------------------------------------------------------------------- 1 | use crate::server::base::Server; 2 | use pyo3::prelude::*; 3 | 4 | #[cfg(target_os = "linux")] 5 | use nix::unistd; 6 | 7 | /// An open TUN interface. 8 | /// 9 | /// A new tun interface can be created by calling `create_tun_interface`. 10 | #[pyclass(module = "mitmproxy_rs.tun")] 11 | #[derive(Debug)] 12 | pub struct TunInterface { 13 | tun_name: String, 14 | server: Server, 15 | } 16 | 17 | #[pymethods] 18 | impl TunInterface { 19 | /// Get the tunnel interface name. 20 | pub fn tun_name(&self) -> &str { 21 | &self.tun_name 22 | } 23 | 24 | /// Request the interface to be closed. 25 | pub fn close(&mut self) { 26 | self.server.close() 27 | } 28 | 29 | /// Wait until the interface has shut down. 30 | pub fn wait_closed<'p>(&self, py: Python<'p>) -> PyResult> { 31 | self.server.wait_closed(py) 32 | } 33 | 34 | /// Returns a `str` describing why tun mode is unavailable, or `None` if TUN mode is available. 35 | /// 36 | /// Reasons for unavailability may be an unsupported platform, or missing privileges. 37 | #[staticmethod] 38 | pub fn unavailable_reason() -> Option { 39 | #[cfg(target_os = "linux")] 40 | if !unistd::geteuid().is_root() { 41 | Some(String::from("mitmproxy is not running as root")) 42 | } else { 43 | None 44 | } 45 | 46 | #[cfg(not(target_os = "linux"))] 47 | Some(String::from("OS not supported for TUN proxy mode")) 48 | } 49 | 50 | pub fn __repr__(&self) -> String { 51 | format!("TunInterface({})", self.tun_name) 52 | } 53 | } 54 | 55 | /// Create a TUN interface that is configured with the given parameters: 56 | /// 57 | /// - `handle_tcp_stream`: An async function that will be called for each new TCP `Stream`. 58 | /// - `handle_udp_stream`: An async function that will be called for each new UDP `Stream`. 59 | /// - `tun_name`: An optional string to specify the tunnel name. By default, tun0, ... will be used. 60 | /// 61 | /// *Availability: Linux* 62 | #[pyfunction] 63 | #[allow(unused_variables)] 64 | #[pyo3(signature = (handle_tcp_stream, handle_udp_stream, tun_name=None))] 65 | pub fn create_tun_interface( 66 | py: Python<'_>, 67 | handle_tcp_stream: PyObject, 68 | handle_udp_stream: PyObject, 69 | tun_name: Option, 70 | ) -> PyResult> { 71 | #[cfg(target_os = "linux")] 72 | { 73 | let conf = mitmproxy::packet_sources::tun::TunConf { tun_name }; 74 | pyo3_async_runtimes::tokio::future_into_py(py, async move { 75 | let (server, tun_name) = 76 | Server::init(conf, handle_tcp_stream, handle_udp_stream).await?; 77 | Ok(TunInterface { server, tun_name }) 78 | }) 79 | } 80 | #[cfg(not(target_os = "linux"))] 81 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 82 | TunInterface::unavailable_reason(), 83 | )) 84 | } 85 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/server/udp.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, SocketAddr}; 2 | 3 | use mitmproxy::packet_sources::udp::UdpConf; 4 | 5 | use crate::server::base::Server; 6 | use pyo3::prelude::*; 7 | 8 | /// A running UDP server. 9 | /// 10 | /// A new server can be started by calling `start_udp_server`. 11 | /// The public API is intended to be similar to the API provided by 12 | /// [`asyncio.Server`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Server) 13 | /// from the Python standard library. 14 | #[pyclass(module = "mitmproxy_rs.udp")] 15 | #[derive(Debug)] 16 | pub struct UdpServer { 17 | /// local address of the UDP socket 18 | local_addr: SocketAddr, 19 | server: Server, 20 | } 21 | 22 | #[pymethods] 23 | impl UdpServer { 24 | /// Request the server to gracefully shut down. 25 | /// 26 | /// The server will stop accepting new connections on its UDP socket, but will flush pending 27 | /// outgoing data before shutting down. 28 | pub fn close(&mut self) { 29 | self.server.close() 30 | } 31 | 32 | /// Wait until the server has shut down. 33 | /// 34 | /// This coroutine will yield once pending data has been flushed and all server tasks have 35 | /// successfully terminated after calling the `Server.close` method. 36 | pub fn wait_closed<'p>(&self, py: Python<'p>) -> PyResult> { 37 | self.server.wait_closed(py) 38 | } 39 | 40 | /// Get the local socket address that the UDP server is listening on. 41 | pub fn getsockname(&self) -> (String, u16) { 42 | (self.local_addr.ip().to_string(), self.local_addr.port()) 43 | } 44 | 45 | pub fn __repr__(&self) -> String { 46 | format!("UdpServer({})", self.local_addr) 47 | } 48 | } 49 | 50 | /// Start a UDP server that is configured with the given parameters: 51 | /// 52 | /// - `host`: The host IP address. 53 | /// - `port`: The listen port. 54 | /// - `handle_udp_stream`: An async function that will be called for each new UDP `Stream`. 55 | #[pyfunction] 56 | pub fn start_udp_server( 57 | py: Python<'_>, 58 | host: IpAddr, 59 | port: u16, 60 | handle_udp_stream: PyObject, 61 | ) -> PyResult> { 62 | let conf = UdpConf { 63 | listen_addr: SocketAddr::from((host, port)), 64 | }; 65 | let handle_tcp_stream = py.None(); 66 | pyo3_async_runtimes::tokio::future_into_py(py, async move { 67 | let (server, local_addr) = Server::init(conf, handle_tcp_stream, handle_udp_stream).await?; 68 | Ok(UdpServer { server, local_addr }) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/server/wireguard.rs: -------------------------------------------------------------------------------- 1 | use std::net::{IpAddr, SocketAddr}; 2 | 3 | use crate::util::string_to_key; 4 | 5 | use mitmproxy::packet_sources::wireguard::WireGuardConf; 6 | 7 | use pyo3::prelude::*; 8 | 9 | use boringtun::x25519::PublicKey; 10 | 11 | use crate::server::base::Server; 12 | 13 | /// A running WireGuard server. 14 | /// 15 | /// A new server can be started by calling `start_udp_server`. 16 | /// The public API is intended to be similar to the API provided by 17 | /// [`asyncio.Server`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.Server) 18 | /// from the Python standard library. 19 | #[pyclass(module = "mitmproxy_rs.wireguard")] 20 | #[derive(Debug)] 21 | pub struct WireGuardServer { 22 | /// local address of the WireGuard UDP socket 23 | local_addr: SocketAddr, 24 | server: Server, 25 | } 26 | 27 | #[pymethods] 28 | impl WireGuardServer { 29 | /// Request the WireGuard server to gracefully shut down. 30 | /// 31 | /// The server will stop accepting new connections on its UDP socket, but will flush pending 32 | /// outgoing data before shutting down. 33 | pub fn close(&mut self) { 34 | self.server.close() 35 | } 36 | 37 | /// Wait until the WireGuard server has shut down. 38 | /// 39 | /// This coroutine will yield once pending data has been flushed and all server tasks have 40 | /// successfully terminated after calling the `Server.close` method. 41 | pub fn wait_closed<'p>(&self, py: Python<'p>) -> PyResult> { 42 | self.server.wait_closed(py) 43 | } 44 | 45 | /// Get the local socket address that the WireGuard server is listening on. 46 | pub fn getsockname(&self) -> (String, u16) { 47 | (self.local_addr.ip().to_string(), self.local_addr.port()) 48 | } 49 | 50 | pub fn __repr__(&self) -> String { 51 | format!("WireGuardServer({})", self.local_addr) 52 | } 53 | } 54 | 55 | /// Start a WireGuard server that is configured with the given parameters: 56 | /// 57 | /// - `host`: The host address for the WireGuard UDP socket. 58 | /// - `port`: The listen port for the WireGuard server. The default port for WireGuard is `51820`. 59 | /// - `private_key`: The private X25519 key for the WireGuard server as a base64-encoded string. 60 | /// - `peer_public_keys`: List of public X25519 keys for WireGuard peers as base64-encoded strings. 61 | /// - `handle_tcp_stream`: An async function that will be called for each new TCP `Stream`. 62 | /// - `handle_udp_stream`: An async function that will be called for each new UDP `Stream`. 63 | #[pyfunction] 64 | pub fn start_wireguard_server( 65 | py: Python<'_>, 66 | host: IpAddr, 67 | port: u16, 68 | private_key: String, 69 | peer_public_keys: Vec, 70 | handle_tcp_stream: PyObject, 71 | handle_udp_stream: PyObject, 72 | ) -> PyResult> { 73 | let private_key = string_to_key(private_key)?; 74 | let peer_public_keys = peer_public_keys 75 | .into_iter() 76 | .map(string_to_key) 77 | .collect::>>()?; 78 | let conf = WireGuardConf { 79 | listen_addr: SocketAddr::from((host, port)), 80 | private_key, 81 | peer_public_keys, 82 | }; 83 | pyo3_async_runtimes::tokio::future_into_py(py, async move { 84 | let (server, local_addr) = Server::init(conf, handle_tcp_stream, handle_udp_stream).await?; 85 | Ok(WireGuardServer { server, local_addr }) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/syntax_highlight.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use anyhow::{anyhow, Result}; 3 | use std::str::FromStr; 4 | 5 | use mitmproxy_highlight::{Language, Tag}; 6 | use pyo3::{exceptions::PyValueError, prelude::*}; 7 | 8 | /// Transform text into a list of tagged chunks. 9 | /// 10 | /// Example: 11 | /// 12 | /// ```python 13 | /// from mitmproxy_rs.syntax_highlight import highlight 14 | /// highlighted = highlight("key: 42", "yaml") 15 | /// print(highlighted) # [('name', 'key'), ('', ': '), ('number', '42')] 16 | /// ``` 17 | #[pyfunction] 18 | pub fn highlight(text: String, language: &str) -> PyResult> { 19 | let language = Language::from_str(language)?; 20 | language 21 | .highlight(text.as_bytes()) 22 | .map(|chunks| { 23 | chunks 24 | .into_iter() 25 | .map(|(tag, text)| (tag.as_str(), text)) 26 | .collect() 27 | }) 28 | .map_err(|e| PyValueError::new_err(format!("{:?}", e))) 29 | } 30 | 31 | /// Return the list of all possible syntax highlight tags. 32 | #[pyfunction] 33 | pub fn tags() -> PyResult> { 34 | Ok(Tag::VALUES 35 | .iter() 36 | .map(|tag| tag.as_str()) 37 | .filter(|&x| !x.is_empty()) 38 | .collect()) 39 | } 40 | 41 | /// Return the list of all supported languages for syntax highlighting. 42 | #[pyfunction] 43 | pub fn languages() -> PyResult> { 44 | Ok(Language::VALUES.iter().map(|lang| lang.as_str()).collect()) 45 | } 46 | -------------------------------------------------------------------------------- /mitmproxy-rs/src/util.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use anyhow::{anyhow, Result}; 3 | use data_encoding::BASE64; 4 | #[cfg(target_os = "macos")] 5 | use mitmproxy::certificates; 6 | 7 | use pyo3::exceptions::PyOSError; 8 | use pyo3::{exceptions::PyValueError, prelude::*, IntoPyObjectExt}; 9 | use rand_core::OsRng; 10 | 11 | use std::net::SocketAddr; 12 | 13 | use boringtun::x25519::{PublicKey, StaticSecret}; 14 | use tokio::sync::mpsc; 15 | 16 | pub fn string_to_key(data: String) -> PyResult 17 | where 18 | T: From<[u8; 32]>, 19 | { 20 | BASE64 21 | .decode(data.as_bytes()) 22 | .ok() 23 | .and_then(|bytes| <[u8; 32]>::try_from(bytes).ok()) 24 | .map(T::from) 25 | .ok_or_else(|| PyValueError::new_err("Invalid key.")) 26 | } 27 | 28 | pub fn socketaddr_to_py(py: Python, s: SocketAddr) -> PyResult { 29 | (s.ip().to_string(), s.port()).into_py_any(py) 30 | } 31 | 32 | pub fn event_queue_unavailable(_: mpsc::error::SendError) -> PyErr { 33 | PyOSError::new_err("Server has been shut down.") 34 | } 35 | 36 | /// Generate a WireGuard private key, analogous to the `wg genkey` command. 37 | #[pyfunction] 38 | pub fn genkey() -> String { 39 | BASE64.encode(&StaticSecret::random_from_rng(OsRng).to_bytes()) 40 | } 41 | 42 | /// Derive a WireGuard public key from a private key, analogous to the `wg pubkey` command. 43 | #[pyfunction] 44 | pub fn pubkey(private_key: String) -> PyResult { 45 | let private_key: StaticSecret = string_to_key(private_key)?; 46 | Ok(BASE64.encode(PublicKey::from(&private_key).as_bytes())) 47 | } 48 | 49 | /// Convert pem certificate to der certificate and add it to macOS keychain. 50 | #[pyfunction] 51 | #[allow(unused_variables)] 52 | pub fn add_cert(py: Python<'_>, pem: String) -> PyResult<()> { 53 | #[cfg(target_os = "macos")] 54 | { 55 | let pem_body = pem 56 | .lines() 57 | .skip(1) 58 | .take_while(|&line| line != "-----END CERTIFICATE-----") 59 | .collect::(); 60 | 61 | let filename = py.import("mitmproxy_rs")?.filename()?; 62 | let executable_path = std::path::Path::new(filename.to_str()?) 63 | .parent() 64 | .ok_or_else(|| anyhow!("invalid path"))? 65 | .join("macos-certificate-truster.app"); 66 | if !executable_path.exists() { 67 | return Err(anyhow!("{} does not exist", executable_path.display()).into()); 68 | } 69 | let der = BASE64.decode(pem_body.as_bytes()).unwrap(); 70 | match certificates::add_cert(der, executable_path.to_str().unwrap()) { 71 | Ok(_) => Ok(()), 72 | Err(e) => Err(PyErr::new::(format!( 73 | "Failed to add certificate: {:?}", 74 | e 75 | ))), 76 | } 77 | } 78 | #[cfg(not(target_os = "macos"))] 79 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 80 | "OS proxy mode is only available on macos", 81 | )) 82 | } 83 | 84 | /// Delete mitmproxy certificate from the keychain. 85 | #[pyfunction] 86 | pub fn remove_cert() -> PyResult<()> { 87 | #[cfg(target_os = "macos")] 88 | { 89 | match certificates::remove_cert() { 90 | Ok(_) => Ok(()), 91 | Err(e) => Err(PyErr::new::(format!( 92 | "Failed to remove certificate: {:?}", 93 | e 94 | ))), 95 | } 96 | } 97 | #[cfg(not(target_os = "macos"))] 98 | Err(pyo3::exceptions::PyNotImplementedError::new_err( 99 | "OS proxy mode is only available on macos", 100 | )) 101 | } 102 | -------------------------------------------------------------------------------- /mitmproxy-rs/stubtest-allowlist.txt: -------------------------------------------------------------------------------- 1 | mitmproxy_rs.mitmproxy_rs 2 | mitmproxy_rs._pyinstaller.hook-mitmproxy_rs 3 | mitmproxy_rs._pyinstaller.hook-mitmproxy_windows 4 | mitmproxy_rs._pyinstaller.hook-mitmproxy_macos 5 | mitmproxy_rs._pyinstaller.hook-mitmproxy_linux 6 | mitmproxy_rs.T 7 | mitmproxy_rs.dns.DnsResolver.__init__ 8 | -------------------------------------------------------------------------------- /mitmproxy-windows/README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy-windows 2 | 3 | This package contains the following precompiled binaries for Windows: 4 | 5 | - `windows-redirector.exe`: A Rust executable that redirects traffic to mitmproxy via a Windows named pipe. 6 | - A vendored copy of [WinDivert](https://reqrypt.org/windivert.html), used by the redirector. 7 | 8 | 9 | ## Redirector Development Setup 10 | 11 | 1. Run `pip install -e .` to install `mitmproxy_windows` as editable. 12 | 2. Run something along the lines of `mitmdump --mode local:curl`. 13 | You should see a `Development mode: Compiling windows-redirector.exe...` message. 14 | -------------------------------------------------------------------------------- /mitmproxy-windows/mitmproxy_windows/WINDIVERT_VERSION: -------------------------------------------------------------------------------- 1 | 2.2.2 2 | -------------------------------------------------------------------------------- /mitmproxy-windows/mitmproxy_windows/WinDivert.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-windows/mitmproxy_windows/WinDivert.dll -------------------------------------------------------------------------------- /mitmproxy-windows/mitmproxy_windows/WinDivert.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-windows/mitmproxy_windows/WinDivert.lib -------------------------------------------------------------------------------- /mitmproxy-windows/mitmproxy_windows/WinDivert64.sys: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-windows/mitmproxy_windows/WinDivert64.sys -------------------------------------------------------------------------------- /mitmproxy-windows/mitmproxy_windows/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def executable_path() -> Path: 5 | """ 6 | Return the Path for windows-redirector.exe. 7 | 8 | For shipped wheels this is just the file in the package, 9 | for development setups this may invoke cargo to build it. 10 | """ 11 | here = Path(__file__).parent.absolute() 12 | exe = here / "windows-redirector.exe" 13 | 14 | # Development path: This should never happen with precompiled wheels. 15 | if not exe.exists() and (here / "../Cargo.toml").exists(): 16 | import logging 17 | import shutil 18 | import subprocess 19 | 20 | logger = logging.getLogger(__name__) 21 | logger.warning("Development mode: Compiling windows-redirector.exe...") 22 | 23 | # Build Redirector 24 | subprocess.run(["cargo", "build"], cwd=here.parent / "redirector", check=True) 25 | # Copy WinDivert to target/debug/ 26 | target_debug = here.parent.parent / "target/debug" 27 | for f in ["WinDivert.dll", "WinDivert.lib", "WinDivert64.sys"]: 28 | if not (target_debug / f).exists(): 29 | shutil.copy(here / f, target_debug / f) 30 | 31 | logger.warning("Development mode: Using target/debug/windows-redirector.exe...") 32 | exe = target_debug / "windows-redirector.exe" 33 | 34 | return exe 35 | -------------------------------------------------------------------------------- /mitmproxy-windows/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mitmproxy-windows" 7 | dynamic = ["version"] 8 | license = "LGPL-3.0-or-later" 9 | requires-python = ">=3.12" 10 | readme = "README.md" 11 | 12 | [project.urls] 13 | Source = "https://github.com/mitmproxy/mitmproxy-rs" 14 | 15 | [tool.hatch.build] 16 | only-include = ["mitmproxy_windows"] 17 | 18 | [tool.hatch.version] 19 | path = "../Cargo.toml" 20 | pattern = "version = \"(?P.+?)\"" 21 | 22 | [tool.hatch.build.force-include] 23 | "../target/release/windows-redirector.exe" = "mitmproxy_windows/windows-redirector.exe" 24 | -------------------------------------------------------------------------------- /mitmproxy-windows/redirector/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "windows-redirector" 3 | license = "LGPL-3.0-or-later" 4 | authors.workspace = true 5 | version.workspace = true 6 | repository.workspace = true 7 | edition.workspace = true 8 | rust-version.workspace = true 9 | publish.workspace = true 10 | 11 | [lints] 12 | workspace = true 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [target.'cfg(windows)'.dependencies] 17 | mitmproxy = { path = "../../" } 18 | tokio = { version = "1.45", features = ["macros", "net", "rt-multi-thread", "sync", "io-util"] } 19 | anyhow = { version = "1.0.97", features = ["backtrace"] } 20 | windivert = "0.6.0" 21 | lru_time_cache = "0.11.11" 22 | log = "0.4.27" 23 | env_logger = "0.11.5" 24 | prost = "0.13.5" 25 | internet-packet = { version = "0.2.2", features = ["checksums"] } 26 | 27 | [target.'cfg(windows)'.dev-dependencies] 28 | hex = "0.4.3" 29 | 30 | [target.'cfg(windows)'.build-dependencies] 31 | winres = "0.1.12" 32 | 33 | [package.metadata.winres] 34 | ProductName = "Mitmproxy Redirector" 35 | FileDescription = "Transparently redirect traffic to a mitmproxy instance" 36 | -------------------------------------------------------------------------------- /mitmproxy-windows/redirector/README.md: -------------------------------------------------------------------------------- 1 | # windows-redirector 2 | 3 | `windows-redirector.exe` spawns with elevated privileges and 4 | redirects traffic to mitmproxy via a Windows named pipe. 5 | -------------------------------------------------------------------------------- /mitmproxy-windows/redirector/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | fn main() { 3 | let mut res = winres::WindowsResource::new(); 4 | res.set_manifest( 5 | r#" 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | "#, 16 | ); 17 | res.set_icon("mitmproxy.ico"); 18 | res.compile().unwrap(); 19 | } 20 | 21 | #[cfg(not(windows))] 22 | fn main() {} 23 | -------------------------------------------------------------------------------- /mitmproxy-windows/redirector/mitmproxy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mitmproxy/mitmproxy_rs/25a085156e717dc6d7477af77cd1b9c2f00d26db/mitmproxy-windows/redirector/mitmproxy.ico -------------------------------------------------------------------------------- /mitmproxy-windows/redirector/src/main.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | include!("main2.rs"); 3 | 4 | #[cfg(not(windows))] 5 | pub fn main() { 6 | panic!("The Windows redirector works on Windows only."); 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/process-list.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(any(windows, target_os = "macos")))] 2 | fn main() { 3 | unimplemented!(); 4 | } 5 | 6 | #[cfg(any(windows, target_os = "macos"))] 7 | use mitmproxy::processes; 8 | 9 | #[cfg(any(windows, target_os = "macos"))] 10 | fn main() -> anyhow::Result<()> { 11 | let mut processes = processes::active_executables()?; 12 | processes.sort_by_cached_key(|p| (p.is_system, !p.is_visible)); 13 | 14 | let mut icon_cache = processes::ICON_CACHE.lock().unwrap(); 15 | 16 | println!( 17 | r#" 18 | 19 | 20 | 21 | 22 | 23 | 24 | "# 25 | ); 26 | for process in processes { 27 | let image = if !process.is_system && process.is_visible { 28 | match icon_cache.get_png(process.executable.clone()) { 29 | Ok(data) => { 30 | let data = data_encoding::BASE64.encode(data); 31 | format!("") 32 | } 33 | Err(e) => e.to_string(), 34 | } 35 | } else { 36 | "".to_string() 37 | }; 38 | println!( 39 | r#" 40 | 41 | 42 | 43 | 44 | 45 | 46 | "#, 47 | image, 48 | process.display_name, 49 | process.is_visible, 50 | process.is_system, 51 | process.executable.to_string_lossy(), 52 | ); 53 | } 54 | println!("
Icondisplay_nameis_visibleis_systemexecutable
{}{}{}{}{}
"); 55 | Ok(()) 56 | } 57 | -------------------------------------------------------------------------------- /src/certificates/macos.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use security_framework::{ 3 | certificate::SecCertificate, 4 | item::{ 5 | AddRef, ItemAddOptions, ItemAddValue, ItemClass, ItemSearchOptions, Reference, SearchResult, 6 | }, 7 | }; 8 | use tokio::process::Command; 9 | 10 | pub fn add_cert(der: Vec, path: &str) -> Result<()> { 11 | let cert = SecCertificate::from_der(&der)?; 12 | let add_ref = AddRef::Certificate(cert); 13 | let mut add_option = ItemAddOptions::new(ItemAddValue::Ref(add_ref)); 14 | add_option.set_label("mitmproxy"); 15 | 16 | let search_result = ItemSearchOptions::new() 17 | .class(ItemClass::certificate()) 18 | .load_refs(true) 19 | .label("mitmproxy") 20 | .search() 21 | .map_err(|e| anyhow!(e))?; 22 | 23 | if let Some(SearchResult::Ref(Reference::Certificate(cert))) = search_result.first() { 24 | cert.delete()?; 25 | } 26 | 27 | add_option.add()?; 28 | 29 | Command::new("open") 30 | .arg(path) 31 | .spawn() 32 | .map_err(|e| anyhow!(e))?; 33 | Ok(()) 34 | } 35 | 36 | pub fn remove_cert() -> Result<()> { 37 | if let SearchResult::Ref(Reference::Certificate(cert)) = ItemSearchOptions::new() 38 | .class(ItemClass::certificate()) 39 | .load_refs(true) 40 | .label("mitmproxy") 41 | .search() 42 | .map_err(|e| anyhow!(e))? 43 | .first() 44 | .ok_or_else(|| anyhow!("Certificate not found"))? 45 | { 46 | cert.delete().map_err(|e| anyhow!(e))?; 47 | }; 48 | Ok(()) 49 | } 50 | -------------------------------------------------------------------------------- /src/certificates/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | mod macos; 3 | #[cfg(target_os = "macos")] 4 | pub use self::macos::{add_cert, remove_cert}; 5 | -------------------------------------------------------------------------------- /src/ipc/mitmproxy_ipc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package mitmproxy_ipc; 4 | 5 | // Note: The protobuf definition is shared between the Rust and Swift parts. 6 | // We are not using prost-build because providing protoc is a hassle on many platforms. 7 | // See .github/workflows/autofix.yml for how to update the respective files, 8 | // or file a PR and let CI handle it. 9 | 10 | // Packet with associated tunnel info (Windows pipe to mitmproxy) 11 | message PacketWithMeta { 12 | bytes data = 1; 13 | TunnelInfo tunnel_info = 2; 14 | } 15 | message TunnelInfo { 16 | optional uint32 pid = 1; 17 | optional string process_name = 2; 18 | } 19 | 20 | // Packet or intercept spec (Windows pipe to redirector) 21 | message FromProxy { 22 | oneof message { 23 | Packet packet = 1; 24 | InterceptConf intercept_conf = 2; 25 | } 26 | } 27 | // Packet (macOS UDP Stream) 28 | // ⚠️ Bump network extension version on changes, https://github.com/mitmproxy/mitmproxy_rs/pull/227. 29 | message Packet { 30 | bytes data = 1; 31 | } 32 | // Intercept conf (macOS Control Stream) 33 | // ⚠️ Bump network extension version on changes, https://github.com/mitmproxy/mitmproxy_rs/pull/227. 34 | message InterceptConf { 35 | repeated string actions = 1; 36 | } 37 | // New flow (macOS TCP/UDP Stream) 38 | // ⚠️ Bump network extension version on changes, https://github.com/mitmproxy/mitmproxy_rs/pull/227. 39 | message NewFlow { 40 | oneof message { 41 | TcpFlow tcp = 1; 42 | UdpFlow udp = 2; 43 | } 44 | } 45 | // ⚠️ Bump network extension version on changes, https://github.com/mitmproxy/mitmproxy_rs/pull/227. 46 | message TcpFlow { 47 | Address remote_address = 1; 48 | TunnelInfo tunnel_info = 2; 49 | } 50 | // ⚠️ Bump network extension version on changes, https://github.com/mitmproxy/mitmproxy_rs/pull/227. 51 | message UdpFlow { 52 | optional Address local_address = 1; 53 | TunnelInfo tunnel_info = 3; 54 | } 55 | message UdpPacket { 56 | bytes data = 1; 57 | Address remote_address = 2; 58 | } 59 | message Address { 60 | string host = 1; 61 | uint32 port = 2; 62 | } -------------------------------------------------------------------------------- /src/ipc/mitmproxy_ipc.rs: -------------------------------------------------------------------------------- 1 | // @generated 2 | // This file is @generated by prost-build. 3 | // Note: The protobuf definition is shared between the Rust and Swift parts. 4 | // We are not using prost-build because providing protoc is a hassle on many platforms. 5 | // See .github/workflows/autofix.yml for how to update the respective files, 6 | // or file a PR and let CI handle it. 7 | 8 | /// Packet with associated tunnel info (Windows pipe to mitmproxy) 9 | #[derive(Clone, PartialEq, ::prost::Message)] 10 | pub struct PacketWithMeta { 11 | #[prost(bytes = "bytes", tag = "1")] 12 | pub data: ::prost::bytes::Bytes, 13 | #[prost(message, optional, tag = "2")] 14 | pub tunnel_info: ::core::option::Option, 15 | } 16 | #[derive(Clone, PartialEq, ::prost::Message)] 17 | pub struct TunnelInfo { 18 | #[prost(uint32, optional, tag = "1")] 19 | pub pid: ::core::option::Option, 20 | #[prost(string, optional, tag = "2")] 21 | pub process_name: ::core::option::Option<::prost::alloc::string::String>, 22 | } 23 | /// Packet or intercept spec (Windows pipe to redirector) 24 | #[derive(Clone, PartialEq, ::prost::Message)] 25 | pub struct FromProxy { 26 | #[prost(oneof = "from_proxy::Message", tags = "1, 2")] 27 | pub message: ::core::option::Option, 28 | } 29 | /// Nested message and enum types in `FromProxy`. 30 | pub mod from_proxy { 31 | #[derive(Clone, PartialEq, ::prost::Oneof)] 32 | pub enum Message { 33 | #[prost(message, tag = "1")] 34 | Packet(super::Packet), 35 | #[prost(message, tag = "2")] 36 | InterceptConf(super::InterceptConf), 37 | } 38 | } 39 | /// Packet (macOS UDP Stream) 40 | /// ⚠️ Bump network extension version on changes, 41 | #[derive(Clone, PartialEq, ::prost::Message)] 42 | pub struct Packet { 43 | #[prost(bytes = "bytes", tag = "1")] 44 | pub data: ::prost::bytes::Bytes, 45 | } 46 | /// Intercept conf (macOS Control Stream) 47 | /// ⚠️ Bump network extension version on changes, 48 | #[derive(Clone, PartialEq, ::prost::Message)] 49 | pub struct InterceptConf { 50 | #[prost(string, repeated, tag = "1")] 51 | pub actions: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, 52 | } 53 | /// New flow (macOS TCP/UDP Stream) 54 | /// ⚠️ Bump network extension version on changes, 55 | #[derive(Clone, PartialEq, ::prost::Message)] 56 | pub struct NewFlow { 57 | #[prost(oneof = "new_flow::Message", tags = "1, 2")] 58 | pub message: ::core::option::Option, 59 | } 60 | /// Nested message and enum types in `NewFlow`. 61 | pub mod new_flow { 62 | #[derive(Clone, PartialEq, ::prost::Oneof)] 63 | pub enum Message { 64 | #[prost(message, tag = "1")] 65 | Tcp(super::TcpFlow), 66 | #[prost(message, tag = "2")] 67 | Udp(super::UdpFlow), 68 | } 69 | } 70 | /// ⚠️ Bump network extension version on changes, 71 | #[derive(Clone, PartialEq, ::prost::Message)] 72 | pub struct TcpFlow { 73 | #[prost(message, optional, tag = "1")] 74 | pub remote_address: ::core::option::Option
, 75 | #[prost(message, optional, tag = "2")] 76 | pub tunnel_info: ::core::option::Option, 77 | } 78 | /// ⚠️ Bump network extension version on changes, 79 | #[derive(Clone, PartialEq, ::prost::Message)] 80 | pub struct UdpFlow { 81 | #[prost(message, optional, tag = "1")] 82 | pub local_address: ::core::option::Option
, 83 | #[prost(message, optional, tag = "3")] 84 | pub tunnel_info: ::core::option::Option, 85 | } 86 | #[derive(Clone, PartialEq, ::prost::Message)] 87 | pub struct UdpPacket { 88 | #[prost(bytes = "bytes", tag = "1")] 89 | pub data: ::prost::bytes::Bytes, 90 | #[prost(message, optional, tag = "2")] 91 | pub remote_address: ::core::option::Option
, 92 | } 93 | #[derive(Clone, PartialEq, ::prost::Message)] 94 | pub struct Address { 95 | #[prost(string, tag = "1")] 96 | pub host: ::prost::alloc::string::String, 97 | #[prost(uint32, tag = "2")] 98 | pub port: u32, 99 | } 100 | // @@protoc_insertion_point(module) 101 | -------------------------------------------------------------------------------- /src/ipc/mod.rs: -------------------------------------------------------------------------------- 1 | mod mitmproxy_ipc; 2 | pub use mitmproxy_ipc::*; 3 | 4 | use crate::intercept_conf; 5 | use std::net::{AddrParseError, IpAddr, SocketAddr}; 6 | use std::str::FromStr; 7 | 8 | impl TryFrom<&Address> for SocketAddr { 9 | type Error = AddrParseError; 10 | 11 | fn try_from(address: &Address) -> Result { 12 | // The macOS network system extension may return IP addresses with scope string. 13 | let host = address.host.split('%').next().unwrap(); 14 | let ip = IpAddr::from_str(host)?; 15 | Ok(SocketAddr::from((ip, address.port as u16))) 16 | } 17 | } 18 | impl From for Address { 19 | fn from(val: SocketAddr) -> Self { 20 | Address { 21 | host: val.ip().to_string(), 22 | port: val.port() as u32, 23 | } 24 | } 25 | } 26 | 27 | impl From for InterceptConf { 28 | fn from(conf: intercept_conf::InterceptConf) -> Self { 29 | InterceptConf { 30 | actions: conf.actions(), 31 | } 32 | } 33 | } 34 | 35 | impl TryFrom for intercept_conf::InterceptConf { 36 | type Error = anyhow::Error; 37 | 38 | fn try_from(conf: InterceptConf) -> Result { 39 | intercept_conf::InterceptConf::try_from(conf.actions) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use super::*; 46 | 47 | #[test] 48 | fn test_socketaddr_from_address() { 49 | let a = Address { 50 | host: "fe80::f0ff:88ff:febc:3df5".to_string(), 51 | port: 8080, 52 | }; 53 | assert!(SocketAddr::try_from(&a).is_ok()); 54 | 55 | let b = Address { 56 | host: "invalid".to_string(), 57 | port: 8080, 58 | }; 59 | assert!(SocketAddr::try_from(&b).is_err()); 60 | 61 | let c = Address { 62 | host: "fe80::f0ff:88ff:febc:3df5%awdl0".to_string(), 63 | port: 8080, 64 | }; 65 | assert!(SocketAddr::try_from(&c).is_ok()); 66 | assert_eq!(SocketAddr::try_from(&a), SocketAddr::try_from(&c)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use network::MAX_PACKET_SIZE; 2 | 3 | pub mod certificates; 4 | pub mod dns; 5 | pub mod intercept_conf; 6 | pub mod ipc; 7 | pub mod messages; 8 | pub mod network; 9 | pub mod packet_sources; 10 | pub mod processes; 11 | pub mod shutdown; 12 | #[cfg(windows)] 13 | pub mod windows; 14 | -------------------------------------------------------------------------------- /src/network/core.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::fmt; 3 | 4 | use std::time::Duration; 5 | 6 | use anyhow::Result; 7 | 8 | use smoltcp::wire::IpProtocol; 9 | use tokio::sync::mpsc::{Permit, Sender}; 10 | 11 | use crate::messages::{NetworkCommand, NetworkEvent, SmolPacket, TransportCommand, TransportEvent}; 12 | use crate::network::icmp::{handle_icmpv4_echo_request, handle_icmpv6_echo_request}; 13 | 14 | use crate::network::tcp::TcpHandler; 15 | use crate::network::udp::{UdpHandler, UdpPacket}; 16 | 17 | pub struct NetworkStack<'a> { 18 | tcp: TcpHandler<'a>, 19 | udp: UdpHandler, 20 | net_tx: Sender, 21 | } 22 | 23 | impl NetworkStack<'_> { 24 | pub fn new(net_tx: Sender) -> Self { 25 | Self { 26 | tcp: TcpHandler::new(net_tx.clone()), 27 | udp: UdpHandler::new(), 28 | net_tx, 29 | } 30 | } 31 | 32 | pub fn handle_network_event( 33 | &mut self, 34 | event: NetworkEvent, 35 | permit: Permit<'_, TransportEvent>, 36 | ) -> Result<()> { 37 | let (packet, tunnel_info) = match event { 38 | NetworkEvent::ReceivePacket { 39 | packet, 40 | tunnel_info, 41 | } => (packet, tunnel_info), 42 | }; 43 | 44 | if let SmolPacket::V4(p) = &packet { 45 | if !p.verify_checksum() { 46 | log::warn!("Received invalid IP packet (checksum error)."); 47 | return Ok(()); 48 | } 49 | } 50 | 51 | match packet.transport_protocol() { 52 | IpProtocol::Tcp => self.tcp.receive_packet(packet, tunnel_info, permit), 53 | IpProtocol::Udp => { 54 | match UdpPacket::try_from(packet) { 55 | Ok(packet) => self.udp.receive_data(packet, tunnel_info, permit), 56 | Err(e) => log::debug!("Received invalid UDP packet: {}", e), 57 | }; 58 | Ok(()) 59 | } 60 | IpProtocol::Icmp => self.receive_packet_icmp(packet), 61 | IpProtocol::Icmpv6 => self.receive_packet_icmp(packet), 62 | _ => { 63 | log::debug!( 64 | "Received IP packet for unknown protocol: {}", 65 | packet.transport_protocol() 66 | ); 67 | Ok(()) 68 | } 69 | } 70 | } 71 | 72 | fn receive_packet_icmp(&mut self, packet: SmolPacket) -> Result<()> { 73 | // Some apps check network connectivity by sending ICMP pings. ICMP traffic is currently 74 | // swallowed by mitmproxy_rs, which makes them believe that there is no network connectivity. 75 | // Generating fake ICMP replies as a simple workaround. 76 | 77 | if let Ok(permit) = self.net_tx.try_reserve() { 78 | // Generating and sending fake replies for ICMP echo requests. Ignoring all other ICMP types. 79 | let response_packet = match packet { 80 | SmolPacket::V4(packet) => handle_icmpv4_echo_request(packet), 81 | SmolPacket::V6(packet) => handle_icmpv6_echo_request(packet), 82 | }; 83 | if let Some(response_packet) = response_packet { 84 | permit.send(NetworkCommand::SendPacket(response_packet)); 85 | } 86 | } else { 87 | log::debug!("Channel full, discarding ICMP packet."); 88 | } 89 | Ok(()) 90 | } 91 | 92 | pub fn handle_transport_command(&mut self, command: TransportCommand) { 93 | if command.connection_id().is_tcp() { 94 | self.tcp.handle_transport_command(command); 95 | } else if let Some(packet) = self.udp.handle_transport_command(command) { 96 | if self 97 | .net_tx 98 | .try_send(NetworkCommand::SendPacket(SmolPacket::from(packet))) 99 | .is_err() 100 | { 101 | log::debug!("Channel unavailable, discarding UDP packet."); 102 | } 103 | } 104 | } 105 | 106 | pub fn poll_delay(&mut self) -> Option { 107 | match (self.tcp.poll_delay(), self.udp.poll_delay()) { 108 | (Some(a), Some(b)) => Some(min(a, b)), 109 | (Some(a), None) => Some(a), 110 | (None, Some(b)) => Some(b), 111 | (None, None) => None, 112 | } 113 | } 114 | 115 | pub fn poll(&mut self) -> Result<()> { 116 | self.udp.poll(); 117 | self.tcp.poll() 118 | } 119 | } 120 | 121 | impl fmt::Debug for NetworkStack<'_> { 122 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 123 | f.debug_struct("NetworkIO").field("tcp", &self.tcp).finish() 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/network/icmp.rs: -------------------------------------------------------------------------------- 1 | use crate::messages::SmolPacket; 2 | use smoltcp::phy::ChecksumCapabilities; 3 | use smoltcp::wire::{ 4 | Icmpv4Message, Icmpv4Packet, Icmpv4Repr, Icmpv6Message, Icmpv6Packet, Icmpv6Repr, IpProtocol, 5 | Ipv4Packet, Ipv4Repr, Ipv6Packet, Ipv6Repr, 6 | }; 7 | 8 | pub(super) fn handle_icmpv4_echo_request( 9 | mut input_packet: Ipv4Packet>, 10 | ) -> Option { 11 | let src_addr = input_packet.src_addr(); 12 | let dst_addr = input_packet.dst_addr(); 13 | 14 | // Parsing ICMP Packet 15 | let mut input_icmpv4_packet = match Icmpv4Packet::new_checked(input_packet.payload_mut()) { 16 | Ok(p) => p, 17 | Err(e) => { 18 | log::debug!("Received invalid ICMPv4 packet: {}", e); 19 | return None; 20 | } 21 | }; 22 | 23 | // Checking that it is an ICMP Echo Request. 24 | if input_icmpv4_packet.msg_type() != Icmpv4Message::EchoRequest { 25 | log::debug!( 26 | "Unsupported ICMPv4 packet of type: {}", 27 | input_icmpv4_packet.msg_type() 28 | ); 29 | return None; 30 | } 31 | 32 | // Creating fake response packet. 33 | let icmp_repr = Icmpv4Repr::EchoReply { 34 | ident: input_icmpv4_packet.echo_ident(), 35 | seq_no: input_icmpv4_packet.echo_seq_no(), 36 | data: input_icmpv4_packet.data_mut(), 37 | }; 38 | let ip_repr = Ipv4Repr { 39 | // Directing fake reply back to the original source address. 40 | src_addr: dst_addr, 41 | dst_addr: src_addr, 42 | next_header: IpProtocol::Icmp, 43 | payload_len: icmp_repr.buffer_len(), 44 | hop_limit: 255, 45 | }; 46 | let buf = vec![0u8; ip_repr.buffer_len() + icmp_repr.buffer_len()]; 47 | let mut output_ipv4_packet = Ipv4Packet::new_unchecked(buf); 48 | ip_repr.emit(&mut output_ipv4_packet, &ChecksumCapabilities::default()); 49 | let mut output_ip_packet = SmolPacket::from(output_ipv4_packet); 50 | icmp_repr.emit( 51 | &mut Icmpv4Packet::new_unchecked(output_ip_packet.payload_mut()), 52 | &ChecksumCapabilities::default(), 53 | ); 54 | Some(output_ip_packet) 55 | } 56 | 57 | pub(super) fn handle_icmpv6_echo_request( 58 | mut input_packet: Ipv6Packet>, 59 | ) -> Option { 60 | let src_addr = input_packet.src_addr(); 61 | let dst_addr = input_packet.dst_addr(); 62 | 63 | // Parsing ICMP Packet 64 | let mut input_icmpv6_packet = match Icmpv6Packet::new_checked(input_packet.payload_mut()) { 65 | Ok(p) => p, 66 | Err(e) => { 67 | log::debug!("Received invalid ICMPv6 packet: {}", e); 68 | return None; 69 | } 70 | }; 71 | 72 | match input_icmpv6_packet.msg_type() { 73 | Icmpv6Message::EchoRequest => (), 74 | Icmpv6Message::RouterSolicit => { 75 | // These happen in Linux local redirect mode, not investigated any further. 76 | log::debug!("Ignoring ICMPv6 router solicitation."); 77 | return None; 78 | } 79 | other => { 80 | log::debug!("Unsupported ICMPv6 packet of type: {other}"); 81 | return None; 82 | } 83 | } 84 | 85 | // Creating fake response packet. 86 | let icmp_repr = Icmpv6Repr::EchoReply { 87 | ident: input_icmpv6_packet.echo_ident(), 88 | seq_no: input_icmpv6_packet.echo_seq_no(), 89 | data: input_icmpv6_packet.payload_mut(), 90 | }; 91 | let ip_repr = Ipv6Repr { 92 | // Directing fake reply back to the original source address. 93 | src_addr: dst_addr, 94 | dst_addr: src_addr, 95 | next_header: IpProtocol::Icmp, 96 | payload_len: icmp_repr.buffer_len(), 97 | hop_limit: 255, 98 | }; 99 | let buf = vec![0u8; ip_repr.buffer_len() + icmp_repr.buffer_len()]; 100 | let mut output_ipv6_packet = Ipv6Packet::new_unchecked(buf); 101 | ip_repr.emit(&mut output_ipv6_packet); 102 | let mut output_ip_packet = SmolPacket::from(output_ipv6_packet); 103 | icmp_repr.emit( 104 | // Directing fake reply back to the original source address. 105 | &dst_addr, 106 | &src_addr, 107 | &mut Icmpv6Packet::new_unchecked(output_ip_packet.payload_mut()), 108 | &ChecksumCapabilities::default(), 109 | ); 110 | Some(output_ip_packet) 111 | } 112 | -------------------------------------------------------------------------------- /src/network/mod.rs: -------------------------------------------------------------------------------- 1 | mod task; 2 | pub use task::add_network_layer; 3 | pub use task::NetworkTask; 4 | 5 | mod virtual_device; 6 | 7 | mod core; 8 | mod icmp; 9 | mod tcp; 10 | #[cfg(test)] 11 | mod tests; 12 | pub(crate) mod udp; 13 | 14 | pub const MAX_PACKET_SIZE: usize = 65535; 15 | -------------------------------------------------------------------------------- /src/network/virtual_device.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use smoltcp::{ 4 | phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken}, 5 | time::Instant, 6 | }; 7 | use tokio::sync::mpsc::{Permit, Sender}; 8 | 9 | use crate::messages::{NetworkCommand, SmolPacket}; 10 | 11 | /// A virtual smoltcp device into which we manually feed packets using 12 | /// [VirtualDevice::receive_packet] and which send outgoing packets to a channel. 13 | pub struct VirtualDevice { 14 | rx_buffer: VecDeque>, 15 | tx_channel: Sender, 16 | } 17 | 18 | impl VirtualDevice { 19 | pub fn new(tx_channel: Sender) -> Self { 20 | VirtualDevice { 21 | rx_buffer: VecDeque::new(), 22 | tx_channel, 23 | } 24 | } 25 | 26 | pub fn receive_packet(&mut self, packet: SmolPacket) { 27 | self.rx_buffer.push_back(packet.into_inner()); 28 | } 29 | } 30 | 31 | impl Device for VirtualDevice { 32 | type RxToken<'a> 33 | = VirtualRxToken 34 | where 35 | Self: 'a; 36 | type TxToken<'a> 37 | = VirtualTxToken<'a> 38 | where 39 | Self: 'a; 40 | 41 | fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { 42 | if self.rx_buffer.is_empty() { 43 | return None; 44 | } 45 | 46 | if let Ok(permit) = self.tx_channel.try_reserve() { 47 | if let Some(buffer) = self.rx_buffer.pop_front() { 48 | let rx = Self::RxToken { buffer }; 49 | let tx = VirtualTxToken { permit }; 50 | return Some((rx, tx)); 51 | } 52 | } 53 | 54 | None 55 | } 56 | 57 | fn transmit(&mut self, _timestamp: Instant) -> Option> { 58 | match self.tx_channel.try_reserve() { 59 | Ok(permit) => Some(VirtualTxToken { permit }), 60 | Err(_) => None, 61 | } 62 | } 63 | 64 | fn capabilities(&self) -> DeviceCapabilities { 65 | let mut capabilities = DeviceCapabilities::default(); 66 | capabilities.medium = Medium::Ip; 67 | capabilities.max_transmission_unit = 1420; 68 | capabilities 69 | } 70 | } 71 | 72 | pub struct VirtualTxToken<'a> { 73 | permit: Permit<'a, NetworkCommand>, 74 | } 75 | 76 | impl TxToken for VirtualTxToken<'_> { 77 | fn consume(self, len: usize, f: F) -> R 78 | where 79 | F: FnOnce(&mut [u8]) -> R, 80 | { 81 | let mut buffer = vec![0; len]; 82 | let result = f(&mut buffer); 83 | 84 | match SmolPacket::try_from(buffer) { 85 | Ok(packet) => { 86 | self.permit.send(NetworkCommand::SendPacket(packet)); 87 | } 88 | Err(err) => { 89 | log::error!("Failed to parse packet from smol: {:?}", err) 90 | } 91 | } 92 | 93 | result 94 | } 95 | } 96 | 97 | pub struct VirtualRxToken { 98 | buffer: Vec, 99 | } 100 | 101 | impl RxToken for VirtualRxToken { 102 | fn consume(self, f: F) -> R 103 | where 104 | F: FnOnce(&[u8]) -> R, 105 | { 106 | f(&self.buffer[..]) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/packet_sources/windows.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | use std::os::windows::ffi::OsStrExt; 3 | use std::path::PathBuf; 4 | 5 | use anyhow::{anyhow, Result}; 6 | use tokio::net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions}; 7 | use tokio::sync::mpsc::Sender; 8 | use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; 9 | use windows::core::w; 10 | use windows::core::PCWSTR; 11 | use windows::Win32::UI::Shell::ShellExecuteW; 12 | use windows::Win32::UI::Shell::SE_ERR_ACCESSDENIED; 13 | use windows::Win32::UI::WindowsAndMessaging::{SW_HIDE, SW_SHOWNORMAL}; 14 | 15 | use crate::intercept_conf::InterceptConf; 16 | use crate::messages::{TransportCommand, TransportEvent}; 17 | use crate::packet_sources::{forward_packets, PacketSourceConf, PacketSourceTask, IPC_BUF_SIZE}; 18 | use crate::shutdown; 19 | 20 | pub struct WindowsConf { 21 | pub executable_path: PathBuf, 22 | } 23 | 24 | impl PacketSourceConf for WindowsConf { 25 | type Task = WindowsTask; 26 | type Data = UnboundedSender; 27 | 28 | fn name(&self) -> &'static str { 29 | "Windows proxy" 30 | } 31 | 32 | async fn build( 33 | self, 34 | transport_events_tx: Sender, 35 | transport_commands_rx: UnboundedReceiver, 36 | shutdown: shutdown::Receiver, 37 | ) -> Result<(Self::Task, Self::Data)> { 38 | let pipe_name = format!( 39 | r"\\.\pipe\mitmproxy-transparent-proxy-{}", 40 | std::process::id() 41 | ); 42 | 43 | let ipc_server = ServerOptions::new() 44 | .pipe_mode(PipeMode::Message) 45 | .first_pipe_instance(true) 46 | .max_instances(1) 47 | .in_buffer_size(IPC_BUF_SIZE as u32) 48 | .out_buffer_size(IPC_BUF_SIZE as u32) 49 | .reject_remote_clients(true) 50 | .create(&pipe_name)?; 51 | 52 | log::debug!("starting {} {}", self.executable_path.display(), pipe_name); 53 | 54 | let pipe_name = pipe_name 55 | .encode_utf16() 56 | .chain(iter::once(0)) 57 | .collect::>(); 58 | 59 | let executable_path = self 60 | .executable_path 61 | .as_os_str() 62 | .encode_wide() 63 | .chain(iter::once(0)) 64 | .collect::>(); 65 | 66 | let result = unsafe { 67 | ShellExecuteW( 68 | None, 69 | w!("runas"), 70 | PCWSTR::from_raw(executable_path.as_ptr()), 71 | PCWSTR::from_raw(pipe_name.as_ptr()), 72 | None, 73 | if cfg!(debug_assertions) { 74 | SW_SHOWNORMAL 75 | } else { 76 | SW_HIDE 77 | }, 78 | ) 79 | }; 80 | 81 | if cfg!(debug_assertions) { 82 | if result.0 as u32 <= 32 { 83 | let err = windows::core::Error::from_win32(); 84 | log::warn!("Failed to start child process: {}", err); 85 | } 86 | } else if result.0 as u32 == SE_ERR_ACCESSDENIED { 87 | return Err(anyhow!( 88 | "Failed to start the interception process as administrator." 89 | )); 90 | } else if result.0 as u32 <= 32 { 91 | let err = windows::core::Error::from_win32(); 92 | return Err(anyhow!("Failed to start the executable: {}", err)); 93 | } 94 | 95 | let (conf_tx, conf_rx) = unbounded_channel(); 96 | 97 | Ok(( 98 | WindowsTask { 99 | ipc_server, 100 | transport_events_tx, 101 | transport_commands_rx, 102 | conf_rx, 103 | shutdown, 104 | }, 105 | conf_tx, 106 | )) 107 | } 108 | } 109 | 110 | pub struct WindowsTask { 111 | ipc_server: NamedPipeServer, 112 | transport_events_tx: Sender, 113 | transport_commands_rx: UnboundedReceiver, 114 | conf_rx: UnboundedReceiver, 115 | shutdown: shutdown::Receiver, 116 | } 117 | 118 | impl PacketSourceTask for WindowsTask { 119 | async fn run(self) -> Result<()> { 120 | log::debug!("Waiting for IPC connection..."); 121 | self.ipc_server.connect().await?; 122 | log::debug!("IPC connected!"); 123 | 124 | forward_packets( 125 | self.ipc_server, 126 | self.transport_events_tx, 127 | self.transport_commands_rx, 128 | self.conf_rx, 129 | self.shutdown, 130 | ) 131 | .await 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/processes/macos_icons.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use cocoa::base::id; 3 | use objc::{class, msg_send, sel, sel_impl}; 4 | use std::collections::hash_map::DefaultHasher; 5 | use std::collections::hash_map::Entry; 6 | use std::collections::HashMap; 7 | use std::hash::{Hash, Hasher}; 8 | use std::io::Cursor; 9 | use std::path::Path; 10 | use std::path::PathBuf; 11 | use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind}; 12 | 13 | #[derive(Default)] 14 | pub struct IconCache { 15 | /// executable name -> icon hash 16 | executables: HashMap, 17 | /// icon hash -> png bytes 18 | icons: HashMap>, 19 | } 20 | 21 | impl IconCache { 22 | pub fn get_png(&mut self, executable: PathBuf) -> Result<&Vec> { 23 | match self.executables.entry(executable) { 24 | Entry::Occupied(e) => { 25 | // Guaranteed to exist because we never clear the cache. 26 | Ok(self.icons.get(e.get()).unwrap()) 27 | } 28 | Entry::Vacant(e) => { 29 | let tiff = tiff_data_for_executable(e.key())?; 30 | let mut hasher = DefaultHasher::new(); 31 | tiff.hash(&mut hasher); 32 | let tiff_hash = hasher.finish(); 33 | e.insert(tiff_hash); 34 | let icon = self 35 | .icons 36 | .entry(tiff_hash) 37 | .or_insert_with(|| tiff_to_png(&tiff)); 38 | Ok(icon) 39 | } 40 | } 41 | } 42 | } 43 | 44 | pub fn tiff_to_png(tiff: &[u8]) -> Vec { 45 | let mut c = Cursor::new(Vec::new()); 46 | let tiff_image = image::load_from_memory_with_format(tiff, image::ImageFormat::Tiff) 47 | .unwrap() 48 | .resize(32, 32, image::imageops::FilterType::Triangle); 49 | tiff_image 50 | .write_to(&mut c, image::ImageFormat::Png) 51 | .unwrap(); 52 | c.into_inner() 53 | } 54 | 55 | pub fn tiff_data_for_executable(executable: &Path) -> Result> { 56 | let mut sys = System::new(); 57 | sys.refresh_processes_specifics( 58 | ProcessesToUpdate::All, 59 | true, 60 | ProcessRefreshKind::nothing().with_exe(UpdateKind::OnlyIfNotSet), 61 | ); 62 | for (pid, process) in sys.processes() { 63 | // process.exe() will return empty path if there was an error while trying to read /proc//exe. 64 | if let Some(path) = process.exe() { 65 | if executable == path.to_path_buf() { 66 | let pid = pid.as_u32(); 67 | unsafe { 68 | #[allow(unexpected_cfgs)] 69 | let app: id = msg_send![ 70 | class!(NSRunningApplication), 71 | runningApplicationWithProcessIdentifier: pid 72 | ]; 73 | if !app.is_null() { 74 | #[allow(unexpected_cfgs)] 75 | let img: id = msg_send![app, icon]; 76 | #[allow(unexpected_cfgs)] 77 | let tiff: id = msg_send![img, TIFFRepresentation]; 78 | #[allow(unexpected_cfgs)] 79 | let length: usize = msg_send![tiff, length]; 80 | #[allow(unexpected_cfgs)] 81 | let bytes: *const u8 = msg_send![tiff, bytes]; 82 | let data = std::slice::from_raw_parts(bytes, length).to_vec(); 83 | return Ok(data); 84 | } 85 | } 86 | } 87 | } 88 | } 89 | bail!("unable to extract icon"); 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use super::*; 95 | use data_encoding::BASE64; 96 | 97 | #[test] 98 | fn png() { 99 | let path = PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder"); 100 | let mut icon_cache = IconCache::default(); 101 | let vec = icon_cache.get_png(path).unwrap(); 102 | assert!(!vec.is_empty()); 103 | dbg!(vec.len()); 104 | let base64_png = BASE64.encode(vec); 105 | dbg!(base64_png); 106 | } 107 | 108 | #[ignore] 109 | #[test] 110 | fn memory_leak() { 111 | let path = PathBuf::from("/System/Library/CoreServices/Finder.app/Contents/MacOS/Finder"); 112 | for _ in 0..500 { 113 | _ = &tiff_data_for_executable(&path); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/processes/mod.rs: -------------------------------------------------------------------------------- 1 | pub use image; 2 | use std::path::PathBuf; 3 | 4 | #[cfg(any(target_os = "linux", target_os = "macos"))] 5 | mod nix_list; 6 | #[cfg(any(target_os = "linux", target_os = "macos"))] 7 | pub use self::nix_list::active_executables; 8 | 9 | #[cfg(windows)] 10 | mod windows_list; 11 | #[cfg(windows)] 12 | pub use self::windows_list::active_executables; 13 | #[cfg(windows)] 14 | pub use self::windows_list::get_process_name; 15 | 16 | #[cfg(target_os = "macos")] 17 | mod macos_icons; 18 | #[cfg(target_os = "macos")] 19 | use self::macos_icons::IconCache; 20 | 21 | #[cfg(windows)] 22 | mod windows_icons; 23 | #[cfg(windows)] 24 | use self::windows_icons::IconCache; 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct ProcessInfo { 28 | pub executable: PathBuf, 29 | pub display_name: String, 30 | pub is_visible: bool, 31 | pub is_system: bool, 32 | } 33 | 34 | pub type ProcessList = Vec; 35 | 36 | #[cfg(any(windows, target_os = "macos"))] 37 | pub static ICON_CACHE: std::sync::LazyLock> = 38 | std::sync::LazyLock::new(|| std::sync::Mutex::new(IconCache::default())); 39 | 40 | pub mod bench { 41 | #[cfg(target_os = "macos")] 42 | pub use super::nix_list::visible_windows; 43 | #[cfg(windows)] 44 | pub use super::windows_list::visible_windows; 45 | 46 | #[cfg(target_os = "macos")] 47 | pub use super::macos_icons::IconCache; 48 | #[cfg(windows)] 49 | pub use super::windows_icons::IconCache; 50 | 51 | #[cfg(target_os = "macos")] 52 | pub use super::macos_icons::{tiff_data_for_executable, tiff_to_png}; 53 | 54 | #[cfg(windows)] 55 | pub use super::windows_list::{ 56 | enumerate_pids, get_display_name, get_is_critical, get_process_name, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/shutdown.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::fmt::{Debug, Formatter}; 3 | 4 | use tokio::sync::watch; 5 | use tokio::task::JoinSet; 6 | 7 | #[derive(Clone)] 8 | pub struct Receiver(watch::Receiver<()>); 9 | 10 | impl Receiver { 11 | pub async fn recv(&mut self) { 12 | self.0.changed().await.ok(); 13 | self.0.mark_changed(); 14 | } 15 | 16 | pub fn is_shutting_down(&self) -> bool { 17 | self.0.has_changed().unwrap_or(true) 18 | } 19 | } 20 | 21 | impl Debug for Receiver { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 23 | f.debug_tuple("Shutdown") 24 | .field(&self.is_shutting_down()) 25 | .finish() 26 | } 27 | } 28 | 29 | pub fn channel() -> (watch::Sender<()>, Receiver) { 30 | let (tx, rx) = watch::channel(()); 31 | (tx, Receiver(rx)) 32 | } 33 | 34 | pub async fn shutdown_task(mut tasks: JoinSet>, shutdown_done: watch::Sender<()>) { 35 | while let Some(task) = tasks.join_next().await { 36 | match task { 37 | Ok(Ok(())) => (), 38 | Ok(Err(error)) => { 39 | log::error!("Task failed: {:?}\n{}", error, error.backtrace()); 40 | tasks.shutdown().await; 41 | } 42 | Err(error) => { 43 | if error.is_cancelled() { 44 | log::error!("Task cancelled: {}", error); 45 | } else { 46 | log::error!("Task panicked: {}", error); 47 | } 48 | tasks.shutdown().await; 49 | } 50 | } 51 | } 52 | shutdown_done.send(()).ok(); 53 | } 54 | 55 | #[cfg(test)] 56 | mod tests { 57 | use super::*; 58 | 59 | #[tokio::test] 60 | async fn shutdown_channel() { 61 | let (tx, mut rx1) = channel(); 62 | let rx2 = rx1.clone(); 63 | assert!(!rx1.is_shutting_down()); 64 | assert!(!rx2.is_shutting_down()); 65 | tx.send(()).unwrap(); 66 | rx1.recv().await; 67 | assert!(rx1.is_shutting_down()); 68 | assert!(rx2.is_shutting_down()); 69 | assert!(rx1.is_shutting_down()); 70 | assert!(rx2.is_shutting_down()); 71 | rx1.recv().await; 72 | assert!(rx1.is_shutting_down()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod network; 2 | -------------------------------------------------------------------------------- /wireguard-test-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mitm-wg-test-client" 3 | license = "MIT" 4 | description = "Test client for mitmproxy_rs's WireGuard functionality" 5 | authors.workspace = true 6 | version.workspace = true 7 | repository.workspace = true 8 | edition.workspace = true 9 | rust-version.workspace = true 10 | publish.workspace = true 11 | 12 | [lints] 13 | workspace = true 14 | 15 | [dependencies] 16 | anyhow = "1.0.97" 17 | data-encoding = "2.8.0" 18 | boringtun = "0.6" 19 | hex = "0.4" 20 | smoltcp = "0.12" 21 | -------------------------------------------------------------------------------- /wireguard-test-client/wireguard_echo_test_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | import logging 4 | import signal 5 | import sys 6 | import textwrap 7 | import time 8 | 9 | import mitmproxy_rs 10 | 11 | # (private key, public key) 12 | server_keypair = ( 13 | "EG47ZWjYjr+Y97TQ1A7sVl7Xn3mMWDnvjU/VxU769ls=", 14 | "mitmV5Wo7pRJrHNAKhZEI0nzqqeO8u4fXG+zUbZEXA0=", 15 | ) 16 | client_keypair = ( 17 | "qG8b7LI/s+ezngWpXqj5A7Nj988hbGL+eQ8ePki0iHk=", 18 | "Test1sbpTFmJULgSlJ5hJ1RdzsXWrl3Mg7k9UTN//jE=", 19 | ) 20 | 21 | LOG_FORMAT = "[%(asctime)s %(levelname)-5s %(name)s] %(message)s" 22 | TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 23 | 24 | try: 25 | from rich import print 26 | from rich.logging import RichHandler 27 | 28 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT, handlers=[RichHandler()]) 29 | except ImportError: 30 | logging.basicConfig(format=LOG_FORMAT, datefmt=TIME_FORMAT) 31 | 32 | logging.Formatter.convert = time.gmtime 33 | logger = logging.getLogger() 34 | logger.setLevel(logging.DEBUG) 35 | 36 | 37 | async def main(): 38 | server = await mitmproxy_rs.start_wireguard_server( 39 | "0.0.0.0", 40 | 51820, 41 | server_keypair[0], 42 | [client_keypair[1]], 43 | handle_stream, 44 | handle_stream, 45 | ) 46 | 47 | print( 48 | textwrap.dedent( 49 | f""" 50 | :white_check_mark: Server started. Use the following WireGuard config for testing: 51 | ------------------------------------------------------------ 52 | [Interface] 53 | PrivateKey = {client_keypair[0]} 54 | Address = 10.0.0.1/32 55 | MTU = 1420 56 | 57 | [Peer] 58 | PublicKey = {server_keypair[1]} 59 | AllowedIPs = 10.0.0.0/24 60 | Endpoint = 127.0.0.1:51820 61 | ------------------------------------------------------------ 62 | 63 | And then run `nc 10.0.0.42 1234` or `nc -u 10.0.0.42 1234` to talk to the echo server. 64 | """ 65 | ) 66 | ) 67 | 68 | def stop(*_): 69 | server.close() 70 | signal.signal(signal.SIGINT, lambda *_: sys.exit()) 71 | 72 | signal.signal(signal.SIGINT, stop) 73 | 74 | await server.wait_closed() 75 | 76 | 77 | async def handle_stream(rw: mitmproxy_rs.Stream): 78 | logger.debug(f"connection task {rw=}") 79 | logger.debug(f"{rw.get_extra_info('peername')=}") 80 | 81 | for _ in range(2): 82 | logger.debug("reading...") 83 | try: 84 | data = await rw.read(4096) 85 | except Exception as exc: 86 | logger.debug(f"read {exc=}") 87 | data = b"" 88 | logger.debug(f"read complete. writing... {len(data)=} {data[:10]=} ") 89 | 90 | try: 91 | rw.write(data.upper()) 92 | except Exception as exc: 93 | logger.debug(f"write {exc=}") 94 | logger.debug("write complete. draining...") 95 | 96 | try: 97 | await rw.drain() 98 | except Exception as exc: 99 | logger.debug(f"drain {exc=}") 100 | logger.debug("drained.") 101 | 102 | logger.debug("closing...") 103 | try: 104 | rw.close() 105 | except Exception as exc: 106 | logger.debug(f"close {exc=}") 107 | logger.debug("closed.") 108 | 109 | 110 | if __name__ == "__main__": 111 | asyncio.run(main(), debug=True) 112 | --------------------------------------------------------------------------------