小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

sock5代理学习与使用

阳春三月,春暖花开,春节的颓废带来的长尾效应也该终结了。作为一个程序员,平时免不了跨越某墙去寻找优秀资源,利用公司的vpn或者自己买vpn总会有些限制,所以最好的是掌握其原理并玩转它。因此我好好的学习了下sock5代理的实现。学习途径很简单:

  1. 阅读sock5协议文档:http://www.faqs.org/rfcs/rfc1928.html
  2. 阅读开源实现:shadowsocks-go
  3. 一步一步实现一个简易的版本:shine(GO语言实现)

现在大多数浏览器或者应用程序都是支持sock5协议的。例如浏览器,可以通过配置pac或者switchy来让请求走sock5,例如使用switchy来配置:

这样请求就会以sock5协议走127.0.0.1:8001。接下来就是在监听127.0.0.1:8001的请求(client),然后建立一个到墙外的主机(server)的连接,并把请求转交给server进行处理,server端获取到数据后就回传给client,最后client将数据交还给浏览器。整个逻辑是很清楚的,接下来就看client和server是如何实现的。

首先看client,client的职责就是监听sock端口,然后按照sock5协议解读请求并将请求交给server端,最后将server端返回的数据写回浏览器。

监听sock端口很简单:

ln, err := net.Listen("tcp", client)
if err != nil {
    log.Fatal(err)
}
log.Printf("starting listen local sock5 at %v ...\n", client)
for {
    conn, err := ln.Accept()
    if err != nil {
        log.Println("accept:", err)
            continue
    }
    // 每次收到一个请求,就开一个协程单独处理
    go handleConnection(conn)
}

按照sock协议,我们需要通过三步来处理每一个请求。

第一步,建立连接:

//  步骤一:
// +----+----------+----------+
// |VER | NMETHODS | METHODS  |
// +----+----------+----------+
// | 1  |    1     | 1 to 255 |
// +----+----------+----------+
func handShake(con net.Conn) (err error) {
    const (
        idVer     = 0
        idNMethod = 1
    )
    shine.SetReadTimeout(con)
    // 目前sock协议最多256个方法, 加上ver和nmethod,最多需要258个字节
    buf := make([]byte, 258)
    var n int
    if n, err = io.ReadAtLeast(con, buf, idNMethod+1); err != nil {
        return
    }

    if buf[idVer] != socksVer5 {
        return errVer
    }

    nmethod := int(buf[idNMethod])
    msgLen := nmethod + 2
    //...
    // send confirmation: version 5, no authentication required
    _, err = con.Write([]byte{socksVer5, 0})
    return
}

这些操作就是读请求的字节进行处理,然后每个字节的含义都是由socks5协议所规定,比如VER占1个字节,表示sock版本号,NMETHOD占1个字节,指示后面的method的位置,METHOD可能是1-255位,具体位置由NMETHOD决定,最后将[]byte{socksVer5, 0}写回浏览器,协议达成。一旦协议达成,浏览器就会发送请求详情。我们此时就可以需要获取请求信息。

第二步,获取请求信息:

这一步我们依旧要看看协议是如何的:

  
+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • VER 协议版本: X’05’
  • CMD:
    • CONNECT:X’01’
    • BIND:X’02’
    • UDP ASSOCIATE:X’03’
  • RSV 保留
  • ATYP 后面的地址类型
  • IPV4:X’01’
  • 域名:X’03’
  • IPV6:X’04’'
  • DST.ADDR 目的地址
  • DST.PORT 以网络字节顺序出现的端口号

通过ATTR后后面的信息,我们可以解析出请求地址:

rawAddress = buf[idAtyp:reqLen]

switch buf[idAtyp] {
case typeIPv4:
    address = net.IP(buf[idIP0 : idIP0+net.IPv4len]).String()
case typeIPv6:
    address = net.IP(buf[idIP0 : idIP0+net.IPv6len]).String()
case typeDm:
    address = string(buf[idDm0 : idDm0+buf[idDmLen]])
}
port := binary.BigEndian.Uint16(buf[reqLen-2 : reqLen])
address = net.JoinHostPort(address, strconv.Itoa(int(port)))

// 然后我们需要按照协议将以下信息写回浏览器,当然数据不一定要真实:
// +----+-----+-------+------+----------+----------+
// |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1  |  1  | X'00' |  1   | Variable |    2     |
// +----+-----+-------+------+----------+----------+
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})

