Introduction

proxysaur is a network application debugging proxy. Currently it supports TCP, HTTP and HTTP(s). There are plans to extend it to other protocols, such as Postgres and Redis, but these aren't implemented yet.

You can use proxysaur to observe, record, replay, and rewrite network requests in real time. This is useful when you're doing web application development and need to see what requests are being performed.

Design Goals

Proxysaur is designed to be extensible with WebAssembly. All of the core logic is written in Rust, but request and response handling are completely delegated to compiled WASI modules.

Proxysaur is designed to be shareable. Once you have a proxy configuration which works, you can share the configuration and any compiled WASM modules with your teammates. You could use this, for example, to provide separate configurations for frontend and backend developers on your team. You could also use proxysaur as a mock network layer in a test suite.

Design Anti-Goals

Proxysaur is not meant to be used in production, at least not in its current form.

Proxysaur is configurable through a CLI interface and a configuration file, and is not meant to be a full-fledged UI application like Fiddler or Charles Proxy. That doesn't mean that one can't be built on top of Proxysaur.

Releases

Proxysaur is currently in the pre-alpha phase of development. The API, CLI, and configuration interfaces could change at anytime.

Getting Help

If you have questions or feedback, the best way to get help is to open an issue.

Before Getting Started

Before you get started, you'll need to install openssl. Currently, proxysaur is known to work with the latest 1.1.1 release.

The following sections provide more details on getting up and running:

Installation

Releases

There are prebuilt releases for macOS and Linux available on the releases page.

Keep in mind you will need a working installation of OpenSSL 1.1.1 which can be installed via homebrew or on Debian-based Linux distributions with apt-get install.

Building from source

Alternatively, you can build proxysaur from source.

You'll need to have Rust installed, and then you can run:

$ git clone https://github.com/pmalmgren/proxysaur.git
$ cd proxysaur
$ cargo build --release
$ cp target/release/proxysaur /usr/local/bin # or somewhere in your PATH

Configuration

Proxysaur uses a toml configuration file to store configuration settings.

ca_path stores a reference to the root certificate authority, which is needed if you want proxysaur to proxy and terminate TLS connections.

Proxysaur can have one or more proxy configurations. These tell the server which ports to listen on, what application protocol to expect on those connections, and how to handle those connections.

Initialization

To generate a configuration file, use the command:

$ proxysaur init [optional-path]

It will create a proxysaur.toml file in the current directory. Alternatively, you can pass in a path.

You can then launch the proxy server with the command:

$ proxysaur

Or, if you specified a configuration file path:

$ proxysaur --config-path /path/to/config

Certificate Authority Configuration

Proxysaur comes with a root certificate authority generator. This is necessary if you want to configure it as a man-in-the-middle forwarding proxy for HTTPS.

$ proxysaur generate-ca

By default, this will place the certificate authority in the XDG data directory on Linux, and the application support directory on macOS.

Proxy Configuration

To add a proxy configuration, use the subcommand add-proxy. This example will create a proxy which will listen on port 9000 and forward the requests to www.google.com:

$ proxysaur add-proxy
Enter host: localhost
Enter port: 9000
Enter protocol [http|httpforward|tcp]: http
Use tls [true/false]: false
Upstream address: www.google.com
Upstream port: 443
Use custom wasi [true/false]? false

Enter host/Enter port

This prompt is the address and port that proxysaur will bind to when listening for incoming connections.

Enter protocols

Currently proxysaur supports three different protocols:

  • http, a reverse proxy protocol which simply forwards along requests to an upstream server
  • httpforward, which allows proxysaur to be configured as a system-wide http proxy
  • tcp, a reverse tcp proxy which forwards along tcp requests to an upstream server

Use tls

If true, the server will terminate connections using TLS and the configured certificate authority.

Upstream address/port

Where to forward the requests.

These configuration settings aren't applicable to the httpforward protocol.

Use custom wasi

If set to true, you will be asked to enter the path to a custom WASI module that handles requests for the protocol you specified.

HTTP Proxying

Proxysaur supports two modes for proxying HTTP requests: HTTP forwarding mode, and reverse proxy mode.

HTTP forwarding mode allows proxysaur to be used as a system-wide proxy.

Reverse proxy mode allows proxysaur to terminate HTTP(S) traffic for a single host.

HTTP(S) Forward Proxy Configuration

Proxysaur ships with a forward proxy WASI module which is highly configurable, and allows the following things:

  • Request rewriting and redirecting
  • Serving responses directly from the filesystem
  • Response rewriting and recording

Unless specified otherwise, this module is activated by default when the httpforward protocol is specified.

The remaining documentation will cover how to use httpforward mode to observe and manipulate HTTP traffic.

Quick start

To support getting started quickly with HTTP debugging, proxysaur comes with a command to launch a debugging proxy:

