shadowsocks-go源代码分析

pinecone · · 3062 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

shadowsocks 协议

shadowsocks协议包格式为IV向量加Playload,这是加密后的结果.

+-------+----------+
|  IV   | Payload  |
+-------+----------+
| Fixed | Variable |
+-------+----------+

解密后为

+--------------+---------------------+------------------+----------+
| Address Type | Destination Address | Destination Port |   Data   |
+--------------+---------------------+------------------+----------+
|      1       |       Variable      |         2        | Variable |
+--------------+---------------------+------------------+----------+

服务端解密需要这个IV向量和原来就协商好的密钥进行解密,握手完成后后续的所有TCP 数据包不会再带上IV,而是使用握手时协商的那个IV。

源码解析

这里分析shadowsocks-server的源代码,整体流程为:

  1. 监听服务

  2. 初始化加密对象

  3. 解密客户端数据

  4. 连接远程主机

  5. 建立通讯连接

监听服务

这里根据配置文件,进行端口监听,后面就是常规的Accept()等待连接.

    for port, password := range config.PortPassword {
        go run(port, password, config.Auth)
        if udp {
            go runUDP(port, password, config.Auth)
        }
    }

初始化加密对象

当有链接来到后首先检查是否已经初始化解密对象,然后封装net.Conn.这里的cipher根据不同的加密算法实现了流加密解密XORKeyStream(dst, src []byte)接口,具体可以看golang自带标准库cipher.Stream,当然这个接口要读取iv向量后才能实现.

        //如果没有初始化,解析初始化.
        if cipher == nil {
            log.Println("creating cipher for port:", port)
            cipher, err = ss.NewCipher(config.Method, password)
            if err != nil {
                log.Printf("Error generating cipher for port: %s %v\n", port, err)
                conn.Close()
                continue
            }
        }
        //这里要根据password产生key.
        key := evpBytesToKey(password, mi.keyLen)

解密客户端数据

net.Conn被封装后Read方法读出的数据即为解密后的数据

func (c *Conn) Read(b []byte) (n int, err error) {
    //如果解密接口没有实现,读取iv向量初始化解密接口.
    if c.dec == nil {
        iv := make([]byte, c.info.ivLen)
        if _, err = io.ReadFull(c.Conn, iv); err != nil {
            return
        }
        //读取到iv向量后就可以初始化解密接口
        if err = c.initDecrypt(iv); err != nil {
            return
        }
        if len(c.iv) == 0 {
            c.iv = iv
        }
    }

    //如果读取buff大于内置buff则分配内存
    cipherData := c.readBuf
    if len(b) > len(cipherData) {
        cipherData = make([]byte, len(b))
    } else {
        cipherData = cipherData[:len(b)]
    }
    
    //读取数据后解密返回
    n, err = c.Conn.Read(cipherData)
    if n > 0 {
        c.decrypt(b[0:n], cipherData[0:n])
    }
    return
}

连接远程主机

当读取到解密的数据后就要开始解包了,这里只复制主要代码.

    //这里判断一个字节的地址类型
    addrType := buf[idType]
    //这里根据地址类型的不同读取地址
    if _, err = io.ReadFull(conn, buf[reqStart:reqEnd]); err != nil {
        return
    }
    //这里读取2个字节的端口
    port := binary.BigEndian.Uint16(buf[reqEnd-2 : reqEnd])
    //当获取到地址和端口后调用dial连接远程主机
    remote, err := net.Dial("tcp", host)

剩下可能还会有ota数据,根据ota已经被弃用了吧,这里就不再说明了,有兴趣的同学可以自己去查资料.

建立通讯连接

这里建立通讯连接有点像使用了io.Copy(),分别连接两个conn.

    //PipeThenClose有点像io.Copy
    if ota {
        go ss.PipeThenCloseOta(conn, remote)
    } else {
        go ss.PipeThenClose(conn, remote)
    }
    ss.PipeThenClose(remote, conn)
    //PipeThenClose的原理就是不断读取然后写入,直到出错.
    for{
        n, err := src.Read(buf)
        // read may return EOF with n > 0
        // should always process n > 0 bytes before handling error
        if n > 0 {
            // Note: avoid overwrite err returned by Read.
            if _, err := dst.Write(buf[0:n]); err != nil {
                Debug.Println("write:", err)
                break
            }
        }
    }

结语

这里分析了shadowsocks-server的源代码,还有shadowsocks-local的感觉大同小异就是多了一个加密的过程.


有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:pinecone

查看原文:shadowsocks-go源代码分析

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

3062 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传