All Projects → arloor → Sogo

arloor / Sogo

http流量伪装的socks5代理

Programming Languages

go
31211 projects - #10 most used programming language

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进行通信。

电报讨论组

电报讨论组 https://t.me/popstary

运行日志

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

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语言、网络编程感兴趣的人可以看看。

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].