Go实现socks5代理|青训营笔记

531 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记。

参考博客:(11条消息) 实战:150行Go实现高性能socks5代理_felix021的博客-CSDN博客

太菜了所以写了个详细版。

SOCKS5 是一个代理协议,它在使用TCP/IP协议通讯的前端机器和服务器机器之间扮演一个中介角色,使得内部网中的前端机器变得能够访问Internet网中的服务器,或者使通讯更加安全。

以CONNECT请求为例,其工作流程为:

1.握手阶段:客户端向代理服务器发出请求信息,用以协商版本和认证方法,代理服务器应答,将选择的方法发送给客户端。

2.认证阶段:客户和代理服务器进入由选定认证方法所决定的子协商过程。

3.请求阶段:子协商过程结束后,客户端发送请求信息,其中明了目标服务器的IP地址和端口。代理服务器验证客户端身份,验证通过后会与目标服务器连接。代理服务器向客户端返回连接信息。

4.relay阶段:连接完成,则代理服务器开始作为中转站中转数据。

首先,我们需要用go实现一个简单的TCP server。

func main() {
	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)
	}
}

在main函数中用net.listen去监听1080端口(socks5的默认端口),返回一个server。在死循环中不断地accept请求,每收到一个请求,启动一个 goroutine 来处理它。

func process(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		b, err := reader.ReadByte()
		if err != nil {
			break
		}
		_, err = conn.Write([]byte{b})
		if err != nil {
			break
		}
	}
}

bufio.NewReader创建了一个带缓冲区的只读流reader,调用ReadByte方法按字节进行读取。现在的函数非常简陋,读取什么就返回什么。 那么,process函数到底做了什么呢?可以先搭一个简易的框架。

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
	}
	err = connect(reader, conn)//connect实现的是请求阶段
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
}

auth函数

浏览器会给代理服务器发送一个包,结构是这样:

R2T1EICDC319~}0V$M85)1I.png

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)//不是socks5的报错
	}
	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也是0x05,对上 SOCKS 5 的暗号
        //我们选取了不需要认证的方式所以返回0x00
	_, err = conn.Write([]byte{socks5Ver, 0x00})
	if err != nil {
		return fmt.Errorf("write failed:%w", err)
	}
	return nil
}

connect函数

在完成认证以后,客户端需要告知服务端它的目标地址。和auth函数差不多,我们根据包的构造把需要的信息读出来,只不过这次稍微复杂一点。

PU65}5NUYF7VWG7RMM811.png

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
	buf := make([]byte, 4)
	_, err = io.ReadFull(reader, buf)//先读前四个字节,即VER,CMD,RSV,ATYP
	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 atypIPV4://如果是ipv4的话再把长度为4个字节的缓冲区读满
		_, 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])//获取到了ip
	case atypeHOST://对于host,先读长度,再按长度创建一个字节数组来读取
		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])//复用之前的buf,只需要两个字节

	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))//创建tcp连接
	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("write failed: %w", err)
	}
	return nil
}

创建完连接之后给客户端发包。按这个格式构造。 ED@QJW1Q_4%F1`IH1_@{XT2.png

其实前面都很好理解,最后双向转发数据的代码有点不太懂。

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest)
		cancel()
	}()

	<-ctx.Done()

由于tcp连接是双工通信,我们可以启动两个goroutinue,每个都用io.copy实现了单向数据转发。

context机制看不太明白,查了下资料。context.Context是Go中定义的一个接口类型,从1.7版本中开始引入。其主要作用是在一次请求经过的所有协程或函数间传递取消信号及共享数据,以达到父协程对子协程的管理和控制的目的。需要注意的是context.Context的作用范围是一次请求的生命周期,即随着请求的产生而产生,随着本次请求的结束而结束。

用context.WithCancel创建一个context,在connect函数的最后等待ctx.Done()。cancel被调用时,ctx.done会立刻返回。两个协程中,当某个方向的copy出错时会调用cancel,返回connect函数,连接关闭。