p2p原理初探

背景

机缘巧合5月朋友在做向日葵远程桌面的API调试遇到窗口创建后,无法显示对方桌面图像问题,直到现在这个问题依然没能处理,但远程连接却建立成功。看了一下日志发现远程桌面的原理是P2P打洞,记一个流水帐。分为以下几个部分

  1. 向日葵远程桌面API
  2. P2P打洞
  3. 延申

向日葵远程桌面

向日葵远程桌面和teamViewer一样都是远程的软件, 相比速度没那么快,胜在免费和广告少,之前在做ETC停车场项目中有用到。

向日葵是有上海贝锐公司开发,它家还有广为人知的内外穿透花生棒和蒲公英路由器管理软件。可以在http://developer.oray.com申请成为开发者,并下载sdk。一个远程桌面的接口如下
![远程桌面](

)
如上,被控制端根据app id和secret调用SLCreateRemote向Oray服务器请求,创建一个地址和session;主动控制端根据地址和session连接被控制端,建立连接之后两者就能进行正常的通讯。你的服务器可有可无,更多也是做些自动等级被控制端的地址和session。

主动控端调用流程如下:
SLInitialize
SLCreateRemote
SLSetReniteCallback
SLOpebRemoteLog
SLCreateRemoteEmptySession
SKSetRemoteSessionOpt
SLConnectRemoteSession
SLDestroyRemoteSession
SLDestroyRemote
SLUninitialize

初步完成了被控制端的自动创建,并返回地址和session;主控端的程序则无法显示界面,原因未知。

通过抓包看到连接是要先同故宫Oray服务器打洞,然后使用udp互相通信,而不经过服务端;此外向日葵提供sdk中nodejs调用C++也很有借鉴意义,暂时不深入。

p2p打洞

主要参考UDP打洞、P2P组网方式研究,从概念和一个简单例子来了解p2p技术。

NAT概念

网络地址转换(NAT,Network Address Translation)属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术。NAT可以分为四种类型rfc3489

先做一个约定:

内网A中有:A1(192.168.0.8)、A2(192.168.0.9)两用户
网关X1(一个NAT设备)有公网IP 1.2.3.4

内网B中有:B1(192.168.1.8)、B2(192.168.1.9)两用户,
网关Y1(一个NAT设备)有公网IP 1.2.3.5

公网服务器:C (6.7.8.9) D (6.7.8.10)

  • Full Cone: 这种NAT内部的机器A连接过外网机器C后,NAT会打开一个端口,然后外网的任何发到这个打开的端口的UDP数据报都可以到达A。不管是不是C发过来的(NAT源端口映射),即从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000 ,192.168.0.8可以收到任意外部主机发到1.2.3.4:62000的数据报。
  • Restricted Cone: 这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口,然后C可以用任何端口和A通信,其他的外网机器不行(目的IP映射) ,从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先给服务器C 6.7.8.9发送一个数据报后,192.168.0.8才能收到6.7.8.9发送到1.2.3.4:62000的数据报。
  • Port Restricted Cone: 这种NAT内部的机器A连接过外网的机器C后,NAT打开一个端口,然后C可以用原来的端口和A通信,其他的外网机器不行(NAT目的端口映射),从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000,只有当内部主机192.168.0.8先向外部主机地址端口6.7.8.9:8000发送一个数据报后,192.168.0.8才能收到6.7.8.9:8000发送到1.2.3.4:62000的数据报
  • Symmetic: 对于这种NAT.连接不同的外部目标。原来NAT打开的端口会变化,而Cone NAT不会,虽然可以用端口猜测,但是成功的概率很小

对于NAT类型探测如下如
![NAT类型探测](

)
github上的P2P-Over-MiddleBoxes-Demo有python版本的实现,代码理解如下

STUN_SERVERS = [
    ('stun.pppan.net', 3478),
    ('stun.ekiga.net', 3478),
    ('stun.ideasip.com', 3478),
    ('stun.voipbuster.com', 3478),
    ]
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
ntype = test_nat(sock, STUN_SERVERS[0], local_ip)