$ proxysaur http
Using configuration file: "/home/me/.config/proxysaur/proxysaur.toml"
Using existing CA dir: "/home/me/.local/share/proxysaur"
Root Certificate: "/home/me/.local/share/proxysaur/myca.crt"
To trust this certificate, run:
sudo cp "/home/me/.local/share/proxysaur/myca.crt" /usr/local/share/ca-certificates/extra
sudo update-ca-certificates
To use in a browser, read more here: https://proxysaur.us/ca#trusting-the-root-certificate-in-your-browser
Proxy configuration path: "/home/me/.config/proxysaur/config.yml"
Visit this URL to make sure everything is working: https://proxysaur.us/test
Proxy HttpForward listening on address: http://localhost:9999

This will initialize a root certificate authority, initialize a configuration file, and start the HTTP proxy with a default configuration.

Copy and paste the steps to trust the root certificate, and then run the command:

$ curl -x "http://localhost:9999" "https://proxysaur.us/test"
<!DOCTYPE html>
<html>
  <body>
    <h1>Proxysaur has eaten your website.</h1>
    <p>If this works, it means your proxy is correctly configured. Congratulations!</p>
    <p>To get started, head over to the <a href="/http_configuration.html#configure-the-forward-proxy">configuration docs page.</a></p>
  </body>
</html>

Go ahead and edit the file defined in the "proxy configuration path," in this case "/home/me/.config/proxysaur/config.yml" to look like this:

hosts:
  google.com:
    scheme: https
    response_rewrites:
      - when:
          - path:
              exact: /test
        rewrite:
          replace_with: |
            <!DOCTYPE html>
            <html>
              <body>
                <h1>Proxysaur has eaten your website.</h1>
              </body>
            </html>
  proxysaur.us:
    scheme: https
    response_rewrites:
      - when:
          - path:
              exact: /test
        rewrite:
          replace_with: |
            <!DOCTYPE html>
            <html>
              <body>
                <h1>Proxysaur has eaten your website.</h1>
                <p>If this works, it means your proxy is correctly configured. Congratulations!</p>
                <p>To get started, head over to the <a href="/http_configuration.html#configure-the-forward-proxy">configuration docs page.</a></p>
              </body>
            </html>

You should see the proxy live reload. Afterwards, run the command curl -x "http://localhost:9999" "https://proxysaur.us/test" to verify that your changes were picked up.

Next Steps

Certificate Authority Setup

Before we're able to intercept HTTPS requests, we need a certificate authority which is trusted by the system making the requests. When HTTP requests come in, proxysaur uses the certificate authority to generate a certificate signing request (or CSR) and generate certificates for the domain. And because the certificates are signed by a trusted certificate authority, browsers and other HTTPS clients will see them as valid without displaying warnings.

Security Concerns

Because the risk of an attacker getting ahold of the root certificate poses a grave security threat, proxysaur-generated root certificate authorities expire after 1 day.

In the future, using something like Linux keyrings or Apple's Secure Enclave may be implemented so that keys aren't stored in plaintext on disk.

Generating a Certificate Authority

Proxysaur has a built in subcommand called generate-ca. It uses openssl to generate a key and certificate file. If you don't give a path to the subcommand, it will place them in the platform-specific data directory, for example in $XDG_DATA_DIRS on Linux.

It will output the location of the CA to stdout, and if you run it multiple times it won't overwrite anything in the existing directory. If you pass in -f it will clear out all of the certificates.

$ proxysaur generate-ca
/home/me/.local/share/proxysaur
$ ls -lah /home/me/.local/share/proxysaur
total 28K
drwxrwxr-x  2 me me 4.0K Apr 28 11:29 .
drwxrwxr-x 45 me me 4.0K Apr 27 08:53 ..
-rw-rw-r--  1 me me  204 Apr 28 11:29 config
-rwxrwxrwx  1 me me  326 Apr 28 11:29 generateca.sh
-rw-rw-r--  1 me me 1.3K Apr 28 11:29 myca.crt
-rw-------  1 me me 1.7K Apr 28 11:29 myca.key
-rw-rw-r--  1 me me 1.3K Apr 28 11:29 myca.pem

Trusting the Root Certificate in your Browser

Firefox

  1. Go to about:preferences in the URL bar
  2. Search for "certificates" and click on "View Certificates"
  3. Click on "Authorities" and then "Import"
  4. Select the root CA

Chrome

  1. Go to chrome://settings/certificates in the URL bar
  2. Click on "Authorities" and then "Import"
  3. Select the root CA

Trusting the Root Certificate on your OS

Linux

On Linux, you can manually trust the root certificate by copying it to your /usr/local/share/ca-certificates/extra directory:

$ CA_LOC=$(proxysaur generate-ca)
$ sudo cp $CA_LOC/myca.crt /usr/local/share/ca-certificates/extra
$ sudo update-ca-certificates