第三步,将请求发送到server端,然后接受返回的数据

remote, err := connectToServer(rawAddress, address)
if err != nil {
    log.Println(err)
    return
}
defer func() {
    if !closed {
        remote.Close()
    }
}()
go shine.PipeThenClose(conn, remote)
shine.PipeThenClose(remote, conn)

connectToServer发送的信息是通过AES加密过的,服务端只有成功的解密请求后才会去处理请求。至此client的整体流程就是这样了。

server端的工作就是接受客户端的请求,然后解析并执行请求,最后传回client数据。首先还是监听端口:

func run(config *shine.Config) {
    port := config.ServerPort
    ln, err := net.Listen("tcp", ":"+strconv.Itoa(port))
    if err != nil {
        log.Fatalf("error listening port %d: %v\n", port, err)
    }
    var cipher *shine.Cipher
    for {
        conn, err := ln.Accept()
        if err != nil {
            debug.Printf("accept error: %v\n", err)
            return
        }
        if cipher == nil {
            cipher, err = shine.NewCipher(config.Method, config.Password)
            if err != nil {
                log.Printf("Error generating cipher for port: %d %v\n", port, err)
                conn.Close()
                continue
            }
        }
        go handleConnection(shine.NewConn(conn, cipher.Copy()))
    }
}

每次监听到连接请求,就开一个协程单独处理,处理流程两步就可以完成:

第一步,解析请求信息:

func getRequest(conn *shine.Conn) (address string, err error) {
    shine.SetReadTimeout(conn)
    // client写入过来的是: rawAddress (idType + IP/Domain) + iv
    // 16  1(addrType) + 1(lenByte) + 255(max length address) + 2(port)
    buf := make([]byte, 259)

    // ReadFull会调用到conn.Read
    // shine.Conn重写了Read方法
    if _, err = io.ReadFull(conn, buf[:idType+1]); err != nil {
        return
    }
    var reqStart, reqEnd int
    addressType := buf[idType]
    switch addressType {
    case typeIPv4:
        reqStart, reqEnd = idIP0, idIP0+lenIPv4
    case typeIPv6:
        reqStart, reqEnd = idIP0, idIP0+lenIPv6
    case typeDm:
        if _, err = io.ReadFull(conn, buf[idType+1:idDmLen+1]); err != nil {
            return
        }
        reqStart, reqEnd = idDm0, idDm0+int(buf[idDmLen])+lenPort
    default:
        err = fmt.Errorf("addr type %d not supported", addressType)
        return
    }
    if _, err = io.ReadFull(conn, buf[reqStart:reqEnd]); err != nil {
        return
    }
    switch addressType {
    case typeIPv4:
        address = net.IP(buf[idIP0: idIP0+net.IPv4len]).String()
    case typeIPv6:
        address = net.IP(buf[idIP0: idIP0+net.IPv6len]).String()
    case typeDm:
        address = string(buf[idDm0: idDm0+int(buf[idDmLen])])
    }

    port := binary.BigEndian.Uint16(buf[reqEnd-2: reqEnd])
    address = net.JoinHostPort(address, strconv.Itoa(int(port)))
    return
}

这里的请求的解析需要遵循客户端发送请求的协议。其次要注意的是Conn是我们自己建立的struct,负责处理加密解密的事情,会覆写net.ConnReadWrite方法。

获取到请求后,我们就能够拿到client真正想要访问的地址,然后我们就可以向目标地址发送TCP请求,这是第二步要做的事。

第二步,建立到目标地址的TCP链接,然后将client的请求信息发送给目标地址,并将返回的数据写回client

remote, err := net.Dial("tcp", address)
if err != nil {
    if ne, ok := err.(*net.OpError); ok && (ne.Err == syscall.EMFILE || ne.Err == syscall.ENFILE) {
        // log too many open file error
        // EMFILE is process reaches open file limits, ENFILE is system limit
        log.Println("dial error:", err)
    } else {
        log.Println("error connecting to:", remote, err)
    }
    return
}
defer func() {
    if !closed {
        remote.Close()
    }
}()
go shine.PipeThenClose(conn, remote)
shine.PipeThenClose(remote, conn)

这样整个代理就搭建起来了。当然整个过程还涉及到加密解密的问题,这是对AES等相关知识的学习与应用,不过与今天的主题无关,具体的实现看代码就可以了。

←支付宝← →微信 →