go socks5实现

什么是SOCKS

SOCKS是一种网络传输协议,主要用于Client端与外网Server端之间通讯的中间传递。SOCKS是"SOCKETS"的缩写,意外这一切socks都可以通过代理。 SOCKS是一种代理协议,相比于常见的HTTP代理,SOCKS的代理更加的底层,传统的HTTP代理会修改HTTP头,SOCKS则不会它只是做了数据的中转。

SOCKS的最新版本为SOCKS5。

SOCKS5协议详解

SOCKS5的协议是基于二进制的,协议设计十分的简洁。

Client端和Server端第一次通讯的时候,Client端会想服务端发送版本认证信息,协议格式如下:

	+----+----------+----------+
	|VER | NMETHODS | METHODS  |
	+----+----------+----------+
	| 1  |    1     | 1 to 255 |
	+----+----------+----------+
  • VER是SOCKS版本,目前版本是0x05;
  • NMETHODS是METHODS部分的长度;
  • METHODS是Client端支持的认证方式列表,每个方法占1字节。当前的定义是:
    • 0x00 不需要认证
    • 0x01 GSSAPI
    • 0x02 用户名、密码认证
    • 0x03 - 0x7F由IANA分配(保留)
    • 0x80 - 0xFE为私人方法保留
    • 0xFF 无可接受的方法

Server端从Client端提供的方法中选择一个并通过以下消息通知Client端:

	+----+--------+
	|VER | METHOD |
	+----+--------+
	| 1  |   1    |
	+----+--------+
  • VER是SOCKS版本,目前是0x05;
  • METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,Client端需要关闭连接。

SOCKS5请求协议格式:

	+----+-----+-------+------+----------+----------+
	|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
	+----+-----+-------+------+----------+----------+
	| 1  |  1  | X'00' |  1   | Variable |    2     |
	+----+-----+-------+------+----------+----------+
  • VER是SOCKS版本,目前是0x05;
  • CMD是SOCK的命令码
    • 0x01表示CONNECT请求
    • 0x02表示BIND请求
    • 0x03表示UDP转发
  • RSV 0x00,保留
  • ATYP DST.ADDR类型
    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03 域名地址,DST.ADDR部分第一个字节为域名长度,剩余的内容为域名。
    • 0x04 IPv6地址,16个字节长度。
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

Server端按以下格式返回给Client端:

	+----+-----+-------+------+----------+----------+
	|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
	+----+-----+-------+------+----------+----------+
	| 1  |  1  | X'00' |  1   | Variable |    2     |
	+----+-----+-------+------+----------+----------+
  • VER是SOCKS版本,目前是0x05;
  • REP应答字段
    • 0x00 表示成功
    • 0x01 普通SOCKSServer端连接失败
    • 0x02 现有规则不允许连接
    • 0x03 网络不可达
    • 0x04 主机不可达
    • 0x05 连接被拒
    • 0x06 TTL超时域名地址
    • 0x07 不支持的命令
    • 0x08 不支持的地址类型
    • 0x09 - 0xFF未定义
  • RSV 0x00,保留
  • ATYP BND.ADDR类型
    • 0x01 IPv4地址,DST.ADDR部分4字节长度
    • 0x03 域名地址,DST.ADDR部分第一个字节为域名长度,剩余的内容为域名。
    • 0x04 IPv6地址,16个字节长度。
  • BND.ADDR Server端绑定的地址
  • BND.PORT 网络字节序表示的Server端绑定的端口

用go实现socks5

package main

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"strconv"
	"strings"
)
const (
	IPV4ADDR = uint8(1) //ipv4地址
	DNADDR   = uint8(3) //域名地址
	IPV6ADDR = uint8(4) //地址
	CONNECTCMD = uint8(1)
	SUCCEEDED                     = uint8(0)
	NETWORKUNREACHABLE            = uint8(3)
	HOSTUNREACHABLE               = uint8(4)
	CONNECTIONREFUSED             = uint8(5)
	COMMANDNOTSUPPORTED           = uint8(7)
)

type Addr struct {
	Dn   string
	IP   net.IP
	Port int
}
func (a Addr) Addr() string {
	if 0 != len(a.IP) {
		return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port))
	}
	return net.JoinHostPort(a.Dn, strconv.Itoa(a.Port))
}


type Command struct {
	Version      uint8
	Command      uint8
	RemoteAddr   *Addr
	DestAddr     *Addr
	RealDestAddr *Addr
	reader       io.Reader
}

func auth(conn io.ReadWriter) error {
	header := make([]byte,2)
	_,err:= io.ReadFull(conn,header)
	if err!=nil {
		return err
	}
	//igonre check version
	methods := make([]byte,int(header[1]))
	_,err = io.ReadFull(conn,methods)
	if err!=nil {
		return err
	}
	_,err = conn.Write([]byte{uint8(5), uint8(0)}) // 返回协议5,不需要认证
	return err
}


