├── .gitignore ├── .idea ├── misc.xml ├── modules.xml ├── sogo.iml └── vcs.xml ├── README.md ├── init.go ├── main.go ├── mio ├── io.go └── prefix.go ├── sogo.json └── utils └── tool.go /.gitignore: -------------------------------------------------------------------------------- 1 | sogo* -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sogo.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sogo 2 | 3 | 一个使用http流量进行混淆的socks5代理。 4 | 5 | 服务器端:[sogo-server](https://github.com/arloor/sogo-server) 6 | 7 | 之前写了一个http代理,用起来也是十分地舒服,但是有几个点还是有些遗憾的: 8 | 9 | - http代理只能代理http协议,相比socks5代理不够通用。。 10 | - netty是个好框架,但是java占用内存是真的多。。 11 | 12 | 所以,我又写了一个socks5代理,起名叫[sogo](https://github.com/arloor/sogo)。 13 | 14 | sogo本身包含sogo(client)和sogo-server。如果把sogo和sogo-server看成一个整体,一个黑盒,这个整体就是一个socks5代理。sogo(client)与本地电脑交互;sogo-server与目标网站交互;sogo(client)和sogo-server之间的交互就是http协议包裹payload进行通信。 15 | 16 | 17 | ## 电报讨论组 18 | 19 | 电报讨论组 https://t.me/popstary 20 | 21 | ## 运行日志 22 | 23 | 以下是观看一个视频的日志: 24 | 25 | ```shell 26 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功 27 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:15 目标域名:www.youtube.com 目标端口:443 28 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功 29 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:11 目标域名:i.ytimg.com 目标端口:443 30 | 2019/04/10 00:14:28 main.go:58: 与客户端握手成功 31 | 2019/04/10 00:14:28 main.go:207: 目的地址类型:3 域名长度:13 目标域名:yt3.ggpht.com 目标端口:443 32 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功 33 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443 34 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功 35 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443 36 | 2019/04/10 00:14:35 main.go:58: 与客户端握手成功 37 | 2019/04/10 00:14:35 main.go:207: 目的地址类型:3 域名长度:32 目标域名:r2---sn-i3belnel.googlevideo.com 目标端口:443 38 | ``` 39 | 40 | 以下是访问github的日志: 41 | ```shell 42 | 2019/04/10 00:15:57 main.go:58: 与客户端握手成功 43 | 2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443 44 | 2019/04/10 00:15:57 main.go:58: 与客户端握手成功 45 | 2019/04/10 00:15:57 main.go:207: 目的地址类型:3 域名长度:10 目标域名:github.com 目标端口:443 46 | 2019/04/10 00:15:59 main.go:58: 与客户端握手成功 47 | 2019/04/10 00:15:59 main.go:207: 目的地址类型:3 域名长度:15 目标域名:live.github.com 目标端口:443 48 | 2019/04/10 00:16:00 main.go:58: 与客户端握手成功 49 | 2019/04/10 00:16:00 main.go:207: 目的地址类型:3 域名长度:14 目标域名:api.github.com 目标端口:443 50 | ``` 51 | 52 | 53 | 54 | ## 特性 55 | 56 | sogo项目最好的两个特性如下: 57 | 58 | 1. 使用http包裹payload(有意义的数据)。 59 | 2. 将sogo-server所在的ip:端口伪装成一个http网站。 60 | 61 | 效用、坚固、美观——对软件产品的三个要求。上面两个特性,既可以说是坚固,也可以说是美观,至于效用就不用说了,在这里谈坚固和美观的前提就是效用被完整地实现。用通俗地话来说,这个代理的坚固和美观就是:伪装、防止被识别。 62 | 63 | ## 处理socks5握手——对socks5协议的实现 64 | 65 | sogo(client)与本地电脑交互,因此需要实现socks5协议,与本地用户(比如chrome)握手协商。 66 | 67 | 一个典型的sock5握手的顺序: 68 | 69 | 1. client:0x05 0x01 0x00 70 | 2. server: 0x05 0x00 71 | 3. client: 0x05 0x01 0x00 0x01 ip1 ip2 ip3 ip4 0x00 0x50 72 | 4. server: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10 73 | 5. client与server开始盲转发 74 | 75 | 这一部分代码见如下两个函数: 76 | 77 | ```java 78 | //file: sogo/main.go 79 | 80 | //读 5 1 0 写回 5 0 81 | func handshake(clientCon net.Conn) error { 82 | var buf = make([]byte, 300) 83 | numRead, err := clientCon.Read(buf) 84 | if err != nil { 85 | return err 86 | } else if numRead == 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 { 87 | return mio.WriteAll(clientCon, []byte{0x05, 0x00}) 88 | } else { 89 | log.Printf("%d", buf[:numRead]) 90 | return mio.WriteAll(clientCon, []byte{0x05, 0x00}) 91 | } 92 | } 93 | 94 | func getTargetAddr(clientCon net.Conn) (string, error) { 95 | var buf = make([]byte, 1024) 96 | numRead, err := clientCon.Read(buf) 97 | if err != nil { 98 | return "", err 99 | } else if numRead > 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 { 100 | if buf[3] == 3 { 101 | 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]])))) 102 | writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10}) 103 | return string(buf[5:5+buf[4]]) + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))), writeErr 104 | } else if buf[3] == 1 { 105 | 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])))) 106 | writeErr := mio.WriteAll(clientCon, []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10}) 107 | return net.IPv4(buf[4], buf[5], buf[6], buf[7]).String() + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))), writeErr 108 | } else { 109 | return "", errors.New("不能处理ipv6") 110 | } 111 | 112 | } else { 113 | return "", errors.New("不能处理非CONNECT请求") 114 | } 115 | } 116 | ``` 117 | 118 | 完成handshake, getTargetAddr 之后,chrome就会发送真实的http请求了,sogo(client) 要做的就是将这部分http请求进行加密,然后加上http请求的头,发送到sogo-server。 119 | 120 | ## 使用http包裹payload 121 | 122 | 第一部分:如何将真实的http请求,再进行加密,最后加上假的http请求头,变成伪装好的http请求,发送给sogo-server。 123 | 124 | 125 | ```java 126 | //file: sogo/mio/prefix.go 127 | var fakeHost = "qtgwuehaoisdhuaishdaisuhdasiuhlassjd.com" //虚假host 128 | 129 | func AppendHttpRequestPrefix(buf []byte, addr string) []byte { 130 | Simple(&buf, len(buf))//对真实的http请求的简单加密 131 | // 演示base64编码 132 | addrBase64 := base64.NewEncoding("abcdefghijpqrzABCKLMNOkDEFGHIJl345678mnoPQRSTUVstuvwxyWXYZ0129+/").EncodeToString([]byte(addr)) 133 | 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...) 134 | return buf 135 | } 136 | ``` 137 | 138 | 包裹完毕之后返回的[]byte就可以发送给sogo-server了。 139 | 140 | 第二部分:将sogo-server从目标网站获得的真实响应进行简单加密,包裹http响应头,发送给sogo(client)。 141 | 142 | 143 | 144 | 145 | ```java 146 | //file: sogo-server/mio/prefix.go 147 | func AppendHttpResponsePrefix(buf []byte) []byte { 148 | Simple(&buf, len(buf)) 149 | 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...) 150 | return buf 151 | } 152 | ``` 153 | 154 | 包裹完毕之后返回的[]byte就可以发送给sogo(client)了。 155 | 156 | ## 解包伪装好的请求、响应 157 | 158 | 先看以下伪装好的请求的样子: 159 | 160 | ```shell 161 | POST /target?at={targetAddrBase64} HTTP/1.1 162 | Host: {fakehost} 163 | Accept: */* 164 | Content-Type: text/plain 165 | accept-encoding: gzip, deflate 166 | content-length: {content-length} 167 | 168 | {payload-after-crypto} 169 | ``` 170 | 171 | sogo-server拿到这个伪装好的请求,要做的事有: 172 | 173 | 1. 获取{targetAddrBase64},拿到真实的目标网站地址 174 | 2. 获取请求头的Host字段,如果不是定义好的fakehost,则说明是直接访问sogo-server,这时sogo-server就是个到混淆网站的反向代理(这就是之前提到的第二个特性。下面将会详细解释如何实现 175 | 3. 获取{content-length},根据这个content-length确定payload部分的长度。 176 | 4. 读取指定长度的payload,解密,并创建到targetAddr的连接,转发至targetAddr 177 | 178 | 这些步骤很明确吧。其实有一些细节,挺麻烦的。 179 | 180 | tcp是面向流的协议,也就是会有很多个连续的上面的片段,要合理划分出这些片段。有些人称这个为解决“tcp粘包”,谷歌tcp粘包就能搜到如何实现这个需求。但是注意,不要称这个为“tcp粘包”,别人会说tcp是面向流的协议,哪来什么包,你知识体系有问题,你看过tcp协议没有。这些话都是知乎上某一问题的答案说的。所以,别说“tcp粘包”,但是可以用这个关键词去搜索如何解决这个问题。 181 | 182 | 如果,现在你看了如何解决这个问题,其实就是一句话,在tcp上层定义自己的应用层协议:也就是tcp报文的格式。http这个应用层协议就是一种tcp报文的一种定义。 183 | 184 | 我们的伪装好的报文就是http协议,所以要做的就是实现自己的http请求解析器,获取我们关心的信息。 185 | 186 | sogo的http请求解析器,在: 187 | 188 | ```java 189 | //file sogo-server/server.go 190 | func read(clientConn net.Conn, redundancy []byte) (payload, redundancyRetain []byte, target string, readErr error) 191 | ``` 192 | 193 | 这一部分有点繁杂。。不多解释,自己看代码吧。 194 | 195 | ## 伪装sogo-server:80为其他http网站 196 | 197 | 这一部分就是第二特性:将sogo-server所在的ip:端口伪装成一个http网站。 198 | 199 | 上一节,我们提到 {fakehost}。我们故意将{fakehost}定义为一个复杂、很长的域名。我们伪装的请求,都会带有如下请求头 200 | 201 | ```shell 202 | Host: {fakehost} 203 | ``` 204 | 205 | 如果,http请求的Host不是这个{fakehost}则说明这不是一个经sogo(client)的请求,而是直接请求了sogo-server。也就是,有人来嗅探啦! 206 | 207 | 对这种,我们就会将该请求,原封不动地转到伪装站。(其实还是有点修改的,但这是细节,看代码吧)所以,直接访问sogo-server-ip:80 就是访问伪装站:80。 208 | 209 | 210 | ## linux上服务端部署 211 | 212 | ```shell 213 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server 214 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo-server.json 215 | 216 | chmod +x sogo-server 217 | mv -f sogo-server /usr/local/bin/ 218 | mv -f sogo-server.json /usr/local/bin/ 219 | kill -9 $(lsof -i:80|tail -1|awk '$1!=""{print $2}') #关闭80端口应用 220 | ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接 221 | (sogo-server &) 222 | ``` 223 | 224 | ## linux上客户端安装 225 | 226 | ```shell 227 | # 国内机器下面两个wget会很慢,考虑本地下载再上传到服务器吧 228 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo.json 229 | wget https://github.com/arloor/sogo/releases/download/v1.0/sogo 230 | 231 | chmod +x sogo 232 | mv -f sogo /usr/local/bin/ 233 | mv -f sogo.json /usr/local/bin/ 234 | ulimit -n 65536 #设置进程最多打开文件数量,防止 too many openfiles错误(太多连接 235 | # 运行前,先修改/usr/local/bin/sogo.json 236 | (sogo &) #以 /usr/local/bin/sogo.json 为配置文件 该配置下,服务端地址被设置为proxy 237 | #(sogo -c path &) #以path指向的文件为配置文件 238 | ``` 239 | 240 | ## windows客户端安装 241 | 242 | 到[Release](https://github.com/arloor/sogo/releases/tag/v1.0)下载`sogo.exe`和`sogo.json`。 243 | 244 | sogo.json内容如下: 245 | 246 | ```json 247 | { 248 | "ClientPort": 8888, 249 | "Use": 0, 250 | "Servers": [ 251 | { 252 | "ProxyAddr": "proxy", 253 | "ProxyPort": 80, 254 | "UserName": "a", 255 | "Password": "b" 256 | } 257 | ], 258 | "Dev":false 259 | } 260 | ``` 261 | 先修改`ProxyAddr`为服务端安装的地址即可。其他配置项是高级功能,例如多服务器管理,多用户管理(用户认证)等等。 262 | 263 | >shadowsocks是没有多用户管理的,ss每个端口对应一个用户。sogo则使用用户名+密码认证,使多个用户使用同一个服务器端口。 264 | 265 | 修改好之后,双击`sogo.exe`,这时会发现该目录下多了一个 sogo_8888.log 的文件,这就说明,在本地的8888端口启动好了这个sock5代理。(没有界面哦。 266 | 267 | ## 结束 268 | 269 | 这篇博客梳理了一下sogo的实现原理,总之,sogo是一个优雅的代理。sogo代码不多,对go语言、网络编程感兴趣的人可以看看。 270 | 271 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/arloor/sogo/utils" 8 | "log" 9 | "os" 10 | "strconv" 11 | "sync" 12 | ) 13 | 14 | var configFilePath string = utils.GetWorkDir() + "sogo.json" //绝对路径或相对路径 15 | 16 | var localAddr string 17 | var proxyAddr string 18 | var authorization string 19 | var prefix1 string = "POST /target?at=" 20 | var prefix2 string 21 | var prefix3 = "\r\n\r\n" 22 | 23 | //prefix= prefix1+target+prefix2+length+prefix 24 | 25 | var pool = &sync.Pool{ 26 | New: func() interface{} { 27 | log.Println("new 1") 28 | return make([]byte, 9192) 29 | }, 30 | } 31 | 32 | var pool2 = &sync.Pool{ 33 | New: func() interface{} { 34 | log.Println("new 22222222") 35 | return make([]byte, 9192) 36 | }, 37 | } 38 | 39 | const fakeHost string = "qtgwuehaoisdhuaishdaisuhdasiuhlassjd.com" 40 | 41 | func printUsage() { 42 | fmt.Println("运行方式: sogo [-c configFilePath ] 若不使用 -c指定配置文件,则默认使用" + configFilePath) 43 | } 44 | 45 | func init() { 46 | 47 | printUsage() 48 | 49 | if len(os.Args) == 3 && os.Args[1] == "-c" { 50 | configFilePath = os.Args[2] 51 | } 52 | 53 | log.SetOutput(os.Stdout) 54 | log.SetFlags(log.Lshortfile | log.Flags()) 55 | configinit() 56 | log.Println("配置信息为:", Config) 57 | server := Config.Servers[Config.Use] 58 | setServerConfig(server) 59 | if !Config.Dev { 60 | log.Println("已启动sogo客户端,请在sogo_" + strconv.Itoa(Config.ClientPort) + ".log查看详细日志") 61 | f, _ := os.OpenFile("sogo_"+strconv.Itoa(Config.ClientPort)+".log", os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0755) 62 | log.SetOutput(f) 63 | } 64 | 65 | localAddr = ":" + strconv.Itoa(Config.ClientPort) 66 | 67 | } 68 | 69 | //设置三个参数 70 | func setServerConfig(server Server) { 71 | proxyAddr = server.ProxyAddr + ":" + strconv.Itoa(server.ProxyPort) 72 | authorization = base64.StdEncoding.EncodeToString([]byte(server.UserName + ":" + server.Password)) 73 | prefix2 = " HTTP/1.1\r\nHost: " + fakeHost + "\r\nAuthorization: Basic " + authorization + "\r\nAccept: */*\r\nContent-Type: text/plain\r\naccept-encoding: gzip, deflate\r\ncontent-length: " 74 | log.Println("服务器配置:", proxyAddr, "认证信息:", "Basic "+authorization) 75 | } 76 | 77 | type Server struct { 78 | ProxyAddr string 79 | ProxyPort int 80 | UserName string 81 | Password string 82 | } 83 | 84 | type Info struct { 85 | Dev bool 86 | ClientPort int 87 | Use int 88 | Servers []Server //8081,请不要修改 89 | } 90 | 91 | var Config = Info{ 92 | true, 93 | 7777, 94 | 0, 95 | []Server{ 96 | Server{"proxy", 80, "a", "b"}, 97 | }, 98 | } 99 | 100 | func (configInfo Info) String() string { 101 | str, _ := configInfo.ToJSONString() 102 | return str 103 | } 104 | 105 | //implement JSONObject 106 | func (configInfo Info) ToJSONString() (str string, error error) { 107 | b, err := json.Marshal(configInfo) 108 | if err != nil { 109 | return "", err 110 | } else { 111 | return string(b), nil 112 | } 113 | } 114 | 115 | func configinit() { 116 | configFile, err := os.Open(configFilePath) 117 | defer configFile.Close() 118 | if err != nil { 119 | log.Println("Error", "打开"+configFilePath+"失败,使用默认配置", err) 120 | return 121 | } 122 | bufSize := 1024 123 | buf := make([]byte, bufSize) 124 | for { 125 | total := 0 126 | n, err := configFile.Read(buf) 127 | total += n 128 | if err != nil { 129 | log.Println("Error", "读取"+configFilePath+"失败,使用默认配置", err) 130 | return 131 | } else if n < bufSize { 132 | log.Println("OK", "读取"+configFilePath+"成功") 133 | buf = buf[:total] 134 | break 135 | } 136 | 137 | } 138 | err = json.Unmarshal(buf, &Config) 139 | if err != nil { 140 | log.Println("Error", "读取"+configFilePath+"失败,使用默认配置", err) 141 | return 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "github.com/arloor/sogo/mio" 9 | "log" 10 | "net" 11 | "net/http" 12 | "strings" 13 | 14 | "strconv" 15 | ) 16 | 17 | var hand = []byte{0x05, 0x00} 18 | var ack = []byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10} 19 | 20 | func handler(w http.ResponseWriter, r *http.Request) { 21 | //写回请求体本身 22 | bufio.NewReader(r.Body).WriteTo(w) 23 | 24 | } 25 | func server8080() { 26 | http.HandleFunc("/", handler) 27 | if err := http.ListenAndServe(":8080", nil); err != nil { 28 | log.Println("serve过程中出错", err) 29 | } 30 | } 31 | 32 | func main() { 33 | //go server8080() 34 | 35 | //================== 36 | 37 | ln, err := net.Listen("tcp", localAddr) 38 | if err != nil { 39 | fmt.Println("监听", localAddr, "失败 ", err) 40 | return 41 | } 42 | defer ln.Close() 43 | fmt.Println("成功监听 ", ln.Addr()) 44 | for { 45 | c, err := ln.Accept() 46 | if err != nil { 47 | log.Println("接受连接失败 ", err) 48 | } else { 49 | go handleClientConnnection(c) 50 | } 51 | } 52 | } 53 | 54 | func ban(addr string) bool { 55 | if strings.Contains(addr, "imap") { 56 | return true 57 | } 58 | return false 59 | } 60 | 61 | func handleClientConnnection(clientCon net.Conn) { 62 | defer clientCon.Close() 63 | handshakeErr := handshake(clientCon) 64 | if handshakeErr != nil { 65 | log.Println("handshakeErr ", handshakeErr) 66 | return 67 | } else { 68 | log.Println("与客户端握手成功") 69 | addr, getTargetErr := getTargetAddr(clientCon) 70 | if getTargetErr != nil { 71 | log.Println("getTargetErr ", getTargetErr) 72 | return 73 | } else if ban(addr) { 74 | log.Println("ban", addr) 75 | return 76 | } else { 77 | //开始连接到服务器,并传输 78 | var serverConn, dialErr = net.Dial("tcp", proxyAddr) 79 | //var serverConn, dialErr = net.Dial("tcp", addr) 80 | if dialErr != nil { 81 | log.Println("dialErr ", dialErr) 82 | clientCon.Close() 83 | return 84 | } 85 | go handleServerConn(serverConn, clientCon) 86 | 87 | var buf = pool.Get().([]byte) 88 | defer pool.Put(buf) 89 | for { 90 | 91 | num, readErr := clientCon.Read(buf[1000:]) 92 | if readErr != nil { 93 | log.Print("readErr ", readErr, clientCon.RemoteAddr()) 94 | clientCon.Close() 95 | serverConn.Close() 96 | return 97 | } 98 | writeErr := mio.WriteAll(serverConn, mio.AppendHttpRequestPrefix(buf[:num+1000], addr, prefix1, prefix2, prefix3)) 99 | //writeErr := mio.WriteAll(serverConn, buf[:num]) 100 | if writeErr != nil { 101 | log.Print("writeErr ", writeErr) 102 | clientCon.Close() 103 | serverConn.Close() 104 | return 105 | } 106 | buf = buf[0:] 107 | } 108 | } 109 | } 110 | 111 | } 112 | 113 | //HTTP/1.1 200 OK 114 | //Content-Type: text/plain; charset=utf-8 115 | //Content-Length: 181 116 | // 117 | //HTTP/1.1 304 Not Modified 118 | //Date: Tue, 09 Apr 2019 08:46:15 GMT 119 | //Server: Apache/2.4.6 (CentOS) 120 | //Connection: Keep-Alive 121 | //Keep-Alive: timeout=5, max=100 122 | //ETag: "37cb-58613717be980" 123 | 124 | func handleServerConn(serverConn, clientCon net.Conn) { 125 | defer serverConn.Close() 126 | readBuf := pool2.Get().([]byte) 127 | payload := pool2.Get().([]byte)[:0] 128 | prefixBuf := pool2.Get().([]byte)[:0] 129 | redundencyBuf := pool2.Get().([]byte)[:0] 130 | defer func() { 131 | pool2.Put(readBuf) 132 | pool2.Put(payload) 133 | pool2.Put(prefixBuf) 134 | pool2.Put(redundencyBuf) 135 | }() 136 | 137 | state := "preflix" 138 | contentlength := -1 139 | for { 140 | var buf []byte = nil //待处理的字符 141 | if len(redundencyBuf) != 0 { 142 | buf = redundencyBuf 143 | redundencyBuf = redundencyBuf[:0] 144 | } else { 145 | readNum, readErr := serverConn.Read(readBuf) 146 | if readErr != nil { 147 | log.Println("readErr ", readErr) 148 | clientCon.Close() 149 | break 150 | } 151 | buf = readBuf[:readNum] 152 | } 153 | 154 | switch state { 155 | case "preflix": 156 | //获取响应的prefix 157 | //HTTP/1.1 200 OK 158 | //Content-Type: text/plain; charset=utf-8 159 | //Content-Length: 3717 160 | for i := 0; i+4 <= len(buf); i++ { //0 1 2 3 4 5 161 | if buf[i] == '\r' && buf[i+2] == '\r' && buf[i+1] == '\n' && buf[i+3] == '\n' { 162 | prefixBuf = append(prefixBuf, buf[:i]...) 163 | state = "payload" 164 | if i+4 < len(buf) { 165 | redundencyBuf = append(redundencyBuf, buf[i+4:]...) 166 | } 167 | break 168 | } 169 | } 170 | if state == "payload" { 171 | //分析头部,获取响应头的contentlength 172 | headrs := strings.Split(string(prefixBuf), "\r\n") 173 | requestline := headrs[0] 174 | parts := strings.Split(requestline, " ") 175 | if len(parts) < 3 { 176 | fmt.Println(requestline) 177 | log.Println(errors.New("不是以HTTP/1.1 200 OK这种开头,说明上个响应有问题。")) 178 | clientCon.Close() 179 | return 180 | } 181 | //version := parts[0] 182 | //code := parts[1] 183 | //msg := parts[2] 184 | var headmap = make(map[string]string) 185 | for i := 1; i < len(headrs); i++ { 186 | headsplit := strings.Split(headrs[i], ": ") 187 | if len(headsplit) == 2 { 188 | headmap[headsplit[0]] = headsplit[1] 189 | } 190 | } 191 | if headmap["Content-Length"] == "" { 192 | contentlength = 0 193 | } else { 194 | contentlength, _ = strconv.Atoi(headmap["Content-Length"]) 195 | } 196 | //log.Println(contentlength) 197 | } else if state == "preflix" { 198 | prefixBuf = append(prefixBuf, buf[:len(buf)]...) 199 | } 200 | case "payload": 201 | toAppend := contentlength - len(payload) 202 | hasAll := true 203 | if toAppend > len(buf) { 204 | hasAll = false 205 | } 206 | if hasAll { 207 | payload = append(payload, buf[:toAppend]...) 208 | state = "ready" 209 | if len(buf) > toAppend { 210 | redundencyBuf = append(redundencyBuf, buf[toAppend:]...) 211 | } 212 | 213 | //下面开始传输payload 214 | mio.Simple(&payload, len(payload)) 215 | mio.WriteAll(clientCon, payload) 216 | payload = payload[:0] 217 | state = "preflix" 218 | } else { 219 | payload = append(payload, buf[:len(buf)]...) 220 | } 221 | 222 | default: 223 | 224 | } 225 | } 226 | } 227 | 228 | //func handleServerConn(serverConn, clientCon net.Conn) { 229 | // defer serverConn.Close() 230 | // redundancy := make([]byte, 0) 231 | // for { 232 | // redundancyRetain, readerr := read(serverConn, clientCon, redundancy) 233 | // redundancy = redundancyRetain 234 | // if readerr != nil { 235 | // log.Println("readerr", readerr) 236 | // break 237 | // } 238 | // } 239 | //} 240 | 241 | func read(serverConn, clientConn net.Conn, redundancy []byte) (redundancyRetain []byte, readErr error) { 242 | buf := pool2.Get().([]byte) 243 | 244 | num := 0 245 | contentlength := -1 246 | prefixAll := false 247 | prefix := pool2.Get().([]byte)[0:0] 248 | //redundancy:=make([]byte,0) 249 | payload := pool2.Get().([]byte)[0:0] 250 | 251 | defer func() { 252 | pool2.Put(buf) 253 | pool2.Put(prefix[:cap(prefix)]) 254 | pool2.Put(payload[:cap(payload)]) 255 | }() 256 | 257 | for { 258 | if len(redundancy) != 0 { 259 | buf = redundancy 260 | num = len(redundancy) 261 | redundancy = redundancy[0:0] 262 | } else { 263 | num, readErr = serverConn.Read(buf) 264 | if readErr != nil { 265 | return redundancy, readErr 266 | } 267 | } 268 | 269 | if num <= 0 { 270 | return nil, errors.New("读到<=0字节,未预期地情况") 271 | } else { 272 | if !prefixAll { //追加到前缀 273 | prefix = append(prefix, buf[:num]...) 274 | //todo 275 | // string(prefix)需要优化,只是为了找到\r\n\r\n别这样,别转成string 276 | if index := strings.Index(string(prefix), "\r\n\r\n"); index >= 0 { 277 | if index+4 < len(prefix) { 278 | payload = append(payload, prefix[index+4:]...) 279 | } 280 | prefix = prefix[:index] 281 | prefixAll = true 282 | //分析头部 283 | headrs := strings.Split(string(prefix), "\r\n") 284 | 285 | requestline := headrs[0] 286 | parts := strings.Split(requestline, " ") 287 | if len(parts) < 3 { 288 | fmt.Println(requestline) 289 | return nil, errors.New("不是以HTTP/1.1 200 OK这种开头,说明上个响应有问题。") 290 | } 291 | //version := parts[0] 292 | //code := parts[1] 293 | //msg := parts[2] 294 | 295 | var headmap = make(map[string]string) 296 | for i := 1; i < len(headrs); i++ { 297 | headsplit := strings.Split(headrs[i], ": ") 298 | headmap[headsplit[0]] = headsplit[1] 299 | } 300 | if headmap["Content-Length"] == "" { 301 | contentlength = 0 302 | } else { 303 | contentlength, _ = strconv.Atoi(headmap["Content-Length"]) 304 | } 305 | } 306 | } else { //追加到payload 307 | payload = append(payload, buf[:num]...) 308 | } 309 | } 310 | buf = buf[0:] 311 | if contentlength != -1 && contentlength < len(payload) { //这说明读多了,要把多的放到redundancy 312 | redundancy = append(redundancy, payload[contentlength:]...) 313 | payload = payload[:contentlength] 314 | } 315 | if contentlength == len(payload) { 316 | //写会 317 | mio.Simple(&payload, len(payload)) 318 | writeErr := mio.WriteAll(clientConn, payload) 319 | if writeErr != nil { 320 | clientConn.Close() 321 | } 322 | return redundancy, writeErr 323 | } 324 | } 325 | } 326 | 327 | func getTargetAddr(clientCon net.Conn) (string, error) { 328 | var buf = pool.Get().([]byte) 329 | defer pool.Put(buf) 330 | numRead, err := clientCon.Read(buf) 331 | if err != nil { 332 | return "", err 333 | } else if numRead > 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 { 334 | if buf[3] == 3 { 335 | 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]])))) 336 | writeErr := mio.WriteAll(clientCon, ack) 337 | return string(buf[5:5+buf[4]]) + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[5+buf[4]:7+buf[4]]))), writeErr 338 | } else if buf[3] == 1 { 339 | 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])))) 340 | writeErr := mio.WriteAll(clientCon, ack) 341 | return net.IPv4(buf[4], buf[5], buf[6], buf[7]).String() + ":" + strconv.Itoa(int(binary.BigEndian.Uint16(buf[8:10]))), writeErr 342 | } else { 343 | return "", errors.New("不能处理ipv6") 344 | } 345 | 346 | } else { 347 | return "", errors.New("不能处理非CONNECT请求") 348 | } 349 | } 350 | 351 | //读 5 1 0 写回 5 0 352 | func handshake(clientCon net.Conn) error { 353 | //var buf = make([]byte,100) 354 | var buf = pool.Get().([]byte) 355 | defer pool.Put(buf) 356 | numRead, err := clientCon.Read(buf) 357 | if err != nil { 358 | return err 359 | } else if numRead == 3 && buf[0] == 0X05 && buf[1] == 0X01 && buf[2] == 0X00 { 360 | return mio.WriteAll(clientCon, hand) 361 | } else { 362 | log.Printf("%d", buf[:numRead]) 363 | return mio.WriteAll(clientCon, hand) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /mio/io.go: -------------------------------------------------------------------------------- 1 | package mio 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func WriteAll(conn net.Conn, buf []byte) error { 8 | //log.Print("写会浏览器") 9 | for writtenNum := 0; writtenNum != len(buf); { 10 | tempNum, err := conn.Write(buf[writtenNum:]) 11 | if err != nil { 12 | return err 13 | } 14 | writtenNum += tempNum 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /mio/prefix.go: -------------------------------------------------------------------------------- 1 | package mio 2 | 3 | import ( 4 | "encoding/base64" 5 | "strconv" 6 | ) 7 | 8 | //POST / HTTP/1.1 9 | //cache-control: no-cache 10 | //Postman-Token: 6b859da0-0a7e-4c4e-a0e2-2544aeef732f 11 | //Content-Type: text/plain 12 | //User-Agent: PostmanRuntime/7.6.1 13 | //Accept: */* 14 | //Host: localhost:8080 15 | //accept-encoding: gzip, deflate 16 | //content-length: 5 17 | //Connection: keep-alive 18 | // 19 | //aaaaa 20 | // 21 | //HTTP/1.1 200 OK 22 | //Date: Tue, 09 Apr 2019 03:26:07 GMT 23 | //Content-Length: 5 24 | //Content-Type: text/plain; charset=utf-8 25 | //Connection: keep-alive 26 | // 27 | //aaaaa 28 | 29 | func AppendHttpRequestPrefix(buf []byte, targetAddr string, prefix1 string, prefix2 string, prefix3 string) []byte { 30 | content := buf[1000:] 31 | prefix := buf[:1000] 32 | Simple(&content, len(content)) 33 | contentLength := strconv.Itoa(len(content)) 34 | // 演示base64编码 35 | addrBase64 := base64.NewEncoding("abcdefghijpqrzABCKLMNOkDEFGHIJl345678mnoPQRSTUVstuvwxyWXYZ0129+/").EncodeToString([]byte(targetAddr)) 36 | prefixEnd := 1000 37 | prefixLength := len(prefix1) + len(addrBase64) + len(prefix2) + len(contentLength) + len(prefix3) 38 | prefixStart := prefixEnd - prefixLength 39 | prefixIndex := prefixStart 40 | for _, x := range prefix1 { 41 | prefix[prefixIndex] = byte(x) 42 | prefixIndex++ 43 | } 44 | for _, x := range addrBase64 { 45 | prefix[prefixIndex] = byte(x) 46 | prefixIndex++ 47 | } 48 | for _, x := range prefix2 { 49 | prefix[prefixIndex] = byte(x) 50 | prefixIndex++ 51 | } 52 | for _, x := range contentLength { 53 | prefix[prefixIndex] = byte(x) 54 | prefixIndex++ 55 | } 56 | for _, x := range prefix3 { 57 | prefix[prefixIndex] = byte(x) 58 | prefixIndex++ 59 | } 60 | buf = buf[prefixStart:] 61 | //println(len(buf)) lenbuf就是实际上传字节数 62 | return buf 63 | } 64 | 65 | //取反 66 | func Simple(bufPtr *[]byte, num int) { 67 | buf := *bufPtr 68 | for i := 0; i < num; i++ { 69 | buf[i] = ^buf[i] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /sogo.json: -------------------------------------------------------------------------------- 1 | { 2 | "ClientPort": 8888, 3 | "Use": 0, 4 | "Servers": [ 5 | { 6 | "ProxyAddr": "proxy", 7 | "ProxyPort": 80, 8 | "UserName": "a", 9 | "Password": "b" 10 | } 11 | ], 12 | "Dev":true 13 | } -------------------------------------------------------------------------------- /utils/tool.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func GetWorkDir() string { 11 | file, err := exec.LookPath(os.Args[0]) 12 | if err != nil { 13 | // 14 | return "" 15 | } 16 | path, err := filepath.Abs(file) 17 | if err != nil { 18 | return "" 19 | } 20 | i := strings.LastIndex(path, "/") 21 | if i < 0 { 22 | i = strings.LastIndex(path, "\\") 23 | } 24 | if i < 0 { 25 | return "" 26 | } 27 | return string(path[:i+1]) 28 | } 29 | --------------------------------------------------------------------------------