diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 4dd0054..d852939 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -7,98 +7,19 @@ on: jobs: integration-test: name: Run Integration Tests - runs-on: [ self-hosted, linux, x64, jammy, large ] + runs-on: [ self-hosted, linux, x64, noble ] steps: - uses: actions/checkout@v2 - - name: Build aproxy Snap - id: snapcraft-build - uses: snapcore/action-build@v1 - with: - snapcraft-args: --build-for amd64 + - run: cat /etc/docker/daemon.json - - name: Upload aproxy Snap - uses: actions/upload-artifact@v4 - with: - name: snap - path: aproxy*.snap + - run: which docker - - name: Install aproxy Snap - run: | - sudo snap install --dangerous aproxy_*_amd64.snap + - run: docker version - - name: Show aproxy Configuration - run: | - sudo snap get aproxy + - run: echo "FROM ubuntu:jammy" > Dockerfile - - name: Configure aproxy - run: | - sudo nft -f - << EOF - define default-ip = $(ip route get $(ip route show 0.0.0.0/0 | grep -oP 'via \K\S+') | grep -oP 'src \K\S+') - define private-ips = { 10.0.0.0/8, 127.0.0.1/8, 172.16.0.0/12, 192.168.0.0/16 } - define aproxy-port = $(sudo snap get aproxy listen | cut -d ":" -f 2) - table ip aproxy - flush table ip aproxy - table ip aproxy { - chain prerouting { - type nat hook prerouting priority dstnat; policy accept; - ip daddr != \$private-ips tcp dport { 80, 443, 11371, 4242 } counter dnat to \$default-ip:\$aproxy-port - } + - run: docker build . - chain output { - type nat hook output priority -100; policy accept; - ip daddr != \$private-ips tcp dport { 80, 443, 11371, 4242 } counter dnat to \$default-ip:\$aproxy-port - } - } - EOF - - - name: Start tcpdump - run: | - sudo tcpdump -i any -s 65535 -w capture.pcap & - echo $! > tcpdump.pid - - - name: Test HTTP - run: | - timeout 60 curl --noproxy "*" http://example.com -svS -o /dev/null - - - name: Test HTTPS - run: | - timeout 60 curl --noproxy "*" https://example.com -svS -o /dev/null - - - name: Test HKP - run: | - timeout 60 gpg -vvv --keyserver hkp://keyserver.ubuntu.com --recv-keys E1DE584A8CCA52DC29550F18ABAC58F075A17EFA - - - name: Test TCP4 - run: | - sudo apt install -y socat - timeout 60 socat /dev/null TCP4:tcpbin.com:4242 - - - name: Test Access Logs - run: | - sudo snap logs aproxy.aproxy | grep -Fq "example.com:80" - sudo snap logs aproxy.aproxy | grep -Fq "example.com:443" - sudo snap logs aproxy.aproxy | grep -Fq "keyserver.ubuntu.com:11371" - sudo snap logs aproxy.aproxy | grep -Eq "[0-9.]+:4242" - - - name: Show Access Logs - if: failure() - run: | - sudo snap logs aproxy.aproxy -n=all - - - name: Stop tcpdump - if: failure() - run: | - PID=$(cat tcpdump.pid) - if [ -n "$PID" ]; then - sudo kill -2 "$PID" || true - fi - sleep 1 - - - name: Upload tcpdump capture - if: failure() - uses: actions/upload-artifact@v4 - with: - name: tcpdump - path: capture.pcap + - run: sudo snap logs -n all aproxy \ No newline at end of file diff --git a/aproxy.go b/aproxy.go index 2ca9b14..511ff1e 100644 --- a/aproxy.go +++ b/aproxy.go @@ -222,7 +222,7 @@ func DialProxy(proxy string) (net.Conn, error) { // DialProxyConnect dials the TCP connection and finishes the HTTP CONNECT handshake with the proxy. // dst: HOST:PORT or IP:PORT -func DialProxyConnect(proxy string, dst string) (net.Conn, error) { +func DialProxyConnect(proxy string, src, dst string) (net.Conn, error) { conn, err := DialProxy(proxy) if err != nil { return nil, err @@ -236,7 +236,8 @@ func DialProxyConnect(proxy string, dst string) (net.Conn, error) { ProtoMajor: 1, ProtoMinor: 1, Header: map[string][]string{ - "User-Agent": {fmt.Sprintf("aproxy/%s", version)}, + "User-Agent": {fmt.Sprintf("aproxy/%s", version)}, + "X-Forwarded-For": {src}, }, Host: dst, } @@ -289,7 +290,7 @@ func RelayTCP(conn io.ReadWriter, proxyConn io.ReadWriteCloser, logger *slog.Log // RelayHTTP relays a single HTTP request and response between a local connection and a proxy. // It modifies the Connection header to "close" in both the request and response. -func RelayHTTP(conn io.ReadWriter, proxyConn io.ReadWriteCloser, logger *slog.Logger) { +func RelayHTTP(conn io.ReadWriter, src string, proxyConn io.ReadWriteCloser, logger *slog.Logger) { defer proxyConn.Close() req, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { @@ -302,6 +303,10 @@ func RelayHTTP(conn io.ReadWriter, proxyConn io.ReadWriteCloser, logger *slog.Lo req.Header.Set("User-Agent", "") } req.Header.Set("Connection", "close") + // For now strip and replace the X-Forwarded-For header from incoming requests. + // We cannot trust X-Forwarded-For on arbitrary requests for security reasons. + // In the future, consider conditionally trusting X-Forwarded-For from known clients. + req.Header.Set("X-Forwarded-For", src) if req.Proto == "HTTP/1.0" { // no matter what the request protocol is, Go enforces a minimum version of HTTP/1.1 // this causes problems for HTTP/1.0 only clients like GPG (HKP) @@ -349,6 +354,7 @@ func HandleConn(conn net.Conn, proxy string) { return } logger = logger.With("original_dst", dst) + src, _, _ := net.SplitHostPort(conn.RemoteAddr().String()) consigned := NewPrereadConn(conn) switch dst.Port { case 443: @@ -359,7 +365,7 @@ func HandleConn(conn net.Conn, proxy string) { } else { host := fmt.Sprintf("%s:%d", sni, dst.Port) logger = logger.With("host", host) - proxyConn, err := DialProxyConnect(proxy, host) + proxyConn, err := DialProxyConnect(proxy, src, host) if err != nil { logger.Error("failed to connect to http proxy", "error", err) return @@ -383,11 +389,11 @@ func HandleConn(conn net.Conn, proxy string) { return } logger.Info("relay HTTP connection to proxy") - RelayHTTP(consigned, proxyConn, logger) + RelayHTTP(consigned, src, proxyConn, logger) default: consigned.EndPreread() logger = logger.With("host", fmt.Sprintf("%s:%d", dst.IP.String(), dst.Port)) - proxyConn, err := DialProxyConnect(proxy, fmt.Sprintf("%s:%d", dst.IP.String(), dst.Port)) + proxyConn, err := DialProxyConnect(proxy, src, fmt.Sprintf("%s:%d", dst.IP.String(), dst.Port)) if err != nil { logger.Error("failed to connect to tcp proxy", "error", err) return diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index dbcc873..d4984c9 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: aproxy -version: 0.2.5 +version: 0.3.0 summary: Transparent proxy for HTTP and HTTPS/TLS connections. description: | Aproxy is a transparent proxy for HTTP and HTTPS/TLS connections. By