新手问题 100行go代码实现基于 QUIC 的 http 代理

AlexaMa · 2018年03月24日 · 474 次阅读

本站开启支持 QUIC 的方法与配置后,主观感觉从国内访问快了很多。看了一下 Chrome 的 timing, 大部分建立连接都能够做到 0-RTT:

既然这样,顺手实现一个基于 QUIC 的 http 代理,把平时查资料时使用的网络也顺带加速一下。(对了,前两天看到 Google 发布了Outline, 看来这项运动从来都不缺少运动员哪……)

http 代理原理

http 代理处理 http 和 https 请求的方式有所不同。对于 http 请求:

  1. 浏览器与代理服务器建立 TCP 连接后,将 http 请求发送给代理服务器。
  2. 代理服务器将 http 请求发送给目标服务器。
  3. 代理服务器获取到相应结果以后,将结果发送给浏览器。

这里有一个细节需要注意,浏览器向代理服务器发送的 http 请求 URI 与直接访问有所不同。

浏览器直接访问 GET http://www.yahoo.com 的 http 请求格式为:

GET / HTTP/1.1
User-Agent: Quic-Proxy
...

而向代理服务器发送的 http 请求格式为:

GET http://www.yahoo.com HTTP/1.1
User-Agent: Quic-Proxy
...

也就是浏览器想代理服务器发送的 http 请求 URI 中包含了 scheme 和 host,目的是为了让代理服务器知道这个代理请求要访问的目标服务器地址。

对于 https 请求,一般是通过CONNECT建立隧道:

  1. 浏览器向代理服务器建立 TCP 连接,发送CONNECT请求。
  2. 代理服务器根据CONNECT请求中包含的 host 信息,向目标服务器建立 TCP 连接,然后向浏览器返回200连接成功的响应。
  3. 这时代理服务器同时维持着连接浏览器和目标服务器的 TCP 连接。
  4. 从浏览器的角度看,相当于建立了一条直连目标服务器的 TCP 隧道。然后直接在该隧道上进行 TLS 握手,发送 http 请求即可实现访问目标服务器的目的。

QUIC Proxy 的设计与实现

QUIC Proxy 部署结构图

QUIC Proxy 的部署结构与上面 http 代理原理稍微有所不同。主要区别是增加了qpclient。主要原因是应用程序与代理服务器支架的请求是明文传输(http 请求代理是全明文,https 请求代理时的 CONNECT 头会泄露目标服务器信息)。我们是要隐私的人(虽然小扎可能并不 care),因此,在应用程序与qpserver之间加了一个qpclient,之间使用QUIC作为传输层。

实现

QUIC Proxy 使用 Go 实现,猴急的同学可以直接到 github 看源码:Quic Proxy, a http/https proxy using QUIC as transport layer.

代码比较简单,基于标准库的http.Server根据 http 代理的原理进行了一点 http 请求的修改。然后,因为qpclientqpserver之间使用 QUIC 作为 transport,而 QUIC 上的每一个 connection 都是可以多路复用(multiplexing)的,因此,对于qpserver需要自己实现一个传入 http.Server 的 listener:

type QuicListener struct {
    quic.Listener
    chAcceptConn chan *AcceptConn
}

type AcceptConn struct {
    conn net.Conn
    err  error
}

func NewQuicListener(l quic.Listener) *QuicListener {
    ql := &QuicListener{
        Listener:     l,
        chAcceptConn: make(chan *AcceptConn, 1),
    }
    go ql.doAccept()
    return ql
}

func (ql *QuicListener) doAccept() {
    for {
        sess, err := ql.Listener.Accept()
        if err != nil {
            log.Error("accept session failed:%v", err)
            continue
        }
        log.Info("accept a session")

        go func(sess quic.Session) {
            for {
                stream, err := sess.AcceptStream()
                if err != nil {
                    log.Error("accept stream failed:%v", err)
                    sess.Close(err)
                    return
                }
                log.Info("accept stream %v", stream.StreamID())
                ql.chAcceptConn <- &AcceptConn{
                    conn: &QuicStream{sess: sess, Stream: stream},
                    err:  nil,
                }
            }
        }(sess)
    }
}

func (ql *QuicListener) Accept() (net.Conn, error) {
    ac := <-ql.chAcceptConn
    return ac.conn, ac.err
}

同样的,qpclientqpserver建立连接也需要考虑到多路复用的问题,实现实现一个基于 QUIC 的 dialer:

type QuicStream struct {
    sess quic.Session
    quic.Stream
}

func (qs *QuicStream) LocalAddr() net.Addr {
    return qs.sess.LocalAddr()
}

func (qs *QuicStream) RemoteAddr() net.Addr {
    return qs.sess.RemoteAddr()
}

type QuicDialer struct {
    skipCertVerify bool
    sess           quic.Session
    sync.Mutex
}

func NewQuicDialer(skipCertVerify bool) *QuicDialer {
    return &QuicDialer{
        skipCertVerify: skipCertVerify,
    }
}

func (qd *QuicDialer) Dial(network, addr string) (net.Conn, error) {
    qd.Lock()
    defer qd.Unlock()

    if qd.sess == nil {
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: qd.skipCertVerify}, nil)
        if err != nil {
            log.Error("dial session failed:%v", err)
            return nil, err
        }
        qd.sess = sess
    }

    stream, err := qd.sess.OpenStreamSync()
    if err != nil {
        log.Info("[1/2] open stream from session no success:%v, try to open new session", err)
        qd.sess.Close(err)
        sess, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil)
        if err != nil {
            log.Error("[2/2] dial new session failed:%v", err)
            return nil, err
        }
        qd.sess = sess

        stream, err = qd.sess.OpenStreamSync()
        if err != nil {
            log.Error("[2/2] open stream from new session failed:%v", err)
            return nil, err
        }
        log.Info("[2/2] open stream from new session OK")
    }

    log.Info("addr:%s, stream_id:%v", addr, stream.StreamID())
    return &QuicStream{sess: qd.sess, Stream: stream}, nil
}

好吧,我承认实现代码似乎在 200 行左右……但是,我们实现了一个 client 和一个 server, 平均下来基本控制在 100 行左右,对吧……(逃……)

部署

:需要 golang 版本 >= 1.9

1. 在远程服务器上安装 qpserver

go get -u github.com/liudanking/quic-proxy/qpserver

2. 启动qpserver:

qpserver -v -l :3443 -cert YOUR_CERT_FILA_PATH -key YOUR_KEY_FILE_PATH

3. 在本地安装 qpclient

go get -u github.com/liudanking/quic-proxy/qpclient

4. 启动 qpclient:

qpclient -v -k -proxy http://YOUR_REMOTE_SERVER:3443 -l 127.0.0.1:18080

5. 设置应用程序代理:

以 Chrome with SwitchyOmega 为例:

Enjoy!

Originally pulished on liudanking.com

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册