Skip to content
http流量伪装的socks5代理
Go
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.idea
mio 优化完成 Apr 15, 2019
utils
.gitignore
README.md Update README.md Apr 14, 2019
init.go 优化完成 Apr 15, 2019
main.go
sogo.json

README.md

sogo

一个使用http流量进行混淆的socks5代理。

服务器端:sogo-server

之前写了一个http代理,用起来也是十分地舒服,但是有几个点还是有些遗憾的:

  • http代理只能代理http协议,相比socks5代理不够通用。。
  • netty是个好框架,但是java占用内存是真的多。。

所以,我又写了一个socks5代理,起名叫sogo

sogo本身包含sogo(client)和sogo-server。如果把sogo和sogo-server看成一个整体,一个黑盒,这个整体就是一个socks5代理。sogo(client)与本地电脑交互;sogo-server与目标网站交互;sogo(client)和sogo-server之间的交互就是http协议包裹payload进行通信。

运行日志

以下是观看一个视频的日志:

2019/04/10 00:14:28 main.go:58: 与客户端握手成功
2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:15 目标域名:www.youtube.com 目标端口:443
2019/04/10 00:14:28 main.go:58: 与客户端握手成功
2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:11 目标域名:i.ytimg.com 目标端口:443
2019/04/10 00:14:28 main.go:58: 与客户端握手成功
2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:13 目标域名:yt3.ggpht.com 目标端口:443
2019/04/10 00:14:35 main.go:58: 与客户端握手成功
2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443
2019/04/10 00:14:35 main.go:58: 与客户端握手成功
2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443
2019/04/10 00:14:35 main.go:58: 与客户端握手成功
2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443

以下是访问github的日志:

2019/04/10 00:15:57 main.go:58: 与客户端握手成功
2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443
2019/04/10 00:15:57 main.go:58: 与客户端握手成功
2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443
2019/04/10 00:15:59 main.go:58: 与客户端握手成功
2019/04/10 00:15:59 main.go:207: 目的地址类型:3 域名长度:15 目标域名:live.github.com 目标端口:443
2019/04/10 00:16:00 main.go:58: 与客户端握手成功
2019/04/10 00:16:00 main.go:207: 目的地址类型:3 域名长度:14 目标域名:api.github.com 目标端口:443

特性

sogo项目最好的两个特性如下:

  1. 使用http包裹payload(有意义的数据)。
  2. 将sogo-server所在的ip:端口伪装成一个http网站。

效用、坚固、美观——对软件产品的三个要求。上面两个特性,既可以说是坚固,也可以说是美观,至于效用就不用说了,在这里谈坚固和美观的前提就是效用被完整地实现。用通俗地话来说,这个代理的坚固和美观就是:伪装、防止被识别。

处理socks5握手——对socks5协议的实现

sogo(client)与本地电脑交互,因此需要实现socks5协议,与本地用户(比如chrome)握手协商。

一个典型的sock5握手的顺序:

  1. client:0x05 0x01 0x00
  2. server: 0x05 0x00
  3. client: 0x05 0x01 0x00 0x01 ip1 ip2 ip3 ip4 0x00 0x50
  4. server: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10
  5. client与server开始盲转发

这一部分代码见如下两个函数:

//file: sogo/main.go

//读 5 1 0 写回 5 0
func handshake(clientCon net.Conn) error {
	var buf = make([]byte, 300)
	numRead, err := clientCon.Read(buf)
	if err != nil {
		return err
	} else if numRead == 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
		return mio.WriteAll(clientCon, []byte{0x05, 0x00})
	} else {
		log.Printf("%d", buf[:numRead])
		return mio.WriteAll(clientCon, []byte{0x05, 0x00})
	}
}

func getTargetAddr(clientCon net.Conn) (string, error) {
	var buf = make([]byte, 1024)
	numRead, err := clientCon.Read(buf)
	if err != nil {
		return "", err
	} else if numRead > 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 {
		if buf[3] == 3 {
			log.Printf("目的地址类型:%d 域名长度:%d 目标域名:%s 目标端口:%s", buf[3], buf[4], buf[5:5+buf[4]], strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))))
			writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10})
			return string(buf[5:5+buf[4]]) + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))), writeErr
		} else if buf[3] == 1 {
			log.Printf("目的地址类型:%d  目标域名:%s 目标端口:%s", buf[3], net.IPv4(buf[4], buf[5], buf[6], buf[7]).String(), strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))))
			writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10})
			return net.IPv4(buf[4], buf[5], buf[6], buf[7]).String() + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))), writeErr
		} else {
			return "", errors.New("不能处理ipv6")
		}

	} else {
		return "", errors.New("不能处理非CONNECT请求")
	}
}

完成handshake, getTargetAddr 之后,chrome就会发送真实的http请求了,sogo(client) 要做的就是将这部分http请求进行加密,然后加上http请求的头,发送到sogo-server。

使用http包裹payload

第一部分:如何将真实的http请求,再进行加密,最后加上假的http请求头,变成伪装好的http请求,发送给sogo-server。

//file: sogo/mio/prefix.go
var fakeHost = "qtgwuehaoisdhuaishdaisuhdasiuhlassjd.com"  //虚假host

