之前自己实现了一个代理服务,当时考虑的是只要支持SOCKS5就好了,因为我经常用CHROME,配合着SwitchySharp,体验还是很棒的。但是我现在有点讨厌CHROME,它现在太庞大了,占用资源太多了。而且我有锁定网页的习惯,一打开CHROME,就十几个甚至二十几个进程起来,让我很不爽。但是不得不说CHROME的安全设计还是非常棒的。然后我就试了下FireFox,额,我觉着它和IE差不多.然后就放弃了,然后看看了手头上的IE已经到11了,平时用起来感觉还是很不错的,所以我想支持IE的代理。
IE的代理机制比较囧,比如说它只支持SOCKS4,不支持SOCKS5,然后又分为HTTP代理,HTTPS代理,还有FTP代理。也没有像CHROME提供强大的代理插件机制。虽然IE提供了PAC机制,但是不得不说,这个机制也很鸡肋,没有像SwitchySharp那样可以做到实时的增减规则。针对以上原因,我就在原有的代码基础上增加了上面的几种代理,不过没支持FTP代理。
SOCKS4代理
SOCKS4协议比较简单,可以参考的文档是WIKI的这篇,还有OpenSSH的这篇。后面还有个SOCKS4A协议,不过这个SOCKS4A基本上没见到人用过。SOCKS4协议的CONNECT命令格式很简单,就一个请求包和回应包。请求包的第一个字段是版本号,占用1个字节,就是0x04,第二个字段是命令类型,占用1个字节,0x01表示CONNECT命令,即请求链接哪个IP : PORT,0x02是BIND,一般用于FTP场景,我没有实现。第三个字段是对端端口,占用2个字节,字节序是网络字节顺序;第四个字段是对端IP,占用4个字节,字节序是网络字节顺序;第五个字段是USERID,可变长度,以0x00结尾。这里要注意的是,在IE11下,USERID为当前用户名,不会为空。所以要读取完整的USERID和最后的0x00。
回应包第一个字段占用一个字节,数据为0;第二个字段占用一个字节,表示状态,0X5A表示成功,0X5B表示拒绝或者失败等等;第三个字节和第四个字段一共6个字节,会被忽略,直接填0即可。
整个协议简单很多,比SOCKS5简单多,但是没有SOCKS5强大。因为SOCKS4只支持IP : PORT方式,也就意味着IEFQ的时候,会自己先走本地DNS,然后拿到地址后才去走SOCKS代理。这里带来的问题是,如果DNS被污染了,就意味着FQ失败了。所以还得用后面的HTTP代理和HTTP隧道。
HTTP Tunnel (HTTP隧道)
HTTP隧道比较简单。就是客户端通过HTTP协议链接到服务端,请求服务端去链接某个域名或者IP的某个端口。协议非常简单,即客户端发送CONNECT Domain : Port HTTP/1.0\r\n\r\n。服务端收到该请求后会去链接指定的域名和端口,链接成功后,会回复客户端HTTP/1.0 200 Connection established\r\n\r\n 客户端收到该回复后,就开始把数据通过代理转发过去。这个时候的代理是盲转,和SOCKS协议一样。
用GO实现的时候也相对来说比较简单,通过net/http包即可完成。自己实现一个ServeHTTP方法,然后发现是CONNECT方法的请求就把连接Hijacked掉。具体代码如下:
hj, ok := response.(http.Hijacker) if !ok { http.Error(response, "Hijacker failed", http.StatusInternalServerError) return } conn, _, err := hj.Hijack() if err != nil { http.Error(response, err.Error(), http.StatusInternalServerError) return } defer conn.Close()
要注意的一点是Hijack后,如果要回复HTTP协议格式的数据,就要自己去操作了,没有办法再使用net/http.ResponseWriter提供的方法了。好在GO的fmt包提供了Fprint/Fprintf函数,所以操作起来也还算简单。
另外一点是,这个HTTP隧道允许在CONNECT发起时,在BODY里携带额外数据以达到优化的目的。所以还要在建立远端链接后,检查是否还有BODY数据,如果有的话,就把数据发出去。
HTTP Proxy
我原先认为的是既然有了HTTP隧道方式的代理机制了,那就都用这套呗,结果IE不这样,HTTP隧道只用在了HTTPS类型的URL,而普通的HTTP URL则走的是普通的HTTP代理机制。HTTP普通的请求类似于下面这样:GET /xxx/yyyy/zzzz.html HTTP/1.0,而HTTP代理则是GET http://www.qqqq.com/xxx/yyy/zzz.html HTTP/1.0,然后还会增加一个额外的HTTP首部Proxy-Connection。这个首部用来干嘛的自行GOOGLE。处理客户端发来的HTTP代理请求时,我的做法是把URL替换为正常的相对URI,然后检查是否存在Proxy-Connection,如果存在,则获取对应的值,并删除该首部,并添加Connection首部,其值为原Proxy-Connection对应的值。然后转发到对端服务器。 GO提供了一个包net/http/httputil,其中封装了一个反向代理的实现。只需要提供建立链接的函数以及对http.Request处理的函数即可。代码具体如下:
func NewHTTPProxy(remoteSocks, cryptoMethod string, password []byte) *HTTPProxy { return &HTTPProxy{ ReverseProxy: &httputil.ReverseProxy{ Director: director, Transport: &http.Transport{ Dial: func(network, addr string) (net.Conn, error) { return dial(network, addr, remoteSocks, cryptoMethod, password) }, }, }, } } func dial(network, addr, remoteSocks, cryptoMethod string, password []byte) (net.Conn, error) { tcpAddr, err := net.ResolveTCPAddr(network, addr) if err != nil { return nil, err } remoteSvr, err := NewRemoteSocks(remoteSocks, cryptoMethod, password) if err != nil { return nil, err } // version(1) + cmd(1) + reserved(1) + addrType(1) + domainLength(1) + maxDomainLength(256) + port(2) req := []byte{0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} copy(req[4:8], []byte(tcpAddr.IP.To4())) binary.BigEndian.PutUint16(req[8:10], uint16(tcpAddr.Port)) err = remoteSvr.Handshake(req) if err != nil { remoteSvr.Close() return nil, err } conn := &HTTPProxyConn{ RemoteSocks: remoteSvr, } return conn, nil } func director(request *http.Request) { u, err := url.Parse(request.RequestURI) if err != nil { return } request.RequestURI = u.RequestURI() v := request.Header.Get("Proxy-Connection") if v != "" { request.Header.Del("Proxy-Connection") request.Header.Del("Connection") request.Header.Add("Connection", v) } }
总结:
本质上HTTP代理和HTTP隧道可以通过同一个端口实现的,但是我没有这样去做,因为我觉着代码分开更方便测试和修改。可以省去很多的麻烦。不过通过简单的组合也一样可以复用同一个端口,我后面会试着去修改。HTTP PROXY和TUNNEL现在在同一个端口实现,通过简单的组合就实现了对应的功能。而SOCKS4和SOCKS5按理来说也是可以用同一个端口的,但是考虑到代码中要判断版本之类的问题,我觉着这样还不如直接分开实现来的简单。
后面还可以考虑的是把SwitchySharp的代理策略移植到该代理服务上,然后再写个IE插件用来实现类似SwitchySharp的功能,这样的话,会方便很多。顺便说下,其实现在IE做的很不错。