def test_nat(sock, stun_server, local_ip='0.0.0.0'):
    resp = test_I(sock, stun_server) //给STUN_SERVERS第0个发消息
    if resp is None:
        return NAT.UDP_BLOCKED //没收到公网的ip,网络不通
    m1 = get_mapped_address(resp) //取到公网ip得到的发出去的ip
    changed_address = get_changed_address(resp) //得到网关ip和端口
    if m1 == local_address: //可能是个公网ip
        # we can't tell whether it's public if we don't specify the local address
        resp = test_II(sock, stun_server) //改变ip和端口
        if resp is None:
            return NAT.SYMMETRIC_UDP_FIREWALL
        return NAT.PUBLIC //一样能发送内容,则为公网
    resp = test_II(sock, stun_server) //再次改变ip和端口
    if not resp is None:
        return NAT.FULL_CONE //无法送达
    resp = test_I(sock, changed_address) //发给网关和端口
    m2 = get_mapped_address(resp)
    if m2 != m1:
        return NAT.SYMMETRIC
    resp = test_III(sock, stun_server) //不改变ip只改变端口
    if resp is None:
        return NAT.PORT_RISTRICT
    else:
        return NAT.ADDR_RISTRICT

要真正实现p2p打洞,就是要A1和B1互相给对象公网ip发一条信息,建立连接,原理如下

A1在客户机时                192.168.0.8:4000——6.7.8.9:8000

X1在网关时                   1.2.3.4:62000——6.7.8.9:8000

服务器C                       6.7.8.9:8000

B1在客户机时                192.168.1.8:4000——6.7.8.9:8000

Y1在网关时                   1.2.3.5:31000——6.7.8.9:8000

两内网用户要实现通过各自网关的直接呼叫,需要以下过程:

1、 客户机A1、B1顺利通过格子网关访问服务器C ,均没有问题(类似于登录)

2、 服务器C保存了 A1、B1各自在其网关的信息(1.2.3.4:62000、1.2.3.5:31000)没有问题。并可将该信息告知A1、B1。

3、 此时A1发送给B1网关的1.2.3.5:31000是否会被B1收到?答案是基本上不行(除非Y1设置为完全圆锥型,但这种设置非常少),因为Y1上检测到其存活的会话中没有一个的目的IP或端口于1.2.3.4:62000有关而将数据包全部丢弃!

4、 此时要实现A1、B1通过X1、Y1来互访,需要服务器C告诉它们各自在自己的网关上建立“UDP隧道”,即命令A1发送一个 192.168.0.8:4000——1.2.3.5:31000的数据报,B1发送一个192.168.1.8:4000——1.2.3.4:62000的数据报,UDP形式,这样X1、Y1上均存在了IP端口相同的两个不同会话(很显然,这要求网关为Cone NAT型,否则,对称型Symmetric NAT设置网关将导致对不同会话开启了不同端口,而该端口无法为服务器和对方所知,也就没有意义)。

测试程序

有一个服务端的server,得到连接的名字和共有私有地址,然后提供接口让所有的连接获取现有的所有连接的名字和共有地址;客户端则启动的时候,登录携带自己的名称,可取获取所有连接数,并给其他连接的公网地址发送udp请求,实现打洞。

//================================
//p2p_server.py
#!/usr/bin/python
#coding=utf-8

import socket, sys, SocketServer, threading, thread, time

SERVER_PORT = 1234
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', SERVER_PORT))
user_list = []

def server_handle():
    while True:
        cli_date, cli_pub_add = sock.recvfrom(8192)
        now_user = []
        headder = []
        cli_str = {}
        headder = cli_date.split('\t')
        for one_line in headder:
            str = {}
            str = one_line
            args = str.split(':')
            cli_str[args[0]] = args[1]
            if cli_str['type'] == 'login':
                del cli_str['type']
                now_user = cli_str
                now_user['cli_pub_ip'] = cli_pub_add[0]
                now_user['cli_pub_port'] = cli_pub_add[1]
                if len(headder) >= 4:
                    now_user['user_name'] = headder[1].split(':')[-1]
                    now_user['private_ip'] = headder[2].split(':')[-1]
                    now_user['private_port'] = headder[3].split(':')[-1]
                user_list.append(now_user)
                toclient = 'info#%s login in successful, the info from server' %now_user['user_name']
                sock.sendto(toclient, cli_pub_add)
                print('-'*100)
                print("%s 已登录,公网IP:%s 端口:%d\n" %(now_user['user_name'], now_user['cli_pub_ip'], now_user['cli_pub_port']))
                print("以下是一登陆的用户列表")
                for one_user in user_list:
                    print('用户名:%s 公网ip:%s 公网端口:%d 私网端口:%s' %(one_user['user_name'], one_user['cli_pub_ip'], one_user['cli_pub_port'], one_user['private_port']))
                break
            elif cli_str['type'] == 'alive':
                pass
            elif cli_str['type'] == 'logout':
                pass
            elif cli_str['type'] == 'getalluser':
                print('-'*100)
                for one_user in user_list:
                    toclient = 'getalluser#username:%s pub_ip:%s pub_port:%d pri_ip:%s pri_port:%s' %(one_user['user_name'], one_user['cli_pub_ip'], one_user['cli_pub_port'], one_user['private_ip'], one_user['private_port'])
                    sock.sendto(toclient, cli_pub_add)

