1. socks协议介绍
编写一个socks5代理服务器。提到代理服务器,很多人首先想到翻墙,但很遗憾,虽然socks5是代理协议,它并不能用于翻墙,因为其传输数据是明文的。
socks5协议历史悠久,起源于互联网早期。它的主要用途是在某些企业的内网中,为了确保安全性,企业会设置严格的防火墙策略,这可能会妨碍访问某些资源。使用socks5协议就相当于在防火墙上开了一个通道,允许授权用户通过一个端口访问内部的所有资源。其实,很多翻墙软件最终暴露出去的也是一个socks5协议的端口。
如果有同学尝试过开发爬虫,就会知道在爬取网页时,IP访问频率容易超过限制。面对这种情况,许多人会去寻找代理IP池,而这些代理IP池中有很多使用的协议就是socks5。
我们先来看看最终完成的代理服务器的效果。启动程序后,在浏览器中配置使用这个代理,然后打开网页。此时,代理服务器的日志会显示你访问的网站的域名或 IP,这表明我们的网络流量正在通过这个代理服务器。我们还可以在命令行中测试代理服务器。使用命令 curl -socks5 代理服务器地址 后面加上一个可访问的 URL,如果代理服务器正常工作,curl 命令就会返回结果。
2. socks5协议工作原理
接下来,我们将探讨 socks5 协议的工作原理。正常情况下,浏览器访问网站时,首先会与目标网站建立一个 TCP 连接,这个过程包括三次握手。完成握手后,浏览器发送 HTTP 请求,并接收 HTTP 响应。如果使用代理服务器,流程会变得更加复杂。
首先,浏览器会与 socks5 代理建立 TCP 连接,然后代理再与实际的服务器建立连接。整个过程可以分为四个阶段:握手阶段、认证阶段、请求阶段和中继阶段。
在握手阶段,浏览器会向 socks5 代理发送请求,该请求包含协议版本号和支持的认证方式。socks5 代理会选择一种认证方式并返回给浏览器。如果返回的内容是 00,表示不需要认证;如果是其他类型,则会进入认证流程,具体流程这里不作说明。
第三阶段是请求阶段。在认证通过后,浏览器会向 socks5 服务器发送请求,请求内容主要包括版本号和请求类型,通常是连接请求,这表明代理服务器需要与某个域名或 IP 地址的特定端口建立 TCP 连接。代理服务器收到请求后,会与后端服务器建立连接,并返回相应的响应。
最后是中继阶段。此时,浏览器会正常发送请求,代理服务器接收后会将请求转发给实际的服务器。如果实际的服务器返回响应,代理也会将该响应转发给浏览器。值得注意的是,代理服务器对流量的细节并不关注,这可以是 HTTP 流量,也可以是其他类型的 TCP 流量。这就是 socks5 协议的工作原理,接下来我们将尝试简单实现它。
2.1socks 代理-tcp echo server
首先,我们将通过 Go 创建一个简单的 TCP 回声服务器。这个服务器的主要功能是接收到的数据直接返回给客户端。
在 main 函数中,我们使用 net.Listen 来监听一个特定的端口,并将返回一个服务器实例。接着,我们进入一个无限循环中,调用 Accept 方法来处理传入的请求。一旦成功接受连接,我们便可以在一个名为 process 的函数中处理它。
在这里,我们使用 go 关键字来启动一个 goroutine,类似于其他语言中的子线程。与子线程相比,goroutine 的开销要小得多,因此它能够轻松地处理成千上万的并发连接。
接下来是 process 函数的实现。我们首先添加 defer connection.Close(),这是 Golang 中的一种语法,用于确保函数结束时关闭连接,以防止资源泄露。
然后,我们使用 bufio.NewReader 创建一个带缓冲的只读流。在之前的猜谜游戏中也使用过这种流。带缓冲流的主要作用是减少底层系统调用的次数。例如,在这里可以逐字节读取数据,但底层流可能将它们合并为几次较大的读取操作。此外,带缓冲流还提供了许多实用的函数来读取数据。
我们可以简单调用 ReadByte 函数来读取单个字节,并将该字节写回连接中。我们已经创建了一个能够返回输入信息的 TCP 服务器。接下来,我们将实现协议的第一步:认证阶段。这一部分会变得比较复杂。
我们来简单测试一下我们的第一个 TCP 服务器。测试时需要使用 nc 命令。请在终端中输入 nc 127.0.0.1 1080,然后输入 Helo,服务器会返回 Helo。
2.2socks代理 - auth
首先,我们实现一个空的 auth 函数,并在 process 函数中调用它。接下来,编写 auth 函数的具体代码。
回顾认证阶段的流程,首先浏览器会向代理服务器发送一个数据包,这个包包含三个字段:
- version:协议版本号,固定为 5。
- methods:认证方法的数量。
- methods 的编码:0 代表不需要认证,2 代表需要用户名和密码认证。
我们使用 read bytes 函数读取版本号,如果版本号不是 5,则直接返回错误。然后读取方法数量,这个字段也是一个字节。接着,我们需要创建一个与方法数量相等长度的切片,并使用 io.ReadFull 函数将其填充。
在这里,我们打印出获取到的版本号和认证方法。
此时,代理服务器需要返回一个响应包,包含两个字段:version(版本号)和 method(选择的认证方式)。我们当前将只实现不需要认证的情况,也就是返回 00。
我们可以使用 curl 命令测试当前版本的功能,结果会是什么样的呢?
此时,curl 命令肯定会失败,因为我们的协议尚未完成。然而,从日志中可以看到,version 和 method 可以正常打印,这表明我们的实现是正确的。
2.3socks5代理 - 请求阶段
接下来我们进入第三步,实现请求处理阶段。我们将读取包含 URL 或 IP 地址及端口的包,并将其打印出来。我们将创建一个名为 connect 的函数,其功能类似于 auth 函数,并在 process 中调用它。接下来,我们将编写 connect 函数的代码。
在请求处理阶段,浏览器会发送一个包,该包包含六个字段:首先是 version,表示版本号,始终为 5;其次是 command,表示请求类型,我们只支持建立连接的请求。接下来是 RSV,保留字段,不需处理;然后是 atype,表示目标地址类型,可能是 IPv4、IPv6 或域名;addr 字段的长度根据 atype 的不同而变化;最后是 port,表示端口号,为两个字节。我们将逐个读取这些字段。
这四个字段总共占用四个字节,我们可以一次性读取。我们定义一个长度为 4 的缓冲区,并将其填满。填满后,第0、1和3个字节分别是 version、cmd 和 type,version 应该是 socket 5,cmd 应该是 1。接下来,atype 可能是 ipv4、ipv6 或 host。如果是 IPv4,我们将再次填满缓冲区(因为缓冲区长度也是 4 个字节),然后逐字节打印成 IP 地址格式,并保存到 addr 变量中。
如果是 host,我们需要先读取它的长度,再创建一个相应长度的缓冲区来填充,然后将其转换为字符串并保存到 addr 变量中。IPv6 由于使用较少,我们暂时不支持。最后,读取两个字节的 port,并按照协议要求的大端字节序转换为数字。由于之前的缓冲区已经不再使用,我们可以直接复用这段内存,建立一个长度为 2 的临时切片用于读取,这样最多只需读取两个字节。
接下来,我们将打印这个地址和端口以便调试。收到浏览器请求包后,我们需要发送一个响应包。这个包有多个字段,但大部分字段不会被使用。第一个字段是版本号,仍然是 socket 5;第二个是返回类型,成功时返回0;第三个是保留字段,填0;第四个是 atype,地址类型,填1;第五、第六个暂时不需要使用,填0。最后,总共包括4 + 4 + 2个字节,并在后面填充六个字节为0。
现在我们来测试当前阶段的成果,先用 curl 进行简单请求。此时请求仍然会失败,但我们可以看到正确打印的访问 IP 地址和端口,这表明我们的实现是正常的。接下来,我们可以进行最后一步,真正与这个端口建立连接,并双向转发数据。
2.4socks5代理 - relay阶段
我们使用 net.dial 来建立一个 TCP 连接。连接建立后,需要添加一个 defer 语句以便在适当的时候关闭连接。接下来,我们需要实现浏览器和下游服务器之间的双向数据转发。标准库中的 io.copy 可以实现单向数据转发,但要实现双向转发,则需要启动两个 goroutine。 现在有一个问题,connect 函数会立即返回,而此时连接可能已经关闭。我们需要等到任意一方出现复制错误时,再返回 connect 函数。
为了解决这个问题,可以使用标准库中的 context 机制,结合 context.WithCancel 来创建一个上下文。在最后,我们等待 ctx.Done(),只要调用了 cancel,ctx.Done 就会立刻返回。然后在之前的两个 goroutine 中各调用一次 cancel。 这样我们的代理服务器终于完成了。现在我们来进行测试。执行curl命令后,终于得到了成功的反馈。
我们可以在浏览器中进行测试。要测试代理,请安装 SwitchOmega 插件。接下来,新建一个情景模式,选择 Socks5 作为代理服务器,设置端口为 1080,然后保存并启用该模式。此时,您仍然可以正常访问网站,代理服务器将显示出浏览器版本的域名和端口。
3. windows下测试
-
使用curl
- 运行程序:
go run ../v4/main.go - cmd输入:
curl --socks5 127.0.0.1:1080 -v 网址,请求正常说明代理工作正常
- 运行程序:
-
使用浏览器(edge,chrome)插件switchyOmega
- 扩展里面安装switchyOmega
- 运行程序
- 打开-->新建情景模式-->
设置代理协议:socks5, 代理服务器:127.0.0.1, 代理端口:1080
-->保存(应用选项)退出
-->点击扩展中switchyOmege选择刚刚设置的情景模式 -->点开任意网页测试
-
代理设置
-
运行程序
-
开始-->设置-->网络和Internet-->手动设置代理打开-输入对应服务器和端口
-
打开任意网页测试
-