引言
在 NPS 系列文章的前几篇中,我们已经对 NPS 的整体架构、服务端核心以及 TCP 隧道和 HTTP 代理的实现有了初步了解。本篇文章将深入 NPS 的另一个重要代理模式——SOCKS5 代理。我们将通过分析 nps/server/proxy/socks5.go
文件,详细剖析 SOCKS5 协议在 NPS 中的实现细节,包括认证机制、请求处理以及 UDP 转发。
SOCKS5 协议简介
SOCKS5 是一种网络代理协议,它允许客户端通过代理服务器间接访问其他服务器。与 HTTP 代理不同,SOCKS5 是一种更底层的协议,它不关心应用层协议(如 HTTP、FTP),而是直接转发 TCP 或 UDP 数据包。这使得 SOCKS5 代理更加通用,可以用于各种网络应用。
SOCKS5 协议主要包括以下几个阶段:
- 协商认证方法:客户端向服务器发送支持的认证方法列表。
- 认证:服务器选择一种认证方法,并与客户端进行认证。
- 请求:客户端向服务器发送连接请求,包括目标地址、端口和连接类型(CONNECT、BIND、UDP ASSOCIATE)。
- 响应:服务器响应请求,表示连接是否成功建立。
socks5.go
:SOCKS5 代理的实现
socks5.go
文件定义了 Sock5ModeServer
结构体,它是 NPS 实现 SOCKS5 代理的核心。
Sock5ModeServer
结构体
Sock5ModeServer
结构体继承了 BaseServer
,并增加了 listener
字段:
type Sock5ModeServer struct {
BaseServer
listener net.Listener
}
BaseServer
:继承了base.go
中定义的通用功能,如流量统计、安全检查等。listener net.Listener
:用于监听传入 SOCKS5 连接的网络监听器。
NewSock5ModeServer()
函数用于创建并初始化一个 Sock5ModeServer
实例。
Start()
方法:启动 SOCKS5 监听
Sock5ModeServer
的 Start()
方法负责启动 TCP 监听,并为每个传入连接调用 handleConn()
方法进行 SOCKS5 协议的协商和处理:
func (s *Sock5ModeServer) Start() error {
return conn.NewTcpListenerAndProcess(s.task.ServerIp+":"+strconv.Itoa(s.task.Port), func(c net.Conn) {
if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
logs.Warn("client id %d, task id %d, error %s, when socks5 connection", s.task.Client.Id, s.task.Id, err.Error())
c.Close()
return
}
logs.Trace("New socks5 connection,client %d,remote address %s", s.task.Client.Id, c.RemoteAddr())
s.handleConn(c) // 调用 SOCKS5 协议处理函数
s.task.Client.AddConn()
}, &s.listener)
}
与 TunnelModeServer
类似,这里也进行了流量和连接数检查。核心在于调用 s.handleConn(c)
来处理 SOCKS5 协议的握手和请求。
handleConn()
:SOCKS5 协议协商与认证
handleConn()
方法是 SOCKS5 协议处理的入口点,它负责与客户端进行认证方法的协商和实际的认证过程:
- 版本协商:读取客户端发送的 SOCKS 版本(必须是 5)。
- 认证方法协商:读取客户端支持的认证方法数量和列表。
- 认证:
- 如果服务端配置了用户名和密码(
s.task.Client.Cnf.U
和s.task.Client.Cnf.P
),或者启用了多用户认证 (s.task.MultiAccount
),则服务端会选择UserPassAuth
(用户名/密码认证) 方法。 - 调用
s.Auth(c)
方法进行实际的用户名/密码认证。 - 如果认证成功,则发送认证成功响应;否则发送认证失败响应并关闭连接。
- 如果未配置认证,则直接选择
No Authentication Required
(无需认证) 方法。
- 如果服务端配置了用户名和密码(
- 请求处理:认证成功后,调用
s.handleRequest(c)
处理客户端的连接请求。
Auth()
:用户名/密码认证
Auth()
方法实现了 SOCKS5 的用户名/密码认证子协议:
- 读取用户名和密码长度:从客户端读取用户名和密码的长度。
- 读取用户名和密码:根据长度读取用户名和密码。
- 验证:
- 如果启用了多用户认证,则从
s.task.MultiAccount.AccountMap
中查找用户名和对应的密码进行验证。 - 否则,使用
s.task.Client.Cnf.U
和s.task.Client.Cnf.P
进行验证。
- 如果启用了多用户认证,则从
- 发送认证结果:根据验证结果发送认证成功 (
authSuccess
) 或失败 (authFailure
) 响应。
handleRequest()
:SOCKS5 请求处理
handleRequest()
方法负责解析客户端的 SOCKS5 请求,并根据请求类型调用不同的处理函数:
- 读取请求头:读取 SOCKS5 请求的 CMD(命令)、RSV(保留字段)和 ATYP(地址类型)等信息。
- 根据 CMD 类型分发:
connectMethod
(1):调用s.handleConnect(c)
处理 TCP 连接请求。bindMethod
(2):调用s.handleBind(c)
处理 BIND 请求(通常用于 FTP 等被动模式)。associateMethod
(3):调用s.handleUDP(c)
处理 UDP 关联请求(用于 UDP 转发)。- 其他:发送
commandNotSupported
响应并关闭连接。
doConnect()
:处理 CONNECT 请求
doConnect()
是 handleConnect()
的核心逻辑,它负责解析目标地址和端口,并建立与目标服务的连接:
- 解析目标地址类型:根据 ATYP 字段判断目标地址是 IPv4、IPv6 还是域名。
- 解析目标地址和端口:读取对应的地址和端口信息。
- 调用
s.DealClient()
:与 TCP 隧道和 HTTP 代理类似,最终都通过BaseServer
的DealClient()
方法来建立与目标服务的连接并进行数据转发。在成功建立连接后,会向客户端发送succeeded
响应。
handleUDP()
:处理 UDP 关联请求
handleUDP()
方法实现了 SOCKS5 的 UDP 关联功能,允许客户端通过 SOCKS5 代理进行 UDP 流量转发:
- 解析目标地址和端口:与
doConnect()
类似,解析客户端请求中的目标地址和端口。 - 本地 UDP 监听:在服务端本地监听一个 UDP 端口,用于接收来自客户端的 UDP 数据。
- 发送 UDP 响应:向客户端发送 SOCKS5 响应,包含本地监听的 UDP 端口信息。
- 建立 UDP 隧道:通过
s.bridge.SendLinkInfo()
建立一个 UDP 隧道到客户端。 - 双向 UDP 数据转发:启动两个 goroutine,一个负责将本地 UDP 监听到的数据转发给客户端,另一个负责将客户端发送过来的 UDP 数据转发给目标服务。
handleBind()
:处理 BIND 请求
handleBind()
方法目前在代码中是空的,这意味着 NPS 尚未实现 SOCKS5 的 BIND 命令。BIND 命令通常用于 FTP 等需要服务器主动连接客户端的场景。
总结
nps/server/proxy/socks5.go
文件详细展示了 NPS 如何实现 SOCKS5 代理协议。它涵盖了从认证协商到不同请求类型(CONNECT、UDP ASSOCIATE)的处理流程。通过对 SOCKS5 协议的深入理解和实现,NPS 提供了强大的通用代理能力,使得用户可以灵活地访问内网资源。虽然 BIND 命令尚未实现,但 NPS 已经能够满足绝大多数 SOCKS5 代理的使用场景。
在下一篇文章中,我们将继续探索 NPS 的其他代理模式,例如 P2P 代理。