a CURD boy's blog.

使用 golang 撸一个反向 socks5 代理

2021.07.03

1.socks5代理

关于使用 Golang 做一个常规的 socks5 代理的文章, 网上已经有很多, 所以本次来尝试一种特殊情况, 即在目标机器不出网的情况下, 使用 Golang 实现一个反向的 socks5 隧道.

2.工具的基本原理

反向 socks5 与正向的 socks5 所不同的地方在于, socks5 服务端并不直接处于目标内网中, 而是通过 agent 维持的长连接来将代理流量传递到目标内网中

如图, 因为有防火墙的存在,user 和 server 无法直接正向连接到 agent 所在的机器,因此需要让 agent 主动回连,在 server 上再做一次类似端口转发的工作。
agent 运行后, 主动向 server 端发起长连接, 每一个长连接将会承载一个socks5代理请求.
server 端监听两个端口, 其中一个是面向用户使用的socks5服务端口, 另一个是与 agent 通信的端口.
在运行之后, agent 会向 server 发起一个长连接, 而 server 端在接收 user 的 socks5 代理连接之后, 将这两个连接怼到一起就好了.

3.代码实现

agent

agent 的实现其实非常简单, 这里使用了这个 socks5 的库, 因为它有一个比较方便的ServeConn方法。在TCP连接建立之后,就将连接交给处理 socks5 协议的库去处理,如果对协议部分感兴趣的话,可以跟到这个 socks5 的库源码里面看一看,大概的步骤就是读取协议头,进行认证(如果有的话),然后从协议头中获取客户端的目标地址,由 server 端向目标发起连接,然后将客户端到 server 的连接和 server 到目标的连接对接起来。

package main

import (
	"fmt"
	"net"
	"time"

	"github.com/armon/go-socks5"
)

var server *socks5. Server 

func main() {
	// 起一个简单的 socks5 服务
	var err error
	 server , err = socks5.New(&socks5.Config{})
	if err != nil {
		panic(err)
	}
	// 不断向 server 发起连接请求, server 的连接池满了之后, 会阻塞在 dial 这一步
	for {
		conn, err := net.Dial("tcp", "127.0.0.1:8989")
		if err != nil {
			continue
		}
		// 连接成功之后, 使用 socks5 库处理该连接
		go handleSocks5(conn)
	}
}

func handleSocks5(conn net.Conn) {
	defer conn.Close()
	_ = conn.SetDeadline(time.Time{})
	// 使用该 socks5 库提供的 ServeConn 方法
	err :=  server .ServeConn(conn)
	if err != nil {
		fmt.Println(err)
	}
}

server

server 这边的实现也很简单首先监听两个端口,一个供 user 连接使用,另一个供 agent 回连使用。
在 agent 成功回连之后,再取一条 user 的连接,调用 golang 的 io.Copy 方法,将两个连接的输入输出互相复制,即可将流量转发到 agent 进行处理。

package main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

func main() {
	// 使用两个 channel 来暂存 agent 和 user 的连接请求
	userConnChan := make(chan net.Conn, 10)
	 agent ConnChan := make(chan net.Conn, 10)
	// 监听 agent 服务端口
	go ListenService( agent ConnChan, "127.0.0.1:8989")
	// 监听 user 服务端口
	go ListenService(userConnChan, "127.0.0.1:1080")
	for  agent Conn := range  agent ConnChan {
		userConn := <-userConnChan
		go copyConn(userConn,  agent Conn)
	}
}

func ListenService(c chan net.Conn, ListenAddress string) {
	listener, err := net.Listen("tcp", ListenAddress)
	if err != nil {
		panic(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println(err)
			continue
		}
		c <- conn
	}
}

func copyConn(srcConn, dstConn net.Conn) {
	_ = srcConn.SetDeadline(time.Time{})
	_ = dstConn.SetDeadline(time.Time{})
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		defer srcConn.Close()
		defer dstConn.Close()
		_, err := io.Copy(srcConn, dstConn)
		if err != nil {
			return
		}
	}()
	go func() {
		defer wg.Done()
		defer dstConn.Close()
		defer srcConn.Close()
		_, err := io.Copy(dstConn, srcConn)
		if err != nil {
			return
		}
	}()
	wg.Wait()
}

3.效果图

4.一些可以改进的地方

可以改进的地方其实有很多, 本文给出的 demo 只能算作一个可行性验证, 如果在真实环境中使用会遇到一些问题, 例如重度使用时不可避免的会遇到连接数过高的问题, socks5 虽然是一个轻量化的协议, 但是特征还是太过于明显, 并且因为 agent 引用了一个完整的 socks5 实现库, 体积必然不会小到哪里去。因此, 如果想要改进的话, 可以尝试由 server 来处理 socks5 的认证步骤, agent 和 server 之间采用自己实现的更轻量化的协议, 这样做可以:

  • 带来更小的 agent 体积
  • 复用 agent 和 server 的 TCP 连接来更精确的控制连接数
  • agent 和 server 之间可以使用 UDP 来通信, 更加隐蔽