macOS

On macOS, you can either trust the root CA by double-clicking it and adding it to your login keychain. You'll have to select "Always Trust" after adding it to the keychain.

You can also try with the following command:

$ CA_LOC=$(proxysaur generate-ca)
$ security add-trusted-cert -d -r trustRoot -k $HOME/Library/Keychains/login.keychain $CA_LOC/myca.crt

iOS

You'll have to send the certificate to yourself via AirDrop, and then follow along with the Apple Support docs.

Configuration

If you haven't already, run proxysaur init to create a new proxysaur.toml file in the current directory.

Make sure that your certificate authority is initialized and created.

Add a proxy with HTTP forward protocol

To setup a forwarding HTTP proxy, use the proxysaur command add-proxy:

$ proxysaur add-proxy
Enter host: localhost
Enter port: 9000
Enter protocol [http|httpforward|tcp]: httpforward
Use tls [true/false]: false
Use custom wasi [true/false]? false
Enter proxy configuration path: proxy.yaml

This will generate the following configuration in proxysaur.toml:

[[proxy]]
port = 9000
protocol = 'httpforward'
tls = false
address = 'localhost'
upstream_address = ''
upstream_port = 9999

To check that this works, start proxysaur and then run the following command:

$ curl -x "http://localhost:9000" "http://neverssl.com"
...

You should see requests and responses in the proxysaur logs:

$ proxysaur
INFO protocols::http::proxy: Received request req=Request { ... }
INFO protocols::http::proxy: New request. new_request=Request {...}
INFO protocols::http::proxy: New response. new_response=Response { ...}
INFO protocols::http::proxy: Finished tunneling. 
INFO protocols::http::proxy: HTTP proxy result. 

Configure the forward proxy

To intercept and rewrite requests, we need to add a proxy configuration.

Create a file called proxy.yaml and add the following:

hosts:
  google.com:
    scheme: https
    response_rewrites:
      - when:
          - path:
              exact: /
        rewrite:
          replace_with: |
            <!DOCTYPE html>
            <html>
              <body>
                <h1>Proxysaur has eaten your website.</h1>
              </body>
            </html>

Now start up proxysaur, and in a separate terminal run:

$ curl -x "http://localhost:9000" "https://google.com/"
<!DOCTYPE html>
<html>
  <body>
    <h1>Proxysaur has eaten your website.</h1>
  </body>
</html>% 

Configuration format

The configuration file has a top-level array of hosts, each of which has its own configuration block with the following properties:

  • scheme - can either be http or https
  • response_rewrites - an array of rewrites which can change the HTTP response, covered in HTTP response rewriting
  • request_rewrites - an array of rewrites which can change the HTTP request, covered in HTTP request rewriting
  • redirect - a rule which can redirect requests to different hosts, covered in HTTP request redirection

Each rewrite or redirect has a when clause, which can match on either a header or the path. The match value for the path can be an exact match, a contains match which will match when the provided substring is found in the path, or a regular expression.

Exact match example

hosts:
  html.duckduckgo.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              exact: /path
        rewrite:
          match:
            header_name:
              exact: origin
            header_value:
              contains: ""
          new_header_name: $0
          new_header_value: "duckduckgo.com" 

Contains match example

hosts:
  html.duckduckgo.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              contains: /api/v1
        rewrite:
          match:
            header_name:
              exact: origin
            header_value:
              contains: ""
          new_header_name: $0
          new_header_value: "duckduckgo.com" 

Regular expression match example

hosts:
  html.duckduckgo.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              regex: /api/v1/(.*)/books
        rewrite:
          match:
            header_name:
              exact: origin
            header_value:
              contains: ""
          new_header_name: $0
          new_header_value: "duckduckgo.com" 

Observing HTTP Requests

For now, HTTP requests can be observed in the CLI output.

In the future, an open telemetry collector might be added, with the option to view the output either in a tool like Jaeger or in a separate web UI.

Redirecting HTTP Requests

HTTP requests can be redirected to another URL with the following configuration block:

hosts:
  google.com:
    scheme: https
    redirect:
        to:
          url:
            url: https://duckduckgo.com/about
            replace_path_and_query: false
        when:
          - path:
              exact: /about

The parameter replace_path_and_query will overwrite the new redirected URL. For example:

hosts:
  google.com:
    scheme: https
    redirect:
        to:
          url:
            url: https://proxysaur.us/
            replace_path_and_query: true
        when:
          - path:
              regex: /.*

In the above configuration block, every request directed to google.com will be redirected to proxysaur.us, with the path component being completely overwritten.

Redirecting file requests

Note: This currently doesn't work, but is valid configuration

You can also use proxysaur to serve static files with the file redirect block:

hosts:
  test2.com:
    scheme: https
    redirect:
      to:
        file:
          path: /usr/local/www
          root_index: true
          replace_path: true
          file_suffix: .html
          content_type: text/html; charset=UTF-8

