部署SOCKS5代理服务器
尝试一些更复杂的实现
- 对于不熟悉的函数,善用官方文档,这是你了解函数最正规最详细的地方
SOCKS5 简介
SOCKS5 是一个传输层代理协议,主要用于通过中间代理服务器来转发客户端和目标 服务器之间的网络通信。它并不关心数据的具体内容,只负责将数据从一个网络位置转发到另一个网络位置。与 HTTP 代理相比,SOCKS5 可以处理任何协议的流量,包括 HTTP、FTP、SMTP、POP3 等。
从简单的TCP echo server做起
- 由于整个协议的实现较为复杂,先尝试一个简单版的
- 这次的测试需要用到nc命令,所以我们先配置好环境
- 在官方网站下载netcat
- 将包含netcat.exe的文件夹添加到系统变量Path中
- 这里使用GoLand的powershell终端,因此还需要再手动添加路径
用你的路径替换掉path\to\nc$env:PATH += ";C:\path\to\nc" - 代码如下
- 行9-22 定义函数process处理客户端conn发来的数据
- 行10 调用结束后关闭连接防止数据泄露
- 行12-20 创建死循环持续读取数据
- 行17 将读取到的数据用Write函数发送回客户端(需要用[]byte{}切片形式)
- 行25 用net.Listen启动tcp服务器,并用server监听127.0.0.1:1080
- 行30 将客户端连接放至client
- 行35 用go关键字创建一个goroutine并行执行函数
- 行9-22 定义函数process处理客户端conn发来的数据
- 运行代码,
运行成功
实现认证阶段
-
协议的第一步:处理客户端与代理服务器的认证
-
首先声明一些常量,这些常量和我们的协议有关,
- 行11 socks5Ver存储协议版本号0x05
- 行12 cmdBind存储一个SOCKS5请求命令0x01代表“Bind”
- 行13-15 表示IP地址类型IPv40x01,主机0x03,IPv60x04
-
我们不再需要之前的死循环,而是改用一个auth函数处理输入,改写后的process如下,
- 行20 调用auth函数处理conn
- 行21 如果返回错误,打印日志,**conn.RemoteAddr()**返回当前连接的地址
- 看到26行的:bulb:了吗,像不像现在的你头上冒出来的那个?
-
然后是重头戏auth,首先我们要明确SOCKS5采用固定顺序协议,服务器会按序发送VER,NMETHODS,METHODS三个字段,
-
行39 读取第一个字节即VER
-
行46 读取第二个字节NMETHODS
-
行50 根据NMETHODS给出的字节数创造一个对应大小的[]byte切片
-
行51 读入METHODS直至填满method
-
-
按照协议,我们需要返回我们使用的VER版本和认证形式0x00即不需要认证
- 行61 将VER和0x00写回客户端
-
我们可以试着运行一下程序,注意在powershell中curl要使用curl.exe确保调用系统命令
-
运行毫无疑问会出问题,毕竟我们离完成还有很长的距离,
-
不过也应该能看到auth函数的运行是没有问题的,可喜可贺
实现请求阶段
- 协议的第二步:读取客户端的请求,解析出目标服务器的地址,端口等并尝试建立连接
- 编写connect函数,先写出解析客户端请求报文的部分
- 回复报文的格式已经给出不再赘述。这次换一种读取的方法不再读取per byte而是创建一个四个字节的切片buf
- 行102 ReadFull读取直至切片被填满
- 行106 从前四个字节中切出VER,CMD,ATYP用于检验
- 处理目标地址类型,
- 行119-124 IPV4情况,会返回固定四个字节的长度,直接使用buf读取,把IP地址打印到addr里去
- 行125-135 域名情况,先返回域名长度再给出地址,与先前的METHOD类似
- 行136-137 偷个懒,IPV6不常见
- 处理端口数据,
- 行141 死马当牛马用,用切片方式再次填充buf的前两个字节
- 行145 按^大端字节序(SOCKS5要求)解析字节切片为无符号十六位整数作为端口号填入port
- 按照协议代理服务器需要返回一个响应报文,
- 必要的返回并没有多少,基本都写在注释里了:thumbsup:
- 同样在process中添加对connect的调用,
- 这样基本就完成了请求部分,离成为王仍需一步
实现relay
- 和代理服务器建立TCP连接,双向转换数据
- 让我们回到connect中去,
- 行147 用net包的Dial函数建立连接
- 行151 函数调用结束后关闭连接
- 然后实现数据交换,这里使用的是io库的Copy函数,这个函数会从一个只读reader里拷贝数据到可写writer中
- 行172-179 并行执行两个goroutine,双向转换数据
- 由于goroutine迅速执行,若不采取措施函数将很快返回nil,而我们需要任何一方不在发送数据后结束,这里的解决方案是context包的WithCancel函数,这个函数创建一个ctx上下文,直到一个cancel被执行时结束所有内部的goroutine
- 行181 阻塞外层函数的进行,直到ctx结束再返回nil
- 行172-179 并行执行两个goroutine,双向转换数据
- 让我们试着curl一下,
- 这样就完成了
- 想要在Chrome浏览器玩的话需要先安装一个SwitchyOmega插件,
- 选择新建情景模式然后填入服务器属性,在插件里选择应用,这样你访问的新界面就会通过你完成的SOCKS5代理服务器
- 妈妈再也不用担心我的IP地址暴露啦
由于这次的代码还挺长的,在下方给出完整代码供参考
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)
const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed: %v", conn.RemoteAddr(), err)
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed: %v", conn.RemoteAddr(), err)
return
}
}
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// +-----+----------+----------+
// | VER | NMETHODS | METHODS |
// +-----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +-----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数
//METHODS: 对应NMETHODS,为METHODS的字节数。一些RFC预定义如下
// X'00' NO AUTHENTICATION REQUIRED
// 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)
// +-----+--------+
// | VER | METHOD |
// +-----+--------+
// | 1 | 1 |
// +-----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return nil
}
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)
}
}
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 {
return fmt.Errorf("not supported cmd:%v", cmd)
}
addr := ""
switch atyp {
case atypeIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
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])
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst 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})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
*今日推荐:好累不想推荐