这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
写好一个代理服务器,先要了解它的运行过程和逻辑,简单来说,就是在我们本身和浏览器端(客户端)进行连接的过程中插入了一个中间商。
用到的常量设置
const (
socks5ver = 0x05
cmdBind = 0x01
atypeIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
主函数
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)
}
}
goroutine函数
func process(conn net.Conn) (err error) {
defer conn.Close()
reader := bufio.NewReader(conn)
err = auth(reader, conn)
if err != nil {
println("auth_err", err)
return err
}
err = connect(reader, conn)
if err != nil {
println("connect错误", err)
return err
}
return nil
}
我们主要需要两个函数来实现代理服务器的功能,这两个函数分别对应了代理服务器的两个过程,即协商过程和请求过程。
- auth函数-协商过程
- connect函数-连接过程
协商过程
协商过程简述:协商过程浏览器端会发送三个字段,我们响应两个字段回去,就完成了一个协商过程,然后进入连接请求过程。
协商具体过程和实现: 浏览器端发来的三个字段具体是VER,NMETHODS,METHODS
` +----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
` ver字段一个字节,nmethods一个字节,而methods是不定长度,由读出来的nmethods的值决定。
iver, err := reader.ReadByte()
if err != nil {
println("协议版本读取err:", err)
return
}
if iver != socks5ver {
println("协议版本错误", iver, "不是socks5")
return
}
nmethods, err := reader.ReadByte()
if err != nil {
println("方法数量读取err", err)
return
}
buf := make([]byte, nmethods)
_, err = io.ReadFull(reader, buf)
if err != nil {
println("读取方法出错", err)
return
}
我们只需要在函数里根据长度读取字节,确定好协议版本和认证连接方式(用户密码认证,无需认证等)这个项目中,则是使用的socks5协议(不然怎么叫socks5代理服务器项目嘞)和无需认证的方式。
tcp连接方式要有来有回,所以我们应该写回我们选择的协议版本和认证方式
_, err = conn.Write([]byte{socks5ver, 0x00})
if err != nil {
println("协商返回出错", err)
return
}
连接请求
这是个稍微复杂点的过程。我们需要接收6个字段,然后返回6个字段。整个连接请求过程可以分为两个阶段
- 处理请求
- 返回确认信息
处理请求
处理请求的方式也协商阶段类似,我们先了解字段的长度种类,然后读取。
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
注意,这里的RSV是一个保留字段,不需要理会和处理。 因此我们提前index为0,1,3的字段。
字段解释:ver是协议版本(和协商阶段一致),cmd是连接请求字段,ATYP是地址类型字段(通过它确定发来的地址是ipv4地址还是HOST地址还是其他),DST.ADDR是地址字段,存放地址,DST.PORT是端口号字段。
具体代码处理实现如下
//前面4个字节分别是iver,cmd(连接请求),RSV(一个保留的量),ATYP(地址类型)
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
println("前4个字节读取错误", err)
return err
}
iver, cmd, atye := buf[0], buf[1], buf[3]
if iver != socks5ver {
println("协议版本不是socks5")
return err
}
if cmd != cmdBind {
println("请求连接错误")
return err
}
var addr string
switch atye {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
println("ipv4地址读取错误", err)
return err
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
println("ipv4地址为", addr)
case atypeHOST:
address_size, err := reader.ReadByte()
if err != nil {
println("host地址读取错误", err)
return err
}
buf2 := make([]byte, address_size)
io.ReadFull(reader, buf2)
addr = string(buf2)
println("host地址为", addr)
case atypeIPV6:
println("暂时不支持ipv6")
default:
println("地址类型不支持")
}
在上述代码中,处理atyp字段的过程中也紧接着获取到了第五个字段,也就是对应的地址,并存到了addr变量中。
最后,再把固定长度为2bytes的端口号字段读取出来
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
println("读取端口号出错", err)
return err
}
port := binary.BigEndian.Uint16(buf[:2])
println("读取到的端口号为", port)
注意这里要将port作二进制的大端序列处理
返回确认信息
字段处理完了,不妨回顾一下,在刚刚的过程中,我们确认了前三个字段信息没有错误,然后获取了地址和端口号,即我们已经获得了一个完整的可以用于拨号的地址,那么为了建立双向的连接,我们要写回我们的确认连接,同时Dial回去,在建立起双向的数据流传输,整个代理服务器的功能就实现了。
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
println("dial拨号出错", err)
return err
}
defer dest.Close()
//这个conn的write很重要,
//如果不写就导致我们没有返回确认,则无法联网
_, err = conn.Write([]byte{socks5ver, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
最后,利用context库,使得函数不会立即退出,让io.copy(这个函数可以使得字节流单向复制,所以用两个io.copy来实现双向数据传输)可以一直执行,直到有一方出错,才停止,然后退出函数。
ctx, cancel := context.WithCancel(context.Background())
go func() {
io.Copy(dest, reader)
cancel()
}()
go func() {
io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
最后的最后,我们需要调试写好的代理服务器。 我们需要用到浏览器插件Proxy SwitchyOmega,启用这个插件后,设置好地址端口号,在我们的程序中去监听对应的地址端口号,然后就可以看到我们的代理服务器的作用了。 调试的结果如图
我们可以看出,所有的网络流量都通过了这个代理服务器,再来到我们的机器,同理,我们返回的数据也会经过代理服务器再发送到浏览器端。