Skip to content

cloudflare/proxy-everything

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

proxyeverything

It's a TPROXY based docker container sidecar to proxy all traffic from a docker container to wherever you want.

It leverages HTTP CONNECT to proxy traffic from the container to the host.

Why do I need this?

This is very useful to do things like "listen to all IP ports that the container connects to". You can implement your own gateway for docker containers like this.

 (your gateway) <--docker bridge device--> [proxy-everything] <-TPROXY-> [user container]

How to build

GOOS=linux CGO_ENABLED=0 go build .

Attention: Do NOT run proxy-everything on your development machine without isolating it in a docker container or its own network namespace, it will modify your iptables.

You should run it on its own docker container. See "How to use it with Docker".

How to implement your own gateway for proxy-everything to use

A gateway server should:

  1. Listen on a TCP port (default: 49121) accessible from the container's Docker bridge network (typically 172.17.0.1:49121).

  2. Accept HTTP CONNECT requests. proxy-everything will send requests in this format:

    CONNECT <destination_host>:<port> HTTP/1.1
    Host: <destination_host>:<port>
    User-Agent: proxy-everything/0.0.1/<source_address>
    Connection: close
    X-Forwarded-For: <source_address>
    X-Proto: tcp
    

    Optional headers may also be present:

    • X-Tls-Sni: <hostname> when outbound TLS traffic exposes SNI.
    • X-Hostname: <hostname> when outbound HTTP traffic exposes a Host header and no SNI was available.
  3. Parse the destination from the Host header (or request URI) to know where to dial.

  4. Establish a connection to the destination (the original target the container wanted to reach).

  5. Respond with HTTP status codes:

    • 2xx if the connection to the destination succeeded - the tunnel is now established.
    • 400 if the connection to the destination failed (e.g., connection refused).
    • The rest of status codes are treated as errors from the gateway.
  6. Relay data bidirectionally between the proxy-everything client and the destination after sending the 200 OK response. The server should:

    • Copy data from the proxy connection to the destination connection.
    • Copy data from the destination connection back to the proxy connection.
    • Handle half-close properly (close write side when read side receives EOF).

See dummyserver.go for a simple reference implementation. If you don't want to implement your own, run proxy-everything's reference implementation like:

SERVER=1 ./proxy-everything

How to use it with Docker

export CONTAINER=mycontainer

$ docker build -t proxy-everything:dev .

$ docker run \
		--add-host=host.docker.internal:host-gateway \
		-d --name $(CONTAINER) ubuntu:latest sleep infinity

$ docker run \
		-it --rm --cap-add=NET_ADMIN \
		--network container:$(CONTAINER) \
		--name $(CONTAINER)-proxy proxy-everything:dev


# In another terminal
$ docker exec $(CONTAINER) bash
# You can run commands here to check how proxy-everything works

This will make proxy-everything to $(DOCKER_GATEWAY_IP):49121, DOCKER_GATEWAY_IP which usually belongs to 172.17.0.0/16. At startup proxy-everything will use the default DNS resolver of the container to resolve host.docker.internal so it knows which IP to use to proxy traffic.

When you use proxy-everything as a tool that proxies connections to your own host process, you need to make sure you listen in the right IP, usually this can be accomplished by talking to docker and asking the network gateway of the container.

Example:

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
...

By the default by this example, the TCP address you should be listening in to receive proxy-everything traffic is 172.17.0.1:49121.

TLS interception

Pass -tls-intercept to terminate TLS inside the proxy so the gateway sees plaintext.

docker run \
    -it --rm --cap-add=NET_ADMIN \
    --network container:$(CONTAINER) \
    proxy-everything:dev -tls-intercept

At startup an ephemeral CA is written to /ca/ca.crt and /ca/ca.key. For outbound TLS connections the proxy peeks the SNI from the ClientHello and adds an X-Tls-Sni header to the CONNECT request. If no SNI is present and the payload looks like HTTP, it also peeks the HTTP request headers and adds X-Hostname. The gateway then decides:

  • 200 -- terminate TLS. The proxy presents a leaf cert (signed by the ephemeral CA) to the container and forwards plaintext to the gateway.
  • 202 -- pass-through. Raw TLS bytes are forwarded as-is; the session stays end-to-end between the container and the origin.

To make the container trust the CA:

docker cp $(CONTAINER)-proxy:/ca/ca.crt /tmp/ca.crt
docker cp /tmp/ca.crt $(CONTAINER):/usr/local/share/ca-certificates/proxy-everything.crt
docker exec $(CONTAINER) update-ca-certificates

Without -tls-intercept all traffic is forwarded as raw bytes and no CA is generated.

