Socks5代理协议与Golang实现 | 青训营

1. socks介绍

1.1 什么是socks

SOCKS(Socket Secure)是一种网络协议,通过代理服务器实现网络通信。简单来说,就是作为一个中转站,在客户端和客户端目标主机之间转发数据。SOCKS是运行在OSI七层协议中的第五层会话层,而我们常用http/https,SMTP,FTP协议都在第七层,所以它可以处理多种常用请求。

1.2 socks有什么用

当我们使用socks代理上网时,我们的目标主机只会看见运行socks的中转站ip地址,这种方式可以隐藏我们的的真实IP地址,增加了匿名性和安全性。如果我们的ip被某些网站限制访问,便可以使用socks进行代理访问。

1.3 socks5与socks4

socks5是socks4的下一个版本,加入了身份认证机制来建立完整的TCP连接,并支持UDP转发,目前互联网上基本以socks5为主。

2. socks5工作过程与Golang实现

本次讲解使用Edge与插件SwitchyOmega作为测试工具,原来来自字节青训营课程资料。

2.1 浏览器和socks5代理建立TCP连接

首先使用golang在本地监听一个端口。

go 复制代码
func main() {
	//监听TCP连接
	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)
	}
}

进入SwitchyOmega选项设置,选择socks5协议,并输入本地地址与监听端口

此时,如果打开SwitchyOmega的代理模式,浏览器的所有请求携带socks5验证信息进入本地1080端口。

2.2 socks5协议协商

标准详见RFC 1928 - SOCK5

在进行代理转发前,socks5需要与客户端进行协商,包括协议版本,支持的认证方式等。

go 复制代码
func process(conn net.Conn) {
	defer conn.Close()

	//把客户端首次请求的数据全部读进reader
	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) {

	// request:
	// +----+----------+----------+
	// |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)
	_, err = io.ReadFull(reader, method)
	if err != nil {
		return fmt.Errorf("read method failed:%w", err)
	}
	//第三步
	//response:
	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}
  • 第一步 读取客户端的首次请求数据放到reader中,读取第一个字节(版本号),判断版本号是否为5,不为5则断开连接.
  • 第二步 读取第二个字节(认证方法数量),并根据认证方法数量,make一个用于存放方法的method,并从reader中读入。
  • 第三步 返回响应,两个字节,第一个是版本5,第二个是表示认证方法(不需要认证)。如果返回0x02就是需要认证。

2.2.1 扩展--加入认证

原理和上面差不多 标准详见RFC 1929 - Username/Password Authentication for SOCKS

go 复制代码
func process(conn net.Conn) {
	defer conn.Close()

	//把客户端首次请求的数据全部读进reader
	reader := bufio.NewReader(conn)
	//------开始协商------

	if err := auth(reader, conn); err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	auReader := bufio.NewReader(conn)
	if err := authentication(auReader, conn); err != nil {
		log.Printf("authentication err")
		return
	}
	//------协商成功-------
	if err := connect(reader, conn); err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}
const subnegotiation = 0x01

func authentication(reader *bufio.Reader, conn net.Conn) (err error) {
	/**
	*  +----+------+----------+------+----------+
	*  |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
	*  +----+------+----------+------+----------+
	*  | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
	*  +----+------+----------+------+----------+
	 */
	ver, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ver failed:%w", err)
	}
	if ver != subnegotiation {
		return fmt.Errorf("not supported ver:%v", ver)
	}
	ulen, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read ulen failed:%w", err)
	}
	uname := make([]byte, ulen)
	_, err = io.ReadFull(reader, uname)
	if err != nil {
		return fmt.Errorf("read uname failed:%w", err)
	}
	if string(uname) != "admin" {
		_, err = conn.Write([]byte{0x01, 0x01})
		if err != nil {
			return fmt.Errorf("write failed:%w", err)
		}
		return fmt.Errorf("auth uname failed:%w", err)
	}

	plen, err := reader.ReadByte()
	if err != nil {
		return fmt.Errorf("read plen failed:%w", err)
	}

	passwd := make([]byte, plen)
	_, err = io.ReadFull(reader, passwd)
	if err != nil {
		return fmt.Errorf("read passwd failed:%w", err)
	}
	if string(passwd) != "123456" {
		_, err = conn.Write([]byte{0x01, 0x01})
		if err != nil {
			return fmt.Errorf("write failed:%w", err)
		}
		return fmt.Errorf("auth uname failed:%w", err)
	}

	/*  +----+--------+
	*  |VER | STATUS |
	*  +----+--------+
	*  | 1  |   1    |
	*  +----+--------+
	 */
	_, err = conn.Write([]byte{0x01, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}

	return
}

2.3请求与中继

go 复制代码
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
}

这部分其实没什么好说的,主要分为两部分,一部分是socks5服务器需要知道请求的就细节,一部分是开始进行中继服务,// BND.ADDR 服务绑定的地址 // BND.PORT 服务绑定的端口DST.PORT这部分返回0意思是socks5服务器和中继服务器是在一起的,不过你也可以进行拆分,把中继服务独立出去,并告知客户端具体的地址。

3. 测试

SwitchyOmega不支持socks5的认证,所以这里使用curl进行测试。

bash 复制代码
#不需要认证 
curl -x socks5h://127.0.0.1:7582 http://www.baidu.com
#如果需要认证 
curl -x socks5h://bigbyto:123456@127.0.0.1:7582 http://www.baidu.com

如果你使用的是cmd或者powershell进行测试,可能会出现下面的情况。

如果你有git,在vscode里面切换到git bash就可以了。 测试验证功能

相关推荐
凡人的AI工具箱2 小时前
15分钟学 Python 第38天 :Python 爬虫入门(四)
开发语言·人工智能·后端·爬虫·python
丶21362 小时前
【SQL】深入理解SQL:从基础概念到常用命令
数据库·后端·sql
木子02042 小时前
Nacos的应用
后端
哎呦没2 小时前
Spring Boot框架在医院管理中的应用
java·spring boot·后端
陈序缘3 小时前
Go语言实现长连接并发框架 - 消息
linux·服务器·开发语言·后端·golang
络73 小时前
Spring14——案例:利用AOP环绕通知计算业务层接口执行效率
java·后端·spring·mybatis·aop
2401_857600953 小时前
明星周边销售网站开发:SpringBoot技术全解析
spring boot·后端·php
AskHarries4 小时前
Spring Cloud 3.x 集成admin快速入门Demo
java·后端·spring cloud
程序员大金4 小时前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_2518364574 小时前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端