func readAddr(r io.Reader) (*Addr, error) {
	d := &Addr{}
	addrType := []byte{0}
	if _, err := r.Read(addrType); err != nil {
		return nil, err
	}
	switch addrType[0] {
	case IPV4ADDR:
		addr := make([]byte, 4)
		if _, err := io.ReadFull(r, addr); err != nil {
			return nil, err
		}
		d.IP = net.IP(addr)
	case IPV6ADDR:
		addr := make([]byte, 16)
		if _, err := io.ReadFull(r, addr); err != nil {
			return nil, err
		}
		d.IP = net.IP(addr)

	case DNADDR:
		if _, err := r.Read(addrType); err != nil {
			return nil, err
		}
		addrLen := int(addrType[0])
		DN := make([]byte, addrLen)
		if _, err := io.ReadFull(r, DN); err != nil {
			return nil, err
		}
		d.Dn = string(DN)

	default:
		return nil, errors.New("unkown addr type")
	}

	port := []byte{0, 0}
	if _, err := io.ReadFull(r, port); err != nil {
		return nil, err
	}
	d.Port = (int(port[0]) << 8) | int(port[1])
	return d, nil
}


func request(conn io.ReadWriter) (*Command, error)  {
	header := []byte{0, 0, 0}
	if _, err := io.ReadFull(conn, header); err != nil {
		return nil, err
	}
	//igonre check version
	dest, err := readAddr(conn)
	if err != nil {
		return nil, err
	}
	cmd := &Command{
		Version:  uint8(5),
		Command:  header[1],
		DestAddr: dest,
		reader:   conn,
	}

	return cmd, nil
}

func replyMsg(w io.Writer, resp uint8, addr *Addr) error {
	var addrType uint8
	var addrBody []byte
	var addrPort uint16
	switch {
	case addr == nil:
		addrType = IPV4ADDR
		addrBody = []byte{0, 0, 0, 0}
		addrPort = 0

	case addr.Dn != "":
		addrType = DNADDR
		addrBody = append([]byte{byte(len(addr.Dn))}, addr.Dn...)
		addrPort = uint16(addr.Port)

	case addr.IP.To4() != nil:
		addrType = IPV4ADDR
		addrBody = []byte(addr.IP.To4())
		addrPort = uint16(addr.Port)

	case addr.IP.To16() != nil:
		addrType = IPV6ADDR
		addrBody = []byte(addr.IP.To16())
		addrPort = uint16(addr.Port)

	default:
		return errors.New("format address error")
	}
	bodyLen := len(addrBody)
	msg := make([]byte, 6+bodyLen)
	msg[0] = uint8(5)
	msg[1] = resp
	msg[2] = 0 // RSV
	msg[3] = addrType
	copy(msg[4:], addrBody)
	msg[4+bodyLen] = byte(addrPort >> 8)
	msg[4+bodyLen+1] = byte(addrPort & 0xff)
	_, err := w.Write(msg)
	return err
}

func handleSocks5(conn io.ReadWriteCloser) error {
	if err := auth(conn);err !=nil {
		return err
	}
	cmd,err := request(conn)
	if err !=nil {
		return  err
	}
	fmt.Printf("%v",cmd.DestAddr)
	if err := handleCmd( cmd, conn); err != nil {
		return err
	}
	return nil
}

func handleCmd(cmd *Command, conn io.ReadWriteCloser) error {
	dest := cmd.DestAddr
	if dest.Dn != "" {
		addr, err := net.ResolveIPAddr("ip", dest.Dn)
		if err != nil {
			if err := replyMsg(conn, HOSTUNREACHABLE, nil); err != nil {
				return err
			}
			return err
		}
		dest.IP = addr.IP
	}

	cmd.RealDestAddr = cmd.DestAddr
	switch cmd.Command {
	case CONNECTCMD:
		return handleConn(conn, cmd)
	default:
		if err := replyMsg(conn, COMMANDNOTSUPPORTED, nil); err != nil {
			return err
		}
		return errors.New("Unsupported command")
	}
}

func handleConn(conn io.ReadWriteCloser, req *Command) error {
	target, err := net.Dial("tcp", req.RealDestAddr.Addr())
	if err != nil {
		msg := err.Error()
		resp := HOSTUNREACHABLE
		if strings.Contains(msg, "refused") {
			resp = CONNECTIONREFUSED
		} else if strings.Contains(msg, "network is unreachable") {
			resp = NETWORKUNREACHABLE
		}
		if err := replyMsg(conn, resp, nil); err != nil {
			return err
		}
		return errors.New(fmt.Sprintf("Connect to %v failed: %v", req.DestAddr, err))
	}
	defer target.Close()

	local := target.LocalAddr().(*net.TCPAddr)
	bind := Addr{IP: local.IP, Port: local.Port}
	if err := replyMsg(conn, SUCCEEDED, &bind); err != nil {
		return err
	}
	go io.Copy(target, req.reader)
	io.Copy(conn, target)
	return nil
}
func main()  {
	l,err := net.Listen("tcp",":8090")
	if err !=nil {
		log.Fatal(err)
	}
	for {
		conn, err := l.Accept()
		if err != nil {
			log.Fatal(err)
		}
		handle := func(conn net.Conn) {
			handleSocks5(conn)
		}
		go handle(conn)
	}
}
$ go run main.go
&{ 180.101.49.12 443} ## 使用curl出现
$ export ALL_PROXY=socks5://127.0.0.1:8090 # 设置终端代理
$ curl https://www.baidu.com
<!DOCTYPE html>
...

上面的例子中我们,使用了linux的终端代理,这边隐藏了一个细节就是,终端代理实践上有socks5 client,所以我们并没有自己拼client相关的二进制协议。 linux的终端只支持http,https。像ssh协议这需要自己去实现client协议,有兴趣的同学可以自己去实现下,这不不再赘述。

总结

本小节介绍了socks5的协议,以及实现了socks代理server。socks5其实不仅仅支持tcp还有支持udp。总体来说是一个简洁但功能强大的协议。