实现一个简单的 SOCKS5 代理服务器 | 青训营

116 阅读10分钟

实现一个简单的 SOCKS5 代理服务器 | 青训营

在这篇学习笔记中,我们将学习如何使用 Go 语言实现一个简单的 SOCKS5 代理服务器。这个代理服务器可以接收客户端连接,并将客户端的数据转发到目标服务器。我们将逐步分析代码并解释每个部分的功能。

1. 引言

SOCKS5(Socket Secure 5)是一种网络协议,用于在客户端和目标服务器之间建立代理连接,使得客户端可以通过代理服务器访问目标服务器上的资源。本文中,我们将使用 Go 语言编写一个简单的 SOCKS5 代理服务器,帮助理解代理服务器的基本工作原理。

2. 导入所需的包

首先,我们导入了需要的包,其中 net 用于网络通信,bufio 用于缓冲读取,io 用于输入输出,log 用于日志输出。

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

3. 定义常量

我们定义了一些常量,包括 SOCKS5 协议版本,绑定命令,不同地址类型等。这些常量在代码中用于协议解析。

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

4. 认证处理

auth 函数用于处理客户端的认证阶段。它从客户端读取数据,校验协议版本,然后发送认证方法并等待客户端的选择。最后,发送认证响应。

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	// 读取协议版本
	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)
	}
	log.Println("ver", ver, "method", method)

	// 发送认证响应
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}
  1. 读取协议版本:

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

    这里使用 reader.ReadByte() 从连接中读取一个字节,即协议版本号。如果读取失败,会返回一个带有错误信息的错误。

  2. 校验协议版本:

    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    

    此步骤检查读取的协议版本是否为 SOCKS5 的版本(0x05)。如果不是,会返回一个错误,说明版本不受支持。

  3. 读取认证方法数目及方法列表:

    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)
    }
    

    在这一步中,首先从连接中读取一个字节,该字节表示支持的认证方法的数量。然后,从连接中读取方法列表,并将其存储在一个切片中。如果读取过程中出现错误,会返回一个带有错误信息的错误。

  4. 发送认证响应:

    _, err = conn.Write([]byte{socks5Ver, 0x00})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    

    这一步是向客户端发送认证响应。响应是一个字节的切片,其中第一个字节是协议版本(应为 0x05),第二个字节是支持的认证方法。在这里,我们发送了一个无需认证的响应(0x00),表示客户端无需进行认证

5. 连接处理

connect 函数用于处理客户端的连接请求。它从客户端读取请求头部,解析目标地址和端口,然后与目标服务器建立连接。最后,启动两个协程来实现数据转发。

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	// 读取请求头部
	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", ver)
	}

	// 解析目标地址
	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()

	// 发送连接成功响应
	_, 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
}
  1. 读取请求头部:

    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]
    

    这里从客户端的连接中读取 4 个字节,其中包含了协议版本、命令和目标地址类型(ATYP)等信息。

  2. 校验协议版本和命令:

    if ver != socks5Ver {
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
        return fmt.Errorf("not supported cmd:%v", ver)
    }
    

    在这一步中,检查协议版本是否为 SOCKS5 的版本(0x05),并校验命令是否为绑定(0x01),如果不满足这些条件,会返回一个相应的错误。

  3. 解析目标地址:

    addr := ""
    switch atyp {
    case atypeIPV4:
        _, err = io.ReadFull(reader, buf)
        // ... 省略其他逻辑 ...
    case atypeHOST:
        // ... 省略其他逻辑 ...
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    

    在这一步,根据目标地址类型(ATYP)的值,解析目标地址的内容。如果 ATYP 是 IPV4 类型,会读取 4 个字节作为 IP 地址;如果是 HOST 类型,会读取一个字节来表示主机名的长度,然后再读取相应长度的主机名;如果是 IPV6 类型,则返回一个错误,因为目前未支持 IPV6。

  4. 解析目标端口:

    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
        return fmt.Errorf("read port failed:%w", err)
    }
    port := binary.BigEndian.Uint16(buf[:2])
    

    这一步读取 2 个字节来解析目标端口号。

  5. 与目标服务器建立连接:

    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()
    

    在这一步,通过 net.Dial 函数与目标服务器建立连接。如果连接失败,会返回相应的错误,并在函数结束时关闭连接。

  6. 发送连接成功响应:

    _, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
        return fmt.Errorf("write failed:%w", err)
    }
    

    在这一步,向客户端发送连接成功的响应。

  7. 启动两个协程实现数据转发:

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func() {
        _, _ = io.Copy(dest, reader)
        cancel()
    }()
    go func() {
        _, _ = io.Copy(conn, dest)
        cancel()
    }()
    <-ctx.Done()
    

    这里创建了两个协程,一个用于将数据从客户端读取并写入目标服务器,另一个用于将数据从目标服务器读取并写入客户端。通过 io.Copy 函数,实现了数据的转发。同时,使用了 context 来控制协程的结束。