This will redirect requests from https://test2.com/ to the local file system. Requests to / will be served from /usr/local/www/index.html etc. This can be useful when overwriting requests to specific paths with pre-recorded responses.

Rewriting HTTP Requests

You can configure proxysaur to overwrite the HTTP headers and body of a request.

Rewriting HTTP headers

Header rewrites have two parts: a match on the name or the value, and the new header name and/or value. Parts of the matched headers can be used in the old. For exact and contains matches, $0 will expand to the entire matched value. So for example, this configuration will rewrite the access-control-allow-origin header to reuse the same name, but with the new value *.

hosts:
  test.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              exact: /
        rewrite:
          match:
            header_name:
              exact: access-control-allow-origin
            header_value:
              # a blank value means anything will be matched
              contains: ""
          new_header_name: $0
          new_header_value: "*"

Regular expressions can be used to do more complex rewrites with capture groups, which can be named or anonymous. In the below example we'll capture the bearer token and convert it to a basic token:

hosts:
  test.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              exact: /
        rewrite:
          match:
            header_name:
              exact: authorization
            header_value:
              regex: Bearer (?P<token>[0-9A-Za-z]+)
          new_header_name: $0
          new_header_value: "Basic $token"

Rewriting the HTTP request body

There isn't any matching involved in an HTTP request body rewrite. The only thing you can do is overwrite the entire request body.

hosts:
  html.duckduckgo.com:
    scheme: https
    request_rewrites:
      - when:
          - path:
              contains: /api
        rewrite:
          replace_with: |
            { "payload": "a new body" }

The proxy will calculate the size of the new body and set the Content-Length header to the appropriate size for you.

Rewriting HTTP Responses

You can configure proxysaur to overwrite the HTTP headers, body, and status code of a response. The header and body rewrites are the same as for HTTP requests.

Status code rewrites

This example rewrites the status code from 200 to 500 for https://test.com/:

hosts:
  test.com:
    scheme: https
    response_rewrites:
      - when:
          - path:
              exact: /
        rewrite:
          status:
            exact: "200"
          new_status: "500"

Customization with WASI

Compiled WASM modules, specifically compiled with WASI, can be used to extend proxysaur. Currently, bindings exist only for Rust. The API exposed by these bindings is not stable and subject to change.

A good reference point is the http-forward-proxy, which is the proxy that is documented for intercepting and rewriting requests via the YAML configuration file.

Writing a Rust proxysaur module

Cargo.toml

Add the following dependency:

proxysaur-bindings = { git = "https://github.com/pmalmgren/proxysaur" }

Configuration

The configuration parameter in the toml file called proxy_configuration_path will be read in and the raw bytes are accessible with the config bindings:

use proxysaur_bindings::config;

fn main() {
    let config_data: Vec<u8> = proxysaur_config::get_config_data();
    ...
}

If the data is invalid, you can use the set_invalid_data function which allows you to set a String error message:

use proxysaur_bindings::config;

fn main() {
    let config_data: Vec<u8> = proxysaur_config::get_config_data();
    let config: Config = serde::from_bytes(&config_data).map_err(|err| {
        let msg = format!("Error serializing data: {}", err);
        config::set_invalid_data(msg);
        err
    }).unwrap();
}

HTTP pre-requests

Before proxysaur proxies a request, it checks to see if it should intercept the request or just forward the data between the client and the server. It does this by running the module, and checking to see what the module set the proxy mode to. A value of Intercept means that it will proceed to call methods to manipulate the request and the response.

For example, this WASI module will proxy requests only to localhost:9234:

use proxysaur_bindings::http::pre_request::{self, HttpPreRequest, ProxyMode};

fn main() {
    let request: HttpPreRequest = pre_request::http_request_get();

    if request.authority == "localhost:9234" {
        pre_request::http_set_proxy_mode(ProxyMode::Intercept);
    }
}

HTTP Requests

After proxysaur received a request from the client, it runs a WASI module and checks the data set by the module. In the request module, you can set anything you want on the request. For example, this one changes the request body to some random data for test.com:

use proxysaur_bindings::http::request::{self, HttpRequest};

fn main() {
    let request: HttpRequest = request::http_request_get().expect("should get the request");

    if request.host == "test.com" {
        request::http_request_set_body("haha!".as_bytes()).expect("should set the body");
    }
}

HTTP Responses

After proxysaur receives a response from the server, it runs a WASI module to check the response data set by the module.

use proxysaur_bindings::http::response::{self, HttpResponse};

fn main() {
    let response: HttpResponse = response::http_response_get().expect("should get the response");

    if response.status == 200 {
        response::http_response_set_status(500).expect("should set the status");
        response::http_response_set_body("broken!".as_bytes()).expect("should set the body");
    }
}