AdguardTeam / gomitmproxy

Simple golang mitm proxy implementation
https://adguard.com/
GNU General Public License v3.0
293 stars 59 forks source link

The latest progress on adding support for transparent proxy to Gomitmproxy #11

Open lls275 opened 2 years ago

lls275 commented 2 years ago

Hey, I stumbled upon a very interesting GO extension this afternoon, it seems to avoid a lot of code changes in Gomitmproxy and implement the function of adding a transparent proxy, I found that adding to Gomitmproxy this afternoon, there seems to be progress, and under the transparent proxy, there is no Before the failed to read request: malformed HTTP request appeared, this seems to be a good start, and I only changed a small range, but I still encountered some problems, which may need the author's help (there is a certificate verification problem , I debug test, but my ability is not enough, I hope the author can solve it).

I found a very interesting extension library, I believe the author must be very interested, https://github.com/inconshreveable/go-vhost, it can get the real hostname in the case of transparent proxy, including the one passed by the client Key information, which seems to be in line with Gomitmproxy's small changes to have the function of transparent proxy.

The only thing I changed is: https://github.com/AdguardTeam/gomitmproxy/blob/master/proxy.go#L210

type dumbResponseWriter struct {
    net.Conn
}

func (dumb dumbResponseWriter) Header() http.Header {
    panic("Header() should not be called on this ResponseWriter")
}

func (dumb dumbResponseWriter) Write(buf []byte) (int, error) {
    if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) {
        return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request
    }
    return dumb.Conn.Write(buf)
}

func (dumb dumbResponseWriter) WriteHeader(code int) {
    panic("WriteHeader() should not be called on this ResponseWriter")
}

func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
    return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil
}

func (p *Proxy) connHijacker(w http.ResponseWriter) net.Conn {
    hij, ok := w.(http.Hijacker)
    if !ok {
        log.Error("httpserver does not support hijacking")
    }
    proxyClient, _, e := hij.Hijack()
    if e != nil {
        log.Error("Cannot hijack connection " + e.Error())
    }
    proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))

    return proxyClient
}

// handleRequest reads an incoming request and processes it
func (p *Proxy) handleRequest(ctx *Context) error {
    tlsConn, err := vhost.TLS(ctx.conn)
    origReq := &http.Request{
        Method: "CONNECT",
        URL: &url.URL{
            Opaque: tlsConn.Host(),
            Host:   net.JoinHostPort(tlsConn.Host(), "443"),
        },
        Host:       tlsConn.Host(),
        Header:     make(http.Header),
        RemoteAddr: ctx.conn.RemoteAddr().String(),
    }
    hijackerConn := p.connHijacker(dumbResponseWriter{tlsConn})
    ctx.conn = hijackerConn

    //origReq, err := p.readRequest(ctx)

Then, pass the traffic to Gomitmproxy again via iptable:


sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to 10.20.1.1:12345
sudo iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to 10.20.1.1:12345

After execution, I found that, hey, failed to read request: malformed HTTP request did not appear, it seems to be good: proxy.go.zip

2022/04/09 10:28:59 12416#6 [debug] id=100476: accepted connection from 10.20.1.92:51399
2022/04/09 10:29:00 12416#3207 [debug] id=100474-1: tunnel finished copying
2022/04/09 10:29:00 12416#3201 [debug] id=100474-1: closed CONNECT tunnel
2022/04/09 10:29:00 12416#3201 [debug] id=100474: closing connection due to: closing connection
2022/04/09 10:29:00 12416#3218 [debug] id=100476-1: handle request CONNECT http:init.push.apple.com
2022/04/09 10:29:00 12416#3218 [debug] id=100476-1: connecting to host: init.push.apple.com:443
2022/04/09 10:29:00 12416#3217 [debug] id=100475-1: tunnel finished copying
2022/04/09 10:29:00 12416#3218 [debug] id=100476-1: connecting to tcp://init.push.apple.com:443

Then, the browser cannot display the normal page and prompts:

safari cannot open the page because it could not establish a secure connection to the server

It seems that the problem appears in the certificate. Can the author reproduce it and help solve it?

lls275 commented 2 years ago

The above should be a good start and hopefully it will be resolved.Below I provide the Debug information of the Vhost extension, which seems to be very rich

p1
lls275 commented 2 years ago

This is a simple example of vhost officially available, and I can run through it and run it normally, but I don't know how to add it to gomitmproxy. Can the author add it? vhost_package_demo.go.zip

package main

import (
    "bufio"
    "bytes"
    "github.com/AdguardTeam/golibs/log"
    "goproxy/go-vhost"
    "io"
    "net"
    "net/http"
    "net/url"
)

func main() {
    l, err := net.Listen("tcp", ":12345")
    log.Printf("start listening to %s", l.Addr())

    if err != nil {
        log.Fatalf("Error listening for https connections - %v", err)
    }
    for {
        c, err := l.Accept()
        if err != nil {
            log.Printf("Error accepting new connection - %v", err)
            continue
        }
        go func(c net.Conn) {
            tlsConn, err := vhost.TLS(c)
            if err != nil {
                log.Printf("Error accepting new connection - %v", err)
            }
            if tlsConn.Host() == "" {
                log.Printf("Cannot support non-SNI enabled clients")
                return
            }
            connectReq := makeRequest(c, tlsConn)
            resp := dumbResponseWriter{tlsConn}

            ServeHTTP(resp, connectReq)
        }(c)
    }
}

func makeRequest(c net.Conn, tlsConn *vhost.TLSConn) *http.Request {
    return &http.Request{
        Method: "CONNECT",
        URL: &url.URL{
            Opaque: tlsConn.Host(),
            Host:   net.JoinHostPort(tlsConn.Host(), "443"),
        },
        Host:       tlsConn.Host(),
        Header:     make(http.Header),
        RemoteAddr: c.RemoteAddr().String(),
    }
}

func ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method == "CONNECT" {
        hij, ok := w.(http.Hijacker)
        if !ok {
            panic("httpserver does not support hijacking")
        }

        proxyClient, _, e := hij.Hijack()
        proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
        if e != nil {
            panic("Cannot hijack connection " + e.Error())
        }
        targetSiteCon, _ := net.Dial("tcp", r.URL.Host)

        log.Info("Accepting CONNECT to %s", r.URL.Host)

        donec := make(chan bool, 2)
        go copyConnectTunnel(targetSiteCon, proxyClient, donec)
        go copyConnectTunnel(proxyClient, targetSiteCon, donec)
        <-donec
        <-donec
    }
}

func copyConnectTunnel(w io.Writer, r io.Reader, donec chan<- bool) {
    if _, err := io.Copy(w, r); err != nil {
        log.Error("Error copying to client: %s", err)
    }
    donec <- true
}

type dumbResponseWriter struct {
    net.Conn
}

func (dumb dumbResponseWriter) Header() http.Header {
    panic("Header() should not be called on this ResponseWriter")
}

func (dumb dumbResponseWriter) Write(buf []byte) (int, error) {
    if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) {
        return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request
    }
    return dumb.Conn.Write(buf)
}

func (dumb dumbResponseWriter) WriteHeader(code int) {
    panic("WriteHeader() should not be called on this ResponseWriter")
}

func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
    return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil
}
ameshkov commented 2 years ago

At this point it'd be better to fork gomitmproxy to another repo and implement it there.

Eventually, we'll add transparent proxy support here as well, but we'll really need to devote quite some time to do that.