if __name__ == '__main__':
    thread.start_new_thread(server_handle, ())

print('服务器进程已启动,等待客户连接')
while True:
    for one_user in user_list:
        toclient = 'keepconnect#111'
        sock.sendto(toclient, (one_user['cli_pub_ip'], one_user['cli_pub_port']))
        time.sleep(1)


//================================
//p2p_client.py
#coding=utf-8
import socket, SocketServer, threading, thread, time
CLIENT_PORT = 4321
SERVER_IP = "49.235.232.69"
SERVER_PORT = 1234
user_list = {}
local_ip = socket.gethostbyname(socket.gethostname())
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('', CLIENT_PORT))
to_user_ip = ''
to_user_port = ''
#sock.bind((local_ip,CLIENT_PORT))

def server_handle():
    print('客户端线程已近启动,等待其他客户端连接')
    while True:
        data, addr = sock.recvfrom(8192)
        #print('server_handle recv '+ data)
        data_str = data.split('#')
        data_type = data_str[0]
        data_info = data_str[1]
        if data_type == 'info':
            del data_str[0]
            print(data_info)
        if data_type == 'getalluser':
            data_sp = data_info.split(' ')
            user_name = data_sp[0].split(':')[1]
            del data_sp[0]
            user_list[user_name] = {}
            for one_line in data_sp:
                arg = one_line.split(':')
                user_list[user_name][arg[0]] = arg[1]
        if data_type == 'echo':
            print(data_info)
        if data_type == 'keepconnect':
            message = 'type:alive'
            sock.sendto(message, addr)
        
if __name__ == '__main__':
    thread.start_new_thread(server_handle, ())
    time.sleep(0.1)
    cmd = raw_input('输入指令>>')
    while True:
        args = cmd.split(' ')
        if args[0] == 'login':
            user_name = args[1]
            local_uname = args[1]
            address = 'private_ip:%s private_port:%d' %(local_ip, CLIENT_PORT)
            headder = 'type:login\tuser_name:%s\tprivate_ip:%s\tprivate_port:%d' %(user_name, local_ip, CLIENT_PORT)
            sock.sendto(headder, (SERVER_IP, SERVER_PORT))
        elif args[0] == 'getalluser':
            headder = 'type:getalluser\tuser_name:a1'
            sock.sendto(headder, (SERVER_IP, SERVER_PORT))
            #print('获取用户列表中...')
            time.sleep(1)
            for one_user in user_list:
                print('username:%s pub_ip:%s pub_port:%s pri_ip:%s pri_port:%s' %(one_user, user_list[one_user]['pub_ip'], user_list[one_user]['pub_port'], user_list[one_user]['pri_ip'],user_list[one_user]['pri_port']))
        elif args[0] == 'connect':
            user_name = args[1]
            to_user_ip = user_list[user_name]['pub_ip']
            to_user_port = int(user_list[user_name]['pub_port'])
        elif args[0] == 'echo':
            m = ' '.join(args[1:])
            message = 'echo#from %s:%s'%(local_uname, m)
            sock.sendto(message, (to_user_ip, to_user_port))
        time.sleep(0.1)
        cmd = raw_input('输入指令>>')

测试:发现同一个局域网内,两台主机之间通过公网ip的打洞是可能异常的(据说直接方法是,通过发私有ip和端口试试),具体原因未知;如果两个位于不同公有ip能成功

延申

p2p是现在磁力下载等的基础,还有zeroNet分布式p2p网络的存在,让p2p应用广泛;此外udp是一个越来越被重视的,比如流媒体推h264流就用udp,http3的QUIC实现也是用udp加速连接。

posted @ 2020-05-31 14:37  大有神的自我独白  阅读(1242)  评论(0)    收藏  举报