gohop VPN源码解析

gohop是Github上用Go语言实现的VPN中Star数前3的一个实现,主要代码在2014年完成的。目前还不支持Mac/Windows(这样直接就把使用范围限制在了程序员圈子)。按项目介绍,本身就是为了翻墙设计的。有如下这些特性:

源码结构

数据包格式

UDP协议包(源码里叫HotPackt)编码方式如下,data是承载的Tun设备的IP数据包,noise是噪音数据:

+——————————————————+——————+———————+
| hopPacket header | data | noise |
+——————————————————+——————+———————+

其中的header主要是存放data的长度、协议包的类型等。

Seq字段的处途在于客户端/服务端接收包的缓冲区是按协议包的Seq由大到小排序的,优先处理Seq大的数据包,而Seq是由发起端递增产生的,也就是说优先处理后发出的数据包。

而Sid则是由客户端启动时随机生成再发送给服务端作为唯一标识的,另外还在PING/HANDSHAKE包里使用作为data字段内容但并没有任何实际用处:

type hopPacketHeader struct {
Flag byte //类型
Seq uint32
Sid uint32 //会话ID
Dlen uint16 //发送前要设置
}

协议包最后会被整个加密再进行传输,加密过程:

| data | ——> snappy compress ——> | compressed data | ——> padding ——> CBC encrypt ——> | IV | encrypt data |

解包代码如下:

func unpackHopPacket(b []byte) (*HopPacket, error) {
iv := b[:cipherBlockSize]
ctext := b[cipherBlockSize:]
if frame := cipher.decrypt(iv, ctext); frame != nil {
buf := bytes.NewBuffer(frame)

p := new(HopPacket)
binary.Read(buf, binary.BigEndian, &p.hopPacketHeader)
p.payload = make([]byte, p.Dlen)
buf.Read(p.payload)
return p, nil
} else {
return nil, errors.New("Decrypt Packet Error")
}

}

客户端

创建多条连接到服务端,对每个服务端端口创建一个连接

for port := cfg.HopStart; port <= cfg.HopEnd; port++ {
server := fmt.Sprintf("%s:%d", cfg.Server, port)
go hopClient.handleUDP(server)
}

客户端的状态如下,当建立连接之后状态就是INIT:

HOP_STAT_INIT      int32 = iota // initing
HOP_STAT_HANDSHAKE // handeshaking
HOP_STAT_WORKING // working
HOP_STAT_FIN // finishing

连接建立后客户端发送PING包,然后发送HANDSHAKE握手包,5秒超时则更发送,一直重试直到握手成功(即收到握手回应后)。发送出握手包后状态即为HOP_STAT_HANDSHAKE。

但是handshakeDone chan是共用的,假设与服务端建立100个连UDP连接只要有一个回应了HANDSHAKE成功的消息,就是成功,但实际上并不能保证所有的端口都是可达,这样有可能会出现有端口被封但还在客户端使用该端口转发数据包的情况:

go func() {
for {
clt.knock(udpConn)
n := mrand.Intn(1000)
time.Sleep(time.Duration(n) * time.Millisecond)
clt.handeshake(udpConn)
select {
case <-clt.handshakeDone:
return
case <-time.After(5 * time.Second):
logger.Debug("Handshake timeout, retry")
}
}
}()

对服务端下发的PING包的回应没有做任何处理:

// heartbeat ack
func (clt *HopClient) handleKnockAck(u *net.UDPConn, hp *HopPacket) {
return
}

收到的服务端的HANDSHAKE包的data格式:

| version(1 bytes) | IP(4 bytes) | CIDR range(1 bytes) |

收到服务端的HANDSHAKE确认包后,设置本地Tun的IP地址、更新状态,要注意最后还发起出一个HANDSHAKE确认包给到服务端,这时客户端的状态切换为HOP_STAT_WORKING,也就是可以正常使用了:

proto_version := hp.payload[0]
by := hp.payload[1:6]
ipStr := fmt.Sprintf("%d.%d.%d.%d/%d",
by[0], by[1], by[2], by[3], by[4])
ip, subnet, _ := net.ParseCIDR(ipStr)

setTunIP(clt.iface, ip, subnet)
res := atomic.CompareAndSwapInt32(&clt.state,
HOP_STAT_HANDSHAKE, HOP_STAT_WORKING)

close(clt.handshakeDone)
clt.toServer(u,
HOP_FLG_HSH|HOP_FLG_ACK, clt.sid[:], true)

也有可能HANDSHAKE在服务端失败了,这时会收到HOP_FLG_HSH | HOP_FLG_FIN的包,这里直接关掉了handshakeError chan,这会导致客户端的退出:

