Socks5 代理|青训营笔记

89 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

Socks5代理

用途:

某些企业的内网为了确保安全性,有很严格的防火墙策略,副作用就是访问某些资源很麻烦,

socks5相当于在防火墙上开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。

在爬虫的时候,爬取过程中很容易会遇到IP访问频率超过限制,这个时候很多人就会去上网找一些代理IP池,这些代理IP池里的很多代理协议就是socks5。

版本1:

实现一个输入什么,输出什么的tcp连接程序

go 关键字:启动一个子线程子进程,开销很小,可以轻松处理上万的并发

版本2:

  • 接下来要开始实现协议的第一步,认证阶段。
  • 我们实现一个 auth 函数,在 process 函数里面调用。
  • 认证阶段的逻辑为,第一步,浏览器会给代理服务器发送一个包,这个包有三个字段。
  • 第一个字段,version 协议版本号,固定是5。第二个字段,methods 认证的方法数目。第三个字段,每个 method 的编码,0代表不需要认证,2代表用户名密码认证。
  • 先用 read bytes 来把版本号读出来,如果版本号不是 socket 5 的话直接返回报错,接下来再读取 method size ,也是一个字节。然后需要去 make 一个相应长度的一个 slice ,用 io.ReadFull 把它去填充进去。现在把获取到的版本号和认证方式打印一下。此时,代理服务器还需要返回一个 response,返回包括 两个字段,一个是 version ,一个是 method。
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
    //第一步,浏览器给代理服务器发送报文,下面三个部分构成
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED 选择的建成方式0x00,不需要认证
    // X’02’ USERNAME/PASSWORD
​
    ver, err := reader.ReadByte()
    if err != nil {
        return fmt.Errorf("read ver failed:%w", err)
    }
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    methodSize, err := reader.ReadByte()
    if err != nil {
        return fmt.Errorf("read methodSize failed:%w", err)
    }
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
        return fmt.Errorf("read method failed:%w", err)
    }
    log.Println("ver", ver, "method", method) //比fmt多了时间戳
    //返回给浏览器一个包
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

版本3

  • 接下来开始做第三步,实现请求阶段。
  • 试图读取到携带 URL 或者 IP 地址+端口的包,然后把它打印出来。实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。
  • 请求阶段,浏览器会发送一个包,包里面包含如下6个字段, version 版本号,command 请求的类型,RSV 保留字段,atype 就是目标地址类型,可能是 IPV 4 IPV 6 或者域名,addr 地址,port 端口号,然后需要逐个去读取这些字段。
  • 接下来把这个地址和端口打印出来用于调试,收到浏览器的这个请求包之后,需要返回一个包。
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
    // +----+-----+-------+------+----------+----------+
    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER 版本号,socks5的值为0x05
    // CMD 0x01表示CONNECT请求
    // RSV 保留字段,值为0x00
    // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    //   0x01表示IPv4地址,DST.ADDR为4个字节
    //   0x03表示域名,DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值
    // DST.PORT 目标端口,固定2个字节
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
        return fmt.Errorf("read header failed:%w", err)
    }
    ver, cmd, atyp := buf[0], buf[1], buf[3]
    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
​
    }
    addr := ""
    switch atyp {
    case atypIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
            return fmt.Errorf("read atyp falied:%w", err)
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3]) //sprintf写到左边的变量上去,而不是标准输出
    case atypeHOST:
        hostSize, err := reader.ReadByte()
        if err != nil {
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
            return fmt.Errorf("read host failed:%w", err)
        }
        addr = string(host)
    case atypeIPV6:
        return errors.New("IPV6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", err)
    }
    port := binary.BigEndian.Uint16(buf[:2]) //大端整型解析出来
    
    log.Println("dial", addr, port)
    // +----+-----+-------+------+----------+----------+
    // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER socks版本,这里为0x05
    // REP Relay field,内容取值如下 X’00’ succeeded
    // RSV 保留字段
    // ATYPE 地址类型
    // BND.ADDR 服务绑定的地址
    // BND.PORT 服务绑定的端口DST.PORT
    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) //IPV4的addr四个字节,端口的两个字节,都是0
    if err != nil {
        return fmt.Errorf("write failed: %w", err)
    }
    return nil
}

版本4

  • 直接用 net.dial 建立一个 TCP 连接,建立完连接之后,要加一个 defer 来关闭连接。
  • 接下来需要建立浏览器和下游服务器的双向数据转发。
  • 标准库的 io.copy 可以实现一个单向数据转发,双向转发需要启动两个 goroutinue。
  • 这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context,在最后等待 ctx.Done() ,只要 cancel 被调用, ctx.Done就会立刻返回,然后在上面的两个 goroutinue 里面 调用一次 cancel 即可。
port := binary.BigEndian.Uint16(buf[:2]) //大端整型解析出来
​
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port)) //建立tcp连接
if err != nil {
    return fmt.Errorf("dial dest failed:%w", err)
}
defer dest.Close()
​
log.Println("dial", addr, port)
// +----+-----+-------+------+----------+----------+
// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1  |  1  | X'00' |  1   | Variable |    2     |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X’00’ succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) //IPV4的addr四个字节,端口的两个字节,都是0
if err != nil {
    return fmt.Errorf("write failed: %w", err)
}
​
ctx, cancel := context.WithCancel(context.Background())
defer cancel() //回收cancel
go func() {
    _, _ = io.Copy(dest, reader)
    cancel()
}()
go func() {
    _, _ = io.Copy(conn, dest)
    cancel()
}()
<-ctx.Done()
return nil