func AppendHttpRequestPrefix(buf []byte, addr string) []byte {
	Simple(&buf, len(buf))//对真实的http请求的简单加密
	// 演示base64编码
	addrBase64 := base64.NewEncoding("abcdefghijpqrzABCKLMNOkDEFGHIJl345678mnoPQRSTUVstuvwxyWXYZ0129+/").EncodeToString([]byte(addr))
	buf = append([]byte("POST /target?at="+addrBase64+" HTTP/1.1\r\nHost: "+fakeHost+"\r\nAccept: */*\r\nContent-Type: text/plain\r\naccept-encoding: gzip, deflate\r\ncontent-length: "+strconv.Itoa(len(buf))+"\r\n\r\n"), buf...)
	return buf
}

包裹完毕之后返回的[]byte就可以发送给sogo-server了。

第二部分:将sogo-server从目标网站获得的真实响应进行简单加密,包裹http响应头,发送给sogo(client)。

//file: sogo-server/mio/prefix.go
func AppendHttpResponsePrefix(buf []byte) []byte {
	Simple(&buf, len(buf))
	buf = append([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: "+strconv.Itoa(len(buf))+"\r\n\r\n"), buf...)
	return buf
}

包裹完毕之后返回的[]byte就可以发送给sogo(client)了。

解包伪装好的请求、响应

先看以下伪装好的请求的样子:

POST /target?at={targetAddrBase64} HTTP/1.1
Host: {fakehost}
Accept: */*
Content-Type: text/plain
accept-encoding: gzip, deflate
content-length: {content-length}

{payload-after-crypto}

sogo-server拿到这个伪装好的请求,要做的事有:

  1. 获取{targetAddrBase64},拿到真实的目标网站地址
  2. 获取请求头的Host字段,如果不是定义好的fakehost,则说明是直接访问sogo-server,这时sogo-server就是个到混淆网站的反向代理(这就是之前提到的第二个特性。下面将会详细解释如何实现
  3. 获取{content-length},根据这个content-length确定payload部分的长度。
  4. 读取指定长度的payload,解密,并创建到targetAddr的连接,转发至targetAddr

这些步骤很明确吧。其实有一些细节,挺麻烦的。

tcp是面向流的协议,也就是会有很多个连续的上面的片段,要合理划分出这些片段。有些人称这个为解决“tcp粘包”,谷歌tcp粘包就能搜到如何实现这个需求。但是注意,不要称这个为“tcp粘包”,别人会说tcp是面向流的协议,哪来什么包,你知识体系有问题,你看过tcp协议没有。这些话都是知乎上某一问题的答案说的。所以,别说“tcp粘包”,但是可以用这个关键词去搜索如何解决这个问题。

如果,现在你看了如何解决这个问题,其实就是一句话,在tcp上层定义自己的应用层协议:也就是tcp报文的格式。http这个应用层协议就是一种tcp报文的一种定义。

我们的伪装好的报文就是http协议,所以要做的就是实现自己的http请求解析器,获取我们关心的信息。

sogo的http请求解析器,在:

//file sogo-server/server.go
func read(clientConn net.Conn, redundancy []byte) (payload, redundancyRetain []byte, target string, readErr error)

这一部分有点繁杂。。不多解释,自己看代码吧。

伪装sogo-server:80为其他http网站

这一部分就是第二特性:将sogo-server所在的ip:端口伪装成一个http网站。

上一节,我们提到 {fakehost}。我们故意将{fakehost}定义为一个复杂、很长的域名。我们伪装的请求,都会带有如下请求头

Host: {fakehost}

如果,http请求的Host不是这个{fakehost}则说明这不是一个经sogo(client)的请求,而是直接请求了sogo-server。也就是,有人来嗅探啦!

对这种,我们就会将该请求,原封不动地转到伪装站。(其实还是有点修改的,但这是细节,看代码吧)所以,直接访问sogo-server-ip:80 就是访问伪装站:80。

linux上服务端部署

wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server
wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server.json

chmod +x sogo-server
mv -f sogo-server /usr/local/bin/
mv -f sogo-server.json /usr/local/bin/
kill -9 $(lsof -i:80|tail -1|awk '$1!=""{print $2}') #关闭80端口应用
ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接
(sogo-server &)

linux上客户端安装

# 国内机器下面两个wget会很慢,考虑本地下载再上传到服务器吧
wget https://github.com/arloor/sogo/releases/download/v1.0/sogo.json
wget https://github.com/arloor/sogo/releases/download/v1.0/sogo

chmod +x sogo
mv -f sogo /usr/local/bin/
mv -f sogo.json /usr/local/bin/
ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接
# 运行前,先修改/usr/local/bin/sogo.json
(sogo &) #以 /usr/local/bin/sogo.json 为配置文件  该配置下,服务端地址被设置为proxy
#(sogo -c path &)  #以path指向的文件为配置文件

windows客户端安装

Release下载sogo.exesogo.json

sogo.json内容如下:

{
  "ClientPort": 8888,
  "Use": 0,
  "Servers": [
    {
      "ProxyAddr": "proxy",
      "ProxyPort": 80,
      "UserName": "a",
      "Password": "b"
    }
  ],
  "Dev":false
}

先修改ProxyAddr为服务端安装的地址即可。其他配置项是高级功能,例如多服务器管理,多用户管理(用户认证)等等。

shadowsocks是没有多用户管理的,ss每个端口对应一个用户。sogo则使用用户名+密码认证,使多个用户使用同一个服务器端口。

修改好之后,双击sogo.exe,这时会发现该目录下多了一个 sogo_8888.log 的文件,这就说明,在本地的8888端口启动好了这个sock5代理。(没有界面哦。

结束

这篇博客梳理了一下sogo的实现原理,总之,sogo是一个优雅的代理。sogo代码不多,对go语言、网络编程感兴趣的人可以看看。

You can’t perform that action at this time.