综上所述,这个函数的作用是在建立连接后,根据协议头部解析出目标地址和端口,并与目标服务器建立连接。然后,它向客户端发送连接成功响应,并启动两个协程实现数据的双向转发,实现了 SOCKS5 代理服务器的核心功能。

6. 连接处理

process 函数用于处理客户端的连接。它在认证通过后调用 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
	}
	log.Println("auth success")

	// 处理连接请求阶段
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}

这个函数实现了代理服务器对客户端连接的处理流程。以下是对这个函数逐步解释的过程:

  1. 延迟关闭连接:

    defer conn.Close()
    

    在函数结束时,使用 defer 关键字关闭连接,以确保在函数执行结束后,连接会被正确关闭,从而释放资源。

  2. 创建读取器:

    reader := bufio.NewReader(conn)
    

    创建一个带缓冲的读取器,用于逐行读取客户端连接中的数据。

  3. 处理认证阶段:

    err := auth(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    log.Println("auth success")
    

    在这一步,调用之前解释过的 auth 函数,对客户端发起的认证请求进行处理。如果认证失败,会记录错误日志,并结束该连接。如果认证成功,会打印认证成功的日志。

  4. 处理连接请求阶段:

    err = connect(reader, conn)
    if err != nil {
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    

    在这一步,调用之前解释过的 connect 函数,处理客户端发起的连接请求。如果处理失败,会记录错误日志,并结束该连接。

7. 主函数

主函数中,我们监听本地地址和端口,等待客户端连接。一旦有连接建立,就启动一个协程处理连接请求。

func main() {
	server, err := net.Listen("tcp", "127.0.0.1:1080")
	if err != nil {
		panic(err)
	}
	defer server.Close()

	log.Println("SOCKS5 proxy server started on 127.0.0.1:1080")

	for {
		client, err := server.Accept()
		if err != nil {
			log.Printf("Accept failed: %v", err)
			continue
		}
		go process(client)
	}
}

8. 全部代码


package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
	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)
	}
	log.Println("ver", ver, "method", method)
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	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", ver)
	}
	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)
	_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
	if err != nil {
		return fmt.Errorf("writer 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
}
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
	}
	log.Println("auth success")
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}
func main() {
	//using nc invent needs deleting ":"
	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 failes %v", err)
			continue
		}
		go process(client)
	}
}

9. 总结

通过这篇学习笔记,我们了解了如何使用 Go 语言编写一个简单的 SOCKS5 代理服务器。我们分析了每个函数的功能,包括认证处理、连接处理以及主函数中的监听和连接处理。通过这个例子,我们学习了如何使用 Go 语言处理网络连接、读取和写入数据,以及如何使用协程来实现并发处理。这种代理服务器的基本原理在实际应用中有很多用途,例如网络安全、数据监控等领域。如果你希望进一步深入了解代理服务器的工作原理,可以尝试扩展该代理服务器的功能,例如支持更多的命令、加密传输等