func (clt *HopClient) handleHandshakeError(
u *net.UDPConn, hp *HopPacket) {
close(clt.handshakeError)
}

//主循环一直等待任何一个连接的HANDSHAKE成功
wait_handshake:
for {
select {
case <-hopClient.handshakeDone:
break wait_handshake
case <-hopClient.handshakeError:
//失败整个客户端会退出
return errors.New("Handshake Fail")

某条连接在收到HANDSHAKE确认包后,即为可用的状态,除了要转发数据包,还要通过心跳包保持连接:

go func() {
for {
time.Sleep(intval)
if clt.state == HOP_STAT_WORKING {
clt.knock(udpConn)
}
}
}()

1.设置到服务端的路由不要走Tun

if clt.cfg.Redirect_gateway && (!clt.cfg.Local) {
if atomic.CompareAndSwapInt32(
&clt.srvRoute, 0, 1) {
if udpAddr, ok :=
udpConn.RemoteAddr().(*net.UDPAddr); ok {
srvIP := udpAddr.IP.To4()
if srvIP != nil {
srvDest := srvIP.String() + "/32"
addRoute(srvDest, net_gateway, net_nic)
clt.routes = append(clt.routes, srvDest)
}
}
}
}

2.设置默认路由为Tun的另一个IP

if cfg.Redirect_gateway {
go func() {
<-routeDone
err = redirectGateway(iface.Name(),
tun_peer.String())
}()
}
读取Tun并转发:
|Tun| —> |Client| —>udp—> |Server|

接收服务端Data类型数据包并转发:
|Server| —>udp—> |Client| —> |Tun|

接收服务端非Data类型数据包并回复:
|Server| —>udp—> |Client| —>udp—> |Server|

1.读取Tun并转发

从Tun设备读取到IP包,封装成HotPacket格式,其中IP包放到了HotPacket的data里,然后放到待发送的队列toNet chan里:

for {
n, err := clt.iface.Read(frame)
copy(buf[HOP_HDR_LEN:], frame[:n])
hp := new(HopPacket)
//hp.Flag不设置是因为0就是Data的Flag类型
hp.payload = buf[HOP_HDR_LEN:]
hp.Seq = clt.Seq()
clt.toNet <- hp
}

然后每个UDP连接都会从toNet chan中取出待发送的HotPacket进行发送:

go func() {
for {
hp := <-clt.toNet
hp.setSid(clt.sid)
udpConn.Write(hp.Pack())
}
}()

2.接收服务端Data类型数据包并转发到Tun

客户端跟据Flag来区分数据包的类型并用不同的处理函数去处理:

pktHandle := map[byte](func(*net.UDPConn, *HopPacket)){
HOP_FLG_HSH | HOP_FLG_ACK: clt.handleHandshakeAck,
HOP_FLG_HSH | HOP_FLG_FIN: clt.handleHandshakeError,
HOP_FLG_PSH: clt.handleHeartbeat,
HOP_FLG_PSH | HOP_FLG_ACK: clt.handleKnockAck,
HOP_FLG_DAT: clt.handleDataPacket,
HOP_FLG_DAT | HOP_FLG_MFR: clt.handleDataPacket,
HOP_FLG_FIN | HOP_FLG_ACK: clt.handleFinishAck,
HOP_FLG_FIN: clt.handleFinish,
}

调用过程:

buf := make([]byte, IFACE_BUFSIZE)
for {
n, err := udpConn.Read(buf)
hp, err := unpackHopPacket(buf[:n])

handle_func, ok := pktHandle[hp.Flag]
handle_func(udpConn, hp)
}

Data类型的数据包的处理:

func (clt *HopClient) handleDataPacket(
u *net.UDPConn, hp *HopPacket) {
clt.recvBuf.Push(hp)
}

recvBuf的最终目的地是toIface,并最终写入Tun设备

hopClient.recvBuf = newHopPacketBuffer(hopClient.toIface)

func (clt *HopClient) handleInterface() {
go func() {
for {
hp := <-clt.toIface
_, err := clt.iface.Write(hp.payload)
}
}()

3.接收服务端非Data类型数据包并回复

同上2所示,客户端跟据Flag来区分数据包的类型并用不同的处理函数去处理,对于非Data类型的数据包,不会写入到Tun中,而是跟据不同的功用做不同的处理。

比如服务端发心跳包,会回应确认包:

func (clt *HopClient) handleHeartbeat(
u *net.UDPConn, hp *HopPacket) {
logger.Debug("Heartbeat from server")
clt.toServer(u, HOP_FLG_PSH|HOP_FLG_ACK, clt.sid[:], true)
}

比如服务端对KNOCK(即PING)回应,是直接忽略:

func (clt *HopClient) handleKnockAck(
u *net.UDPConn, hp *HopPacket) {
return
}

服务端

服务端要做的事是维护所有的VPN客户端节点及其状态。gohop服务端要维护一个C类IP地址池,记录了VPN节点源地址(包括多个UDP来源地址)、会话ID、运行状态等信息的并由会话ID/源IP索引的Peer字典。

type HopServer struct {

peers map[uint64]*HopPeer
ippool *hopIPPool
}

type HopPeer struct {
ip net.IP
_addrs_lst []*hUDPAddr
state int32

}

服务端要将接收到的DATA协议包转发到Tun设备,将读取到的Tun设备的数据包(IP层数据包)转发至某个VPN客户端节点。处理流程示意:

读取Tun并转发:
[Server Tun] —> [fromIface buf] —> [toNet buf]—>udp—> [Client]

接收客户端节点的Data类型协议包并转发:
[Client] —>udp—> [fromNet buf] —> [toIface buf] —> [Server Tun]

接收客户端节控制类型协议包并回复:
[Client] —>udp—> [fromNet buf] —>udp—> [Client]

iface, err := newTun("")

hopServer.iface = iface
ip, subnet, err := net.ParseCIDR(cfg.Addr)
err = setTunIP(iface, ip, subnet)

hopServer.ipnet = &net.IPNet{ip, subnet.Mask}
hopServer.ippool.subnet = subnet

for {
n, err := iface.Read(buf)

hpbuf := make([]byte, n+HOP_HDR_LEN)
copy(hpbuf[HOP_HDR_LEN:], buf[:n])
hopServer.fromIface <- hpbuf
}

跟据目标IP地址找到对应的Peer(代表一个客户端VPN节点),通过这个peer所在的channel index(每一个创建的server socket作为一个channel,一个Peer握手时用的是哪个channel,就一直使用该channel)发送UDP数据包,发送给客户端的UDP地址使用的是已记录的该Peer的UDP地址中的任意一个:

go hopServer.forwardFrames()

func (srv *HopServer) forwardFrames() {
for {
select {
case pack := <-srv.fromIface:
frame := pack[HOP_HDR_LEN:]
dest := waterutil.IPv4Destination(frame).To4()
mkey := ip4_uint64(dest)

if hpeer, found := srv.peers[mkey]; found {
hp := new(HopPacket)
hp.Flag = HOP_FLG_DAT
hp.buf = pack
hp.payload = pack[HOP_HDR_LEN:]
hp.Seq = hpeer.Seq()

addr, idx, ok := hpeer.addr()
upacket := &udpPacket{addr, hp.Pack(), idx}
srv.toNet[idx] <- upacket
}

toNet的队列是创建服务端UDP端口时创建的,通过Peer结构保存点节VPN的来源地址和所达的server socket,这都是为了回复能拿回源地址:

toNet := make(chan *udpPacket, srv._chanBufSize)
srv.toNet[idx] = toNet

go func() {
for {
packet := <-toNet
udpConn.WriteTo(packet.data, packet.addr)
}
}()

如果使用TCP,连接本身就包含两端节点的信息,就不需要在应用层存储这地址/channel信息。如果能在UDP之上,应用之下,实现类似TCP会话的概念,这里的代码就简洁多了,而且解耦后协议不再受限于UDP,实际上这里使用KCP协议最好不过

从UDP监听端口收包放到fromNet队列中:

for {
var plen int
packet := new(udpPacket)
packet.channel = idx
buf := make([]byte, IFACE_BUFSIZE)
plen, packet.addr, err = udpConn.ReadFromUDP(buf)
packet.data = buf[:plen]

srv.fromNet <- packet
}

跟据协议包类型转到相应处理函数,注意这里有解密/解协议包的过程(解包的实现并没有足够的字段和严格的逻辑去判断数据包是不是伪造的,只是拿FLAG字段去判断有没有正确的处理函数,防止伪造包更依赖于加密+压缩算法解码失败于否):

for {
select {
case pack := <-srv.fromIface:

case packet := <-srv.fromNet:
hPack, err := unpackHopPacket(packet.data)
handle_func, ok := srv.pktHandle[hPack.Flag]
handle_func(packet, hPack)
}
}

对于DATA类型数据包的处理:

func (srv *HopServer) handleDataPacket(
u *udpPacket, hp *HopPacket) {

sid := uint64(hp.Sid)
sid = (sid << 32) & uint64(0xFFFFFFFF00000000)

if hpeer, ok := srv.peers[sid];
ok && hpeer.state == HOP_STAT_WORKING {

hpeer.recvBuffer.Push(hp)
hpeer.lastSeenTime = time.Now()
}
}

recvBuffer是跟服务端的toIface关联到一起的,最终会被写入Tun:

hpeer := new(HopPeer)
hpeer.recvBuffer = newHopPacketBuffer(srv.toIface)

控制类协议包包括PING、HANDSHAKE(握手)、FIN(客户端主动退出)等。

PING(也叫knock)的处理:

服务端收到PING包之后就立即记录这个VPN节点的地址并放到Peer字典里。一方面是由于客户端的每个连接都会发起PING再HANDSHAKE的请求,只要一个HANDSHAKE成功状态就切换成HOP_STAT_WORKING了,所以这些多余的PING,无非是用来通知服务端添加该节点的客户端的可用地址。

func (srv *HopServer) handleKnock(u *udpPacket, hp *HopPacket) {
sid := uint64(binary.BigEndian.Uint32(hp.payload[:4]))
sid = (sid << 32) & uint64(0xFFFFFFFF00000000)

hpeer, ok := srv.peers[sid]
if !ok {
hpeer = newHopPeer(sid, srv, u.addr, u.channel)
srv.peers[sid] = hpeer
} else {
hpeer.insertAddr(u.addr, u.channel)
}

HANDSHAKE的处理:

服务端收到一个HANDSHAKE请求,即为该VPN节点分配一个IP,并且状态也更新为HOP_STAT_HANDSHAKE,回复并再等待客户端对此确认包的一个确认才能完成整个握手。

func (srv *HopServer) handleHandshake(u *udpPacket, hp *HopPacket) {
sid := uint64(binary.BigEndian.Uint32(hp.payload[:4]))
sid = (sid << 32) & uint64(0xFFFFFFFF00000000)
hpeer, ok := srv.peers[sid]

cltIP, err := srv.ippool.next()
hpeer.ip = cltIP.IP.To4()
mask, _ := cltIP.Mask.Size()
buf := bytes.NewBuffer(make([]byte, 0, 8))
buf.WriteByte(HOP_PROTO_VERSION)
buf.Write([]byte(hpeer.ip))
buf.WriteByte(byte(mask))
key := ip4_uint64(hpeer.ip)
srv.peers[key] = hpeer

atomic.StoreInt32(&hpeer.state, HOP_STAT_HANDSHAKE)
srv.toClient(hpeer,
HOP_FLG_HSH|HOP_FLG_ACK, buf.Bytes(), true)

收到客户端节点对确认包的确认后,这个节节即可用了,状态变为HOP_STAT_WORKING:

func (srv *HopServer) handleHandshakeAck(
u *udpPacket, hp *HopPacket) {
sid := uint64(binary.BigEndian.Uint32(hp.payload[:4]))
sid = (sid << 32) & uint64(0xFFFFFFFF00000000)
hpeer, ok := srv.peers[sid]

if ok = atomic.CompareAndSwapInt32(
&hpeer.state,
HOP_STAT_HANDSHAKE,
HOP_STAT_WORKING); ok {

hpeer.hsDone <- struct{}{}
}

FIN的处理:

客户端在退出程序前的清理工作包括发送FIN给服务端,这里服务端主动删掉此节点:

func (srv *HopServer) handleFinish(
u *udpPacket, hp *HopPacket) {
sid := uint64(binary.BigEndian.Uint32(hp.payload[:4]))
sid = (sid << 32) & uint64(0xFFFFFFFF00000000)
srv.deletePeer(sid)
}

除此之外,服务端定期检查每个节点的最近活跃时间(PING/DATA包的最近到达时间),超过一定间隔未收到消息就会将客户端节点踢出:

func (srv *HopServer) peerTimeoutWatcher() {
timeout := time.Second *
time.Duration(srv.cfg.PeerTimeout)
interval := time.Second *
time.Duration(srv.cfg.PeerTimeout/2)

for {

time.Sleep(interval)

time.Sleep(interval)
for sid, hpeer := range srv.peers {
conntime := time.Since(hpeer.lastSeenTime)
if conntime > timeout {
go srv.kickOutPeer(sid)
}
}
}
}

项目可优化的地方

  1. 发数据包发送改成通用模块,并维护两端状态,以解耦让发送协议可以任意替换成TCP/KCP/HTTP2等
  2. 删除无用代码(Fragmentation的部分)和注释
  3. 将设置路由、Tun的代码抽出,最好是合并到water模块