Go基础语法之实战(socks5代理)| 青训营笔记
系列介绍
哈哈哈,其实这个系列如题,就是黄同学也参加了这一届青训营😚。作为一名知识内容输出爱好者,我是非常喜欢这个活动的。在接下来的日子里我会持续更新这个系列,希望可以通过这个过程,将我在这次后端青训营的学习过程,尤其是针对课程中知识内容的课下实践,自己的心得体会,知识总结输出到掘金社区。💖
哈哈哈,其实也希望通过这个来对我这种懒狗的一种鞭策。🏅
目前该系列已经发布了:
- 👉Go快速上手之基础语法 | 青训营笔记 - 掘金 (juejin.cn) 👈
- 👉 Go语法实战个人拓展(词典) | 青训营笔记 - 掘金 (juejin.cn) 👈
- 👉 Go基础语法之实战 | 青训营笔记 - 掘金 (juejin.cn) 👈
感兴趣的可以看看🌹
本文摘要
socks5代理简单的介绍。- 针对官方的这个实战小项目的实践过程中针对代码、工具的使用分析。
1. SOCKS5
1.1 介绍
SOCKS5是一个代理协议,往往在TCP/IP架构的前端和服务端(后端)之间扮演一个中间件的角色(这一点,类似的,黄同学还知道有MQTT中的MQTT Broker,网络层的话还有熟悉NAT路由器),这样做的目的在于使得内部网的前端应用能够访问外部网(Internet)的服务器或者整个通信过程更加安全。(黄同学认为前者就类似NAT)。- 可以代理任何类型的流量,除了
http外,支持IPv6和UDP协议,且可用于DNS(域名解析)。(这一点算是在SOCKS4的基础上,拓展了对流量的兼容性)。 - 提供可选的身份验证功能,只允许授权用户访问代理服务器,从而提升安全性和隐私性。
- 在
OSI参考模型中,属于会话层,通过TCP连接转发UDP数据包。(具体:客户端向代理发送UDP数据报文,代理服务器向目标服务器通过TCP连接转发报文)。
✨ TCP 连接 传输UDP数据包
讲道理,上面的第四点,黄同学也有点懵逼。梳理一下懵逼的点:
- 为什么TCP连接可以传输UDP数据包,从层级来说,两个协议是同层的,即都是传输层协议。
- 如果是经过封装后,但是UDP传输的定义是报文传输,而TCP是按字节传输,那么是如何实现的?
socks5在这个过程是起到怎么样的作用?
在一些文献中(包括RFC文档),黄同学面前可以再次给出自己理解,针对上述问题的解答。
- 简单来说,TCP和UDP都有自己独特的首部,会标识端口,长度等信息。TCP在连接建立时,会协定一个最大分段大小MSS(这个个人理解就是TCP数据包的最大大小),MSS 一般会小于MTU(网络(
IP)层的最大分组大小)。而UDP则没有MSS这种类似的概念,不过一般也是要求小于MTU,因此要使得在TCP连接中传输UDP数据包,肯定是要将UDP数据包通过封装后,装载到TCP数据包,交由TCP层传输。 - 如何封装?这里交由
SOCKS5将UDP数据包前面加入一个SOCKS5的头部,用来标识UDP的地址和端口信息。通过这些信息,代理服务器在收到后既可以解封UDP数据包,并进行转发给目标服务器。 - 注意⚠️,这里的封装并不是指在
OSI模型中的层次封装,指在应用层的协议封装,直接点SOCKS5就是在应用层对UDP数据包来一次封装,然后按照网络模型来交给TCP层传送,即在应用层进行一种协议转换(UDP和TCP,应用层的隧道),TCP层不需要知道上层的数据是什么协议,只需要按照TCP的方式进行传输即可。
1.2 工作原理
- 三个阶段:握手阶段、认证阶段、传输阶段。
- 握手阶段:客户端和代理建立TCP连接后,客户端发送握手请求(包括SOCKS版本号、认证方法和支持的认证方式列表)。代理响应这个握手请求,并选择性进行认证。
- 认证阶段:如果要认证(就是可以不用认证,其实有些说法中 无认证 也是一种认证方式),代理会验证客户端的身份,常见的认证方式:用户名密码认证、**
GSSAPI**认证等。 - 传输阶段:认证成功后,这个就比较简单了,客户端发送真正的请求(目标的地址与端口等),代理收到后,根据信息和目标服务器建立连接(TCP),然后通过连接转发客户端的请求。(目标服务器的过程类似)
1.3 特点 与 替代方案
特点
- 不限制传输信息的协议,只专注于数据包的传输,所以socks5应用广泛。
- 提供了身份验证,具有一定程度的灵活性与安全性。
- 传输的数据包小,使得socks5代理传输非常快。
- 不提供数据加密,不能保护数据被监听与篡改。也不会隐藏真实的IP地址,用户隐私存在泄露的风险。
替代方案
有个似乎不能发😅
- HTTP代理。
- HTTPS代理。
2. 实战
2.1 实战案例介绍
很直接,就是搭建一个socks5 代理服务器,使得我们的设备(客户端)可以通过这个代理服务器去访问外部网络,外部网络的响应也是通过这个代理返回给客户端。
2.2 版本迭代
v1:发啥回啥
先上代码:
package main
import (
"bufio"
"fmt"
"log"
"net"
)
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) /*go: goroutine, 类似子线程, 开销比子线程小 */
}
}
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
for {
b, err := reader.ReadByte()
if err != nil {
break
}
_, err = conn.Write([]byte{b})
if err != nil {
break
}
fmt.Println(b) /*新增的代码用来校验*/
}
}
-
这是一个最简单的代理服务器,但其实还没有实现代理的功能,搭了一个简单的服务器。
-
代码中的一些有意思的:
net.Listen()用这个api,算是搭建了一个服务器,专门侦听指定ip的指定端口。go关键词,这个其实就涉及到之前讲的第一篇笔记👉Go快速上手之基础语法 | 青训营笔记 - 掘金 (juejin.cn)中关于goroutine。类比,就是执行多一个线程(开销要小于线程)。- 整个代码其实就是,客户端发了什么,服务端就回什么,黄同学多加了一句,在服务端进行打印。
-
运行测试(windows),黄同学用的时windows的cmd,并没有课程中的linux的nc指令,所以是用telnet来连接服务器。
telnet 127.0.0.1 1080从上面我们可以知道,在
telnet窗口中,输入一个字符就输出一个字符。
v2:多了认证
相比v1,多了认证部分,主要认证的是 协议版本,判断是否是socks5。
-
代码,这里只放相比v1的改动部分,首先是声明一个全局变量,表示socks5的版本号
const socks5Ver = 0x05 /*socket5的版本号*/然后是在
Process中调用新增的认证函数authfunc 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 } log.Println("auth success") }然后是新增的
auth函数,会检验报文中的协议是否是socks5协议。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 // 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) /*根据编码数量创建slice*/ _, err = io.ReadFull(reader, method) /*填充创建的slice*/ 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 } -
以上代码,主要是新增了认证函数
auth,在里边实现认证,打印对应的信息,同时给客户端返回一些信息。 -
实际运行,和官方一样,黄同学这里也是用curl(当然,还是用cmd):
curl --socks5 127.0.0.1 1080 -v http://www.baidu.com可以发现,通过这个代理去访问外部网,并不成功,但是代理服务器这版显示还是认证成功了。
v3:新增了对连接请求的处理
-
相比v2,多了对客户端的连接请求进行处理,但是还是没有和目标服务器建立连接。
-
看代码的改动部分:几个全局变量存储,用于判断指令和目标协议类型
const cmdBind = 0x01 const atypeIPV4 = 0x01 const atypeHOST = 0x03 const atypeIPV6 = 0x04process中调用了connect函数的调用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 } }connect函数的实现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) /*先读4个字节,即版本号,cmd,保留字段和目标地址类型*/ _, 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] // 跳过非socks5协议以及连接命令 if ver != socks5Ver { return fmt.Errorf("not supported ver:%v", ver) } if cmd != cmdBind { return fmt.Errorf("not supported cmd:%v", cmd) } addr := "" // 根据目标类型(协议类型)执行不同结果,ipv6不处理 switch atyp { case atypeIPV4: _, err = io.ReadFull(reader, buf) /*读取目标ip地址*/ 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]) /*从一个字节切片中读取一个大端序的无符号整数*/ 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) } return nil } -
运行测试一下,虽然没有成功连接目标网址,但是代理这边的运行结果显示了获取到了ip地址和端口。
v4:dial 建立tcp连接,双goroutine,双向数据转发
-
代码上,主要是对
connect函数进行更改,调用了Dial来和目标服务器建立TCP连接。创建上下文对象和取消函数,使用两个goroutine,使用阻塞等待。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)) /*建立了tcp连接*/ 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()) /*创建一个可取消的上下文对象ctx和取消函数cancel*/ defer cancel() /*函数退出时,调用取消函数,释放ctx*/ // 下面两个goroutine,实现双向数据转发,如果传输结束或失败就会调用cancel go func() { _, _ = io.Copy(dest, reader) cancel() }() go func() { _, _ = io.Copy(conn, dest) cancel() }() <-ctx.Done() /*阻塞等待上下文ctx被取消,只有当上面两个传输都完成调用cancel时,这里的ctx才会被取消,这一步才会调用,返回*/ return nil }
-
实际运行(cmd)
可以发现,客户端这边成功收到了百度首页的html数据代码。代理服务器也有所显示。
下面通过
SwitchyOmega这个插件来让浏览器使用这个代理。
2.3 浏览器使用代理 (SwitchyOmega)
- 首先,你需要给你的浏览器安装拓展
SwitchyOmega,黄同学这里用的是Chrome浏览器。如果你也是用这个浏览器,可以在应用商店安装。 - 安装后,需要启用这个插件,然后在插件页选择情景模式,设置代理信息如下图所示
- 插件一定要选择你设定的代理!
- 然后在浏览器随便访问一些网址,比如黄同学这里访问
LC。可以看到代理服务器这边有显示对应的信息。
参考资料
黄同学在编写这篇文章,除了自己的实践外,还参考了不少资料。如果朋友想要通过我的这篇简陋笔记文章去探索那些可以称为宝玉或者 💎 般的知识,不妨通过下面的链接看看: