Go语言基础-Proxy实现

本文最后更新于:6 months ago

Go语言基础-Proxy的实现

一、前置知识:SOCKS5代理

SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是“SOCKet Secure”的缩写。

某些企业为了安全性,设置了很强的防火墙,带来的副作用是哪怕你是管理员,访问资源也很麻烦。SOCKS5就是开了一个小口子,用来访问资源的。

防火墙内的客户端想要访问外部的服务器时,会跟SOCKS代理服务器进行连接。客户端的请求会通过代理服务器发送到外部的服务器,最终响应也会通过代理服务器发送回客户端。

基本模型

image-20220514220424129

协商过程(认证)

SOCKS5比SOCKS4a多了验证、IPv6、UDP支持。

建立与SOCKS5服务器的TCP连接后客户端需要先发送请求来确认协议版本及认证方式,格式为(以字节为单位):

VER NMETHODS METHODS
1 1 1-255
  • VER是SOCKS版本,这里应该是0x05;
  • NMETHODS是METHODS部分的长度;
  • METHODS是客户端支持的认证方式列表,每个方法占1字节。基本的有:
    • 0x00 不需要认证
    • 0x01 GSSAPI
    • 0x02 用户名、密码认证

服务器从客户端提供的方法中选择一个并通过以下消息通知客户端(以字节为单位):

VER METHOD
1 1
  • VER是SOCKS版本,这里应该是0x05;
  • METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,客户端需要关闭连接。

请求阶段

客户端发送请求的格式

认证结束后客户端就可以发送请求信息。如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装。

SOCKS5请求格式(以字节为单位):

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 动态 2
  • VER是SOCKS版本,这里应该是0x05;
  • CMD是SOCK的命令码
    • 0x01表示CONNECT请求
    • 0x02表示BIND请求
    • 0x03表示UDP转发
  • RSV 0x00,保留
  • ATYP DST.ADDR类型
    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
    • 0x04 IPv6地址,16个字节长度。
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

服务器响应的格式

服务器按以下格式回应客户端的请求(以字节为单位):

VER REP RSV ATYP BND.ADDR BND.PORT
1 1 0x00 1 动态 2
  • VER是SOCKS版本,这里应该是0x05;
  • REP应答字段

    • 0x00表示成功
    • 0x01普通SOCKS服务器连接失败
    • 0x02现有规则不允许连接
    • 0x03网络不可达
    • 0x04主机不可达
    • 0x05连接被拒
    • 0x06 TTL超时
    • 0x07不支持的命令
    • 0x08不支持的地址类型
    • 0x09 - 0xFF未定义
  • RSV 0x00,保留

  • ATYP BND.ADDR类型

    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
    • 0x04 IPv6地址,16个字节长度。
  • BND.ADDR 服务器绑定的地址

  • BND.PORT 网络字节序表示的服务器绑定的端口

二、前置知识:bufio包的使用

bufio是有缓存的io,避免因为多次写操作而降低程序的性能。

具体可以参考这个文章:https://zhuanlan.zhihu.com/p/129781512

有时间,再自己总结吧…

三、代码实现

1. v1:简易的echo server

与linuxC++的实现过程类似。

  • 首先监听IP地址和端口号,调用net.Listen();
  • 调用server.Accept()等待并建立连接;
  • 当有连接建立之后,启动一个协程,来处理这个连接的数据
//process部分就不放出了,就是一个简单的数据读取和输出的过程
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)
   }
}

nc安装教程如下:https://www.cnblogs.com/linyufeng/p/13206252.html

使用nc命令可以直接和一个ip端口建立tcp连接。

image-20220514214450225

2. v2:协商阶段

main函数不需要修改,在process函数中新增鉴权的部分:

//新增的鉴权过程
err := auth(reader, conn)
if err != nil {
   log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
   return
}

auth函数的实现流程如下:

  1. 使用reader.ReadByte将版本号读取出来;
  2. 使用reader.ReadByte将mthodsize读取出来,并建立相应大小的method数组;
  3. 使用io.ReadFull将method填充完全;
  4. 往连接中写入版本号和方法conn.Write([]byte{socks5Ver, 0x00})

运行程序后,在终端执行curl --socks5 127.0.0.1:1080 -v http://www.qq.com,可以显示出版本号、method和认证成功。

image-20220514220635063

3. v3:请求阶段

我们在auth下面再实现一个connect函数,该函数用于读取客户端发送的数据,然后与服务器进行tcp连接;之后将连接的信息发送回给客户端。

err = connect(reader, conn)
if err != nil {
   log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
   return
}

读取客户端的数据主要包括版本号Ver、命令码CMD、保留字RSB、ATYP目标地址类型、DST.ADDR 一个可变长度的值、DST.PORT 目标端口。

写回给客户端的过程属性类似,略。

运行程序后,可以正确返回ip地址和端口号:

image-20220514220524235

image-20220514220512876

4. v4:relay阶段

将客户端的请求发送到服务器,将服务器的响应发送回给客户端。

//使用ctx机制来进行同步
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

//使用io.Copy——死循环将src中的数据读取到dest中
//两个goroutine的方向不一样,一个是从浏览器拷贝到服务器,一个是从服务器拷贝到浏览器
go func() {
    _, _ = io.Copy(dest, reader)
    cancel()
}()
go func() {
    _, _ = io.Copy(conn, dest)
    cancel()
}()

//使用ctx的机制,来进行同步一下
    <-ctx.Done()

运行程序如下所示:

image-20220514215014797

image-20220514220556547

5.使用插件进行测试

可以使用Proxy SwitchyOmega插件。

该软件本身不提供给代理服务器,需要连接我们自己的代理服务器。

image-20220514215111141image-20220514215047480

在启动右上角的小圈圈后,浏览器访问的网页都会经过我们的代理服务器。

image-20220514215030250

参考资料

[1] https://zhuanlan.zhihu.com/p/129781512

[2] https://zh.m.wikipedia.org/zh-hans/SOCKS

[3] 字节青训营《Go语言上手 - 基础语言》


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!