Ingress CONNECT listener

Pass -http-ingress-address=<ip:port> to enable an always-on ingress listener inside proxy-everything. If the flag is empty, no ingress listener is started.

  • CONNECT requests are accepted on that listener.
  • X-Dst-Addr must contain the full destination as IP:port.
  • If the destination port is closed, the listener returns 400.
  • GET /ca returns the PEM-encoded CA certificate (/ca/ca.crt). Only useful when -tls-intercept is enabled. Returns 404 if the certificate doesn't exist.
  • PUT /egress updates shared runtime configuration. Supported fields are:
    • port -- updates http-egress-port
    • internet.enabled -- enables or disables internet forwarding for DNS interception
    • dns.allowHostnames -- array of hostname globs such as *.google.com, google.com, or *

Example:

# Fetch the CA certificate (requires -tls-intercept)
curl http://127.0.0.1:49122/ca -o ca.crt

curl -X PUT http://127.0.0.1:49122/egress \
  -H 'Content-Type: application/json' \
  -d '{"port":8080,"internet":{"enabled":true},"dns":{"allowHostnames":["*.google.com","google.com"]}}'

curl -v -x http://127.0.0.1:49122 https://ignored.example \
  -H 'X-Dst-Addr: 172.17.0.1:8443'

DNS interception

UDP interception is currently limited to DNS on port 53 and is optional.

  • Pass -dns-enabled to enable DNS interception.
  • -dns-address controls the IPv4 transparent DNS listener address. Default: 127.0.0.9:5000.
  • -dns-address-v6 controls the IPv6 transparent DNS listener address. Default: [::1]:50009.

Behavior:

  • If internet.enabled=true, intercepted DNS queries are forwarded to the original resolver and the upstream response is normally relayed unchanged.
  • If the upstream response is NXDOMAIN and the queried hostname matches dns.allowHostnames, the proxy synthesizes a fallback answer instead of returning NXDOMAIN.
  • If internet.enabled=false, the query is handled locally: matching hostnames get a fallback answer and non-matching hostnames get NXDOMAIN.
  • Fallback answers are currently 11.0.0.1 for A queries and fd00::1 for AAAA queries.

Philosophy

  1. Make it work with docker defaults.
  2. Multiplatform.
  3. HTTP CONNECT everything.

Current limitations

  1. UDP support is currently limited to DNS on port 53.
  2. Proxying to unix sockets is not implemented yet due to lack of support on MacOS.

TLDR: How?

We run a sidecar container that joins the container network like in https://gost.run/en/tutorials/redirect/:

docker run --add-host=host.docker.internal:host-gateway -it --rm --name iptables-test ubuntu:latest bash

# in another terminal
docker run -it --rm --cap-add=NET_ADMIN --name iptables-test-2 --network container:iptables-test ubuntu:latest bash

apt update && apt install -y iptables iproute2

ip rule add fwmark 1 lookup 100
ip route add local default dev lo table 100

iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT

iptables -t mangle -N PROXY
iptables -t mangle -A PROXY -p tcp -d 127.0.0.0/8 -j RETURN

# ignore subnet that belongs to the docker interface
iptables -t mangle -A PROXY -p tcp -d 192.168.0.0/16 -j RETURN

iptables -t mangle -A PROXY -p tcp -m mark --mark 100 -j RETURN
iptables -t mangle -A PROXY -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 12345
iptables -t mangle -A PREROUTING -p tcp -j PROXY

# Only for local mode
iptables -t mangle -N PROXY_LOCAL
iptables -t mangle -A PROXY_LOCAL -p tcp -m conntrack --ctdir REPLY -j RETURN
iptables -t mangle -A PROXY_LOCAL -p tcp -d 127.0.0.0/8 -j RETURN
iptables -t mangle -A PROXY_LOCAL -p tcp -d 255.255.255.255/32 -j RETURN
iptables -t mangle -A PROXY_LOCAL -p tcp -d 192.168.0.0/16 -j RETURN
iptables -t mangle -A PROXY_LOCAL -p tcp -m mark --mark 100 -j RETURN
iptables -t mangle -A PROXY_LOCAL -p tcp -j MARK --set-mark 1
iptables -t mangle -A OUTPUT -p tcp -j PROXY_LOCAL

In the proxy, we make use of a syscall to get the original destination IP:

if err := syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil {
    return fmt.Errorf("setsockoptint: %w", err)
}

The above can work for both IPv4 and IPv6.


Thank you to upx contributors and authors, it makes proxy-everything live as a very tiny image that can be pulled and ran very quickly.

https://linux.die.net/man/1/upx

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages