LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

C#实现P2P之UDP穿透NAT的原理

admin
2021年2月2日 8:58 本文热度 4631

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:

    NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。

    最开始NAT是运行在路由器上的一个功能模块。

    最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。

    因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)

    关于基本的NAT可以参看RFC 1631

另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:

                                                                         Server S1                         
                                                            18.181.0.31:1235                          
                                                                           |
                                  ^  Session 1 (A-S1)  ^      |  
                                   |  18.181.0.31:1235  |      |   
                                  v 155.99.25.11:62000 v   |    
                                                                          |
                                                                      NAT
                                                                155.99.25.11
                                                                          |
                                  ^  Session 1 (A-S1)  ^      |  
                                   |  18.181.0.31:1235  |      |  
                                    v   10.0.0.1:1234    v      |  
                                                                          |
                                                                    Client A
                                                              10.0.0.1:1234


    有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?

    首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。

一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。

看看下面的情况:

Server S1                                     Server S2
 18.181.0.31:1235                              138.76.29.7:1235
        |                                             |
        |                                             |
        +----------------------+----------------------+
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v
                               |
                            Cone NAT
                          155.99.25.11
                               |
   ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^
   |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |
   v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v
                               |
                            Client A
                         10.0.0.1:1234

   接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?

    这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)

    好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。

    但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。

那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。

现在我们来看看一个P2P软件的流程,以下图为例:

 Server S (219.237.60.1)
     |
      |
   +----------------------+----------------------+
   |                                             |
 NAT A (外网IP:202.187.45.3)                 NAT B (外网IP:187.34.1.56)
   |   (内网IP:192.168.0.1)                      | (内网IP:192.168.0.1)
   |                                             |
Client A  (192.168.0.20:4000)             Client B (192.168.0.10:40000)

    首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。

    此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。

    总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。

    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。   

    下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。

    程序现在支持三个命令:send , getu , exit

 

    send格式:send username message

    功能:发送信息给username

 

    getu格式:getu

    功能:获得当前服务器用户列表

 

    exit格式:exit

功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)

 

源代码

注:原文代码是用C++写的,这里仅附上C#代码

1.  WellKnown公用库

namespace P2P.WellKnown

{

    using System;

    using System.IO;

using System.Runtime.Serialization.Formatters.Binary;

    /// <summary>

    /// P2PConsts 的摘要说明。

    /// </summary>

    public class P2PConsts

    {

        /// <summary>

        /// 服务器侦听端口号

        /// </summary>

public const int SRV_PORT  = 2280;

    }

        /// <summary>

    /// User 的摘要说明。

    /// </summary>

    [Serializable]

    public class User

    {

        protected string userName;

        protected IPEndPoint netPoint;

 

        public User(string UserName, IPEndPoint NetPoint)

        {

            this.userName = userName;

            this.netPoint = NetPoint;

        }

        public string UserName

        {

            get { return userName; }

        }

 

        public IPEndPoint NetPoint

        {

            get { return netPoint; }

            set { netPoint = value;}

        }

    }

    /// <summary>

    /// UserCollection 的摘要说明。

    /// </summary>

    [Serializable]

    public class UserCollection : CollectionBase

    {

        public void Add(User user)

        {

            InnerList.Add(user);

        }

 

        public void Remove(User user)

        {

            InnerList.Remove(user);

        }

 

        public User this[int index]

        {

            get { return (User)InnerList[index]; }

        }

 

        public User Find(string userName)

        {

            foreach(User user in this)

            {

                if (string.Compare(userName, user.UserName, true) == 0)

                {

                    return user;

                }

            }

            return null;

        }

    }

    /// <summary>

    /// FormatterHelper 序列化,反序列化消息的帮助类

    /// </summary>

    public class FormatterHelper

    {

        public static byte[] Serialize(object obj)

        {

            BinaryFormatter binaryF = new BinaryFormatter();

            MemoryStream ms = new MemoryStream(1024*10);

            binaryF.Serialize(ms, obj);

            ms.Seek(0, SeekOrigin.Begin);

            byte[] buffer = new byte[(int)ms.Length];

            ms.Read(buffer, 0, buffer.Length);

            ms.Close();

            return buffer;

        }

 

        public static object Deserialize(byte[] buffer)

        {

            BinaryFormatter binaryF = new BinaryFormatter();

            MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length, false);

            object obj = binaryF.Deserialize(ms);

            ms.Close();

            return obj;

        }

    }

    /// <summary>

    /// Message base class

    /// </summary>

    [System.Serializable]

    public abstract class MessageBase

    {

    }

 

    // Message from Client to Server

    namespace C2S

    {

        /// <summary>

        /// 客户端发送到服务器的消息基类

        /// </summary>

        public abstract class CSMessage : MessageBase

        {

            private string userName;

            protected CSMessage(string anUserName)

            {

                userName = anUserName;

            }

            public string UserName

            {

                get { return userName; }

            }

        }

        /// <summary>

        /// 用户登录消息

        /// </summary>

        public class LoginMessage : CSMessage

        {

            private string password;

            public LoginMessage(string userName, string password) : base(userName)

            {

                this.password = password;

            }

            public string Password

            {

                get { return password; }

            }

        }

        /// <summary>

        /// 用户登出消息

        /// </summary>

        public class LogoutMessage : CSMessage

        {

            public LogoutMessage(string userName) : base(userName)

            {}

        }

        /// <summary>

        /// 请求用户列表消息

        /// </summary>

        public class GetUsersMessage : CSMessage

        {

            public GetUsersMessage(string userName) : base(userName)

            {}

        }

        /// <summary>

        /// 请求Purch Hole消息

        /// </summary>

        public class TranslateMessage : CSMessage

        {

            protected string toUserName;

            public TranslateMessage(string userName, string toUserName) : base(userName)

            {

                this.toUserName = toUserName;

            }

            public string ToUserName

            {

                get { return this.toUserName; }

            }

        }

    }

 

    // Message from server to the client

    namespace S2C

    {

        /// <summary>

        /// 服务器发送到客户端消息基类

        /// </summary>

        public abstract class SCMessage : MessageBase

        {}

        /// <summary>

        /// 请求用户列表应答消息

        /// </summary>

        public class GetUsersResponseMessage : SCMessage

        {

            private UserCollection userList;

            public GetUsersResponseMessage(UserCollection users)

            {

                this.userList = users;

            }

            public UserCollection UserList

            {

                get { return userList; }

            }

        }

        /// <summary>

        /// 转发请求Purch Hole消息

        /// </summary>

        public class SomeOneCallYouMessage : SCMessage

        {

            protected System.Net.IPEndPoint remotePoint;

            public SomeOneCallYouMessage(System.Net.IPEndPoint point)

            {

                this.remotePoint = point;

            }

            public System.Net.IPEndPoint RemotePoint

            {

                get { return remotePoint; }

            }

        }

    }

 

    // Message from peer to the peer

    namespace P2P

    {

        /// <summary>

        /// 点对点消息基类

        /// </summary>

        public abstract class PPMessage : MessageBase

        {}

        /// <summary>

        /// 测试消息

        /// </summary>

        public class WorkMessage : PPMessage

        {

            private string message;

            public WorkMessage(string msg)

            {

                message = msg;

            }

            public string Message

            {

                get { return message; }

            }

        }

        /// <summary>

        /// 测试应答消息

        /// </summary>

        public class ACKMessage : PPMessage

        {

        }

        /// <summary>

        /// P2P Purch Hole Message

        /// </summary>

        public class TrashMessage : PPMessage

        {}

    }  

}

 

 

2.  P2Pserver

namespace P2P.P2PServer

{

    using System;

    using System.Net;

    using System.Net.Sockets;

    using System.Threading;

    using P2P.WellKnown;

    /// <summary>

    /// AppClass 的摘要说明。

    /// </summary>

    public class AppClass

    {

        public static void Main()

        {

            Server server = new Server();

            try

            {

                server.Start();

                Console.ReadLine();

                server.Stop();

            }

            catch

            {

            }

        }

    }

    /// <summary>

    /// Server 的摘要说明。

    /// </summary>

    public class Server

    {

        private UdpClient server;

        private UserCollection userList;

        private Thread serverThread;

        private IPEndPoint remotePoint;

 

        public Server()

        {

            userList = new UserCollection();

            remotePoint = new IPEndPoint(IPAddress.Any, 0);

            serverThread = new Thread(new ThreadStart(Run));

        }

 

        public void Start()

        {

            try

            {

                server = new UdpClient(P2PConsts.SRV_PORT);

                serverThread.Start();

                Console.WriteLine("P2P Server started, waiting client connect...");

            }

            catch(Exception exp)

            {

                Console.WriteLine("Start P2P Server error: " + exp.Message);

                throw exp;

            }

        }

 

        public void Stop()

        {

            Console.WriteLine("P2P Server stopping...");

            try

            {

                serverThread.Abort();

                server.Close();

                Console.WriteLine("Stop OK.");

            }

            catch(Exception exp)

            {

                Console.WriteLine("Stop error: " + exp.Message);

                throw exp;

            }

 

        }

 

        private void Run()

        {

            byte[] buffer = null;

            while (true)

            {

                byte[] msgBuffer = server.Receive(ref remotePoint);

                try

                {

                    object msgObj = FormatterHelper.Deserialize(msgBuffer);

                    Type msgType = msgObj.GetType();

                    if (msgType == typeof(P2P.WellKnown.C2S.LoginMessage))

                    {

                        // 转换接受的消息

                        P2P.WellKnown.C2S.LoginMessage lginMsg = (P2P.WellKnown.C2S.LoginMessage)msgObj;

                        Console.WriteLine("has an user login: {0}", lginMsg.UserName);

                        // 添加用户到列表

                        IPEndPoint userEndPoint = new IPEndPoint(remotePoint.Address, remotePoint.Port);

                        User user = new User(lginMsg.UserName, userEndPoint);

                        userList.Add(user);

                        // 发送应答消息

                        P2P.WellKnown.S2C.GetUsersResponseMessage usersMsg = new P2P.WellKnown.S2C.GetUsersResponseMessage(userList);

                        buffer = FormatterHelper.Serialize(usersMsg);

                        server.Send(buffer, buffer.Length, remotePoint);

                    }

                    else if (msgType == typeof(P2P.WellKnown.C2S.LogoutMessage))

                    {

                        // 转换接受的消息

                        P2P.WellKnown.C2S.LogoutMessage lgoutMsg = (P2P.WellKnown.C2S.LogoutMessage)msgObj;

                        Console.WriteLine("has an user logout: {0}", lgoutMsg.UserName);

                        // 从列表中删除用户

                        User lgoutUser = userList.Find(lgoutMsg.UserName);

                        if (lgoutUser != null)

                        {

                            userList.Remove(lgoutUser);

                        }

                    }

                    else if (msgType == typeof(P2P.WellKnown.C2S.TranslateMessage))

                    {

                        // 转换接受的消息

                        P2P.WellKnown.C2S.TranslateMessage transMsg = (P2P.WellKnown.C2S.TranslateMessage)msgObj;

                        Console.WriteLine("{0}(1) wants to p2p {2}", remotePoint.Address.ToString(), transMsg.UserName, transMsg.ToUserName);

                        // 获取目标用户

                        User toUser = userList.Find(transMsg.ToUserName);

                        // 转发Purch Hole请求消息

                        if (toUser == null)

                        {

                            Console.WriteLine("Remote host {0} cannot be found at index server", transMsg.ToUserName);                          

                        }

                        else

                        {

                            P2P.WellKnown.S2C.SomeOneCallYouMessage transMsg2 = new P2P.WellKnown.S2C.SomeOneCallYouMessage(remotePoint);

                            buffer = FormatterHelper.Serialize(transMsg);

                            server.Send(buffer, buffer.Length, toUser.NetPoint);                           

                        }

                    }

                    else if (msgType == typeof(P2P.WellKnown.C2S.GetUsersMessage))

                    {

                        // 发送当前用户信息到所有登录客户

                        P2P.WellKnown.S2C.GetUsersResponseMessage srvResMsg = new P2P.WellKnown.S2C.GetUsersResponseMessage(userList);                       

                        buffer = FormatterHelper.Serialize(srvResMsg);

                        foreach(User user in userList)

                        {

                            server.Send(buffer, buffer.Length, user.NetPoint);

                        }

                    }

                    Thread.Sleep(500);

                }

                catch{}

            }

        }

    }

}

 

 

3.  P2Pclient

namespace P2P.P2PClient

{

    using System;

    using System.Net;

    using System.Net.Sockets;

    using System.Threading;

    using P2P.WellKnown;

    /// <summary>

    /// AppClass 的摘要说明。

    /// </summary>

    public class AppClass

    {

        public static void Main()

        {

            Client client = new Client("202.96.134.103");

            client.ConnectToServer("myname", "mypassword");

            client.Start();

            Console.WriteLine("test arguments");

            while (true)

            {

                string str = Console.ReadLine();

                client.PaserCommand(str);

            }

        }

    }

    /// <summary>

    /// Client 的摘要说明。

    /// </summary>

    public class Client : IDisposable

    {

        private const int MAXRETRY = 10;

        private UdpClient client;

        private IPEndPoint hostPoint;

        private IPEndPoint remotePoint;

        private UserCollection userList;

        private string myName;

        private bool ReceivedACK;

        private Thread listenThread;

 

        public Client(string serverIP)

        {

            ReceivedACK = false;

            remotePoint = new IPEndPoint(IPAddress.Any, 0);

            hostPoint = new IPEndPoint(IPAddress.Parse(serverIP), P2PConsts.SRV_PORT);

            client = new UdpClient();

            userList = new UserCollection();

            listenThread = new Thread(new ThreadStart(Run));

        }

 

        public void Start()

        {

            if (this.listenThread.ThreadState==ThreadState.Unstarted)

            {

                this.listenThread.Start();

                Console.WriteLine("You can input you command:/n");

                Console.WriteLine("Command Type:/"send/",/"exit/",/"getu/"");

                Console.WriteLine("Example : send Username Message");

                Console.WriteLine("          exit");

                Console.WriteLine("          getu");

            }

        }

 

        public void ConnectToServer(string userName, string password)

        {

            myName = userName;

            // 发送登录消息到服务器

            P2P.WellKnown.C2S.LoginMessage lginMsg = new P2P.WellKnown.C2S.LoginMessage(userName, password);

            byte[] buffer = FormatterHelper.Serialize(lginMsg);

            client.Send(buffer, buffer.Length, hostPoint);

            // 接受服务器的登录应答消息

            buffer = client.Receive(ref remotePoint);

            P2P.WellKnown.S2C.GetUsersResponseMessage srvResMsg = (P2P.WellKnown.S2C.GetUsersResponseMessage)FormatterHelper.Deserialize(buffer);

            // 更新用户列表

            userList.Clear();

            foreach(User user in srvResMsg.UserList)

            {

                userList.Add(user);

            }

            this.DisplayUsers(userList);

        }

 

        /// <summary>

        /// 这是主要的函数:发送一个消息给某个用户(C)

        /// 流程:直接向某个用户的外网IP发送消息,如果此前没有联系过

        /// 那么此消息将无法发送,发送端等待超时。

        /// 超时后,发送端将发送一个请求信息到服务端,要求服务端发送

        /// 给客户C一个请求,请求C给本机发送打洞消息

        /// *以上流程将重复MAXRETRY次

        /// </summary>

        /// <param name="toUserName">对方用户名</param>

        /// <param name="message">待发送的消息</param>

        /// <returns></returns>

        private bool SendMessageTo(string toUserName, string message)

        {

            User toUser = userList.Find(toUserName);

            if (toUser == null)

            {

                return false;

            }

            for (int i=0; i<MAXRETRY; i++)

            {

                P2P.WellKnown.P2P.WorkMessage workMsg = new P2P.WellKnown.P2P.WorkMessage(message);

                byte[] buffer = FormatterHelper.Serialize(workMsg);

                client.Send(buffer, buffer.Length, toUser.NetPoint);

 

                // 等待接收线程将标记修改

                for (int j=0; j<10; j++)

                {

                    if (this.ReceivedACK)

                    {

                        this.ReceivedACK = false;

                        return true;

                    }

                    else

                    {

                        Thread.Sleep(300);

                    }

                }

                // 没有接收到目标主机的回应,认为目标主机的端口映射没有

                // 打开,那么发送请求信息给服务器,要服务器告诉目标主机

                // 打开映射端口(UDP打洞)

                P2P.WellKnown.C2S.TranslateMessage transMsg = new P2P.WellKnown.C2S.TranslateMessage(myName, toUserName);

                buffer = FormatterHelper.Serialize(transMsg);

                client.Send(buffer, buffer.Length, hostPoint);

                // 等待对方先发送信息

                Thread.Sleep(100);

            }

            return false;

        }

 

        public void PaserCommand(string cmdstring)

        {

            cmdstring = cmdstring.Trim();

            string[] args = cmdstring.Split(new char[]{'' ''});

            if (args.Length > 0)

            {

                if (string.Compare(args[0], "exit", true) == 0)

                {

                    P2P.WellKnown.C2S.LogoutMessage lgoutMsg = new P2P.WellKnown.C2S.LogoutMessage(myName);

                    byte[] buffer = FormatterHelper.Serialize(lgoutMsg);

                    client.Send(buffer, buffer.Length, hostPoint);

                    // do clear something here

                    Dispose();

                    System.Environment.Exit(0);

                }

                else if (string.Compare(args[0], "send", true) == 0)

                {                  

                    if (args.Length > 2)

                    {

                        string toUserName = args[1];

                        string message    = "";

                        for(int i=2; i<args.Length; i++)

                        {

                            if (args[i] == "") message += " ";

                            else message += args[i];

                        }

                        if (this.SendMessageTo(toUserName, message))

                        {

                            Console.WriteLine("Send OK!");

                        }

                        else

                            Console.WriteLine("Send Failed!");

                    }

                }

                else if (string.Compare(args[0], "getu", true) == 0)

                {

                    P2P.WellKnown.C2S.GetUsersMessage getUserMsg = new P2P.WellKnown.C2S.GetUsersMessage(myName);

                    byte[] buffer = FormatterHelper.Serialize(getUserMsg);

                    client.Send(buffer, buffer.Length, hostPoint);

                }

                else

                {

                    Console.WriteLine("Unknown command {0}", cmdstring);

                }

            }

        }

 

        private void DisplayUsers(UserCollection users)

        {

            foreach (User user in users)

            {

                Console.WriteLine("Username: {0}, IP:{1}, Port:{2}", user.UserName, user.NetPoint.Address.ToString(), user.NetPoint.Port);

            }

        }

 

        private void Run()

        {

            byte[] buffer;

            while (true)

            {

                buffer = client.Receive(ref remotePoint);

                object msgObj = FormatterHelper.Deserialize(buffer);

                Type msgType = msgObj.GetType();

                if (msgType == typeof(P2P.WellKnown.S2C.GetUsersResponseMessage))

                {

                    // 转换消息

                    P2P.WellKnown.S2C.GetUsersResponseMessage usersMsg = (P2P.WellKnown.S2C.GetUsersResponseMessage)msgObj;

                    // 更新用户列表

                    userList.Clear();

                    foreach(User user in usersMsg.UserList)

                    {

                        userList.Add(user);

                    }

                    this.DisplayUsers(userList);

                }

                else if (msgType == typeof(P2P.WellKnown.S2C.SomeOneCallYouMessage))

                {

                    // 转换消息

                    P2P.WellKnown.S2C.SomeOneCallYouMessage purchReqMsg = (P2P.WellKnown.S2C.SomeOneCallYouMessage)msgObj;

                    // 发送打洞消息到远程主机

                    P2P.WellKnown.P2P.TrashMessage trashMsg = new P2P.WellKnown.P2P.TrashMessage();

                    buffer = FormatterHelper.Serialize(trashMsg);

                    client.Send(buffer, buffer.Length, purchReqMsg.RemotePoint);

                }

                else if (msgType == typeof(P2P.WellKnown.P2P.WorkMessage))

                {

                    // 转换消息

                    P2P.WellKnown.P2P.WorkMessage workMsg = (P2P.WellKnown.P2P.WorkMessage)msgObj;

                    Console.WriteLine("Receive a message: {0}", workMsg.Message);

                    // 发送应答消息

                    P2P.WellKnown.P2P.ACKMessage ackMsg = new P2P.WellKnown.P2P.ACKMessage();

                    buffer = FormatterHelper.Serialize(ackMsg);

                    client.Send(buffer, buffer.Length, remotePoint);

                }

                else if (msgType == typeof(P2P.WellKnown.P2P.ACKMessage))

                {

                    this.ReceivedACK = true;

                }

                else if (msgType == typeof(P2P.WellKnown.P2P.TrashMessage))

                {

                    Console.WriteLine("Recieve a trash message");

                }

                Thread.Sleep(100);

            }

        }

        #region IDisposable 成员

 

        public void Dispose()

        {

            try

            {

                this.listenThread.Abort();

                this.client.Close();

            }

            catch

            {}

        }

 

        #endregion

    }

}


该文章在 2021/2/2 8:58:57 编辑过

全部评论1

admin
2021年2月2日 11:2
互联网草案B.福特
文档:麻省理工学院的draft-ford-midcom-p2p-01.txt
过期:2004年4月27日P. Srisuresh
                                                          Caymas系统
                                                                凯格尔
                                                               kegel.com
                                                            2003年10月

              中间盒之间的对等(P2P)通信

该备忘录的状态

   本文档为互联网草案,并受所有条款约束RFC2026第10节的内容。互联网草案是互联网工程任务组(IETF),其领域及其工作组。请注意,其他小组也可能分配工作文档作为Internet草案。

   互联网草案是文件草案,有效期最长为六个月并可能在任何时候被其他文档更新,替换或废弃时间。使用Internet-草稿作为参考是不合适的材料或引用它们,而不是将其作为“进行中的工作”。

   当前的Internet草案列表可以在以下位置访问:
   http://www.ietf.org/1id-abstracts.html

   可以从以下位置访问Internet草案目录目录:
   http://www.ietf.org/shadow.html

   该文档的分发是无限的。

版权声明

   版权所有(C)互联网协会(2003)。版权所有。

抽象

   本备忘录记录了当前点对点使用的方法(P2P)应用程序在存在中间盒的情况下进行通信例如防火墙和网络地址转换器(NAT)。在此外,该备忘录还为应用程序设计人员提供了指南和中间盒实施者可以采取的措施使用或可以立即,广泛地部署P2P应用程序无需使用特殊的代理,中继或中间通信协议。

目录

   1.简介............................................... ..
   2.术语.............................................. ...
   3.通过中间盒进行P2P通信的技术............
       3.1。中继....................................................
       3.2。连接反转....................................
       3.3。UDP打孔.....................................
             3.3.1。不同NAT背后的对等...
             3.3.2。在同一个NAT之后的对等方...................
             3.3.3。被多个NAT隔开的对等体...
             3.3.4。一致的端口绑定.......................
       3.4。UDP端口号预测...............................
       3.5。同时打开TCP ..................................
   4.应用程序设计准则................................
       4.1。什么适用于P2P中间盒..................................
       4.2。同一NAT后面的应用程序........................
       4.3。同行发现......................................................
       4.4。TCP P2P应用程序....................................
       4.5。使用Midcom协议..................................
   5. NAT设计准则....................................
       5.1。弃用对称NAT ...
       5.2。向对称NAT设备添加增量Cone-NAT支持
       5.3。维护UDP端口的一致端口绑定.....
             5.3.1。保留端口号.................................
       5.4。保持TCP端口的一致端口绑定.....
       5.5。P2P应用程序超时...................
   6.安全注意事项...............................................

1.简介

   今天的互联网已经普遍部署了驱动的“中间盒”,例如网络地址转换器(NAT)主要是由于IPv4地址空间的持续消耗的这些建立的不对称寻址和连接机制然而,中间盒为点对点创建了独特的问题(P2P)应用程序和协议,例如电话会议和多人在线游戏。这些问题甚至可能持续存在进入IPv6世界,其中NAT通常用作IPv4兼容性机制[NAT-PT],甚至防火墙仍然很普遍不再需要NAT之后。

   当前部署的中间盒主要围绕客户端/服务器范例,其中相对匿名的客户端计算机主动启动与具有稳定状态的连接良好的服务器的连接IP地址和DNS名称。大多数中间盒实现非对称私有内部网络上的主机的通信模型可以启动到公用网络上主机的传出连接,但是外部主机无法启动与内部主机的连接,但由中间盒管理员专门配置。在里面在NAPT的常见情况下,内部网络上的客户端没有公共Internet上的唯一IP地址,但必须共享由NAPT与其他主机管理的单个公用IP地址在同一专用网络上。的匿名性和不可访问性中间盒后面的内部主机对客户端来说不是问题仅需要启动传出的软件,例如网络浏览器连接。这种不可访问性有时被视为隐私效益。

   但是,在对等范式中,Internet主机可以通常被认为是“客户”需要建立沟通彼此直接会话。发起者和响应者可能位于没有端点的不同中间盒后面具有任何永久IP地址或其他形式的公共网络存在。例如,一种常见的在线游戏架构供参与的应用程序主机与知名的服务器用于初始化和管理目的。后续的为此,主机之间建立直接连接在游戏过程中快速有效地传播更新。
   同样,文件共享应用程序可能会与知名的服务器用于资源发现或搜索,但直接建立与对等主机的连接以进行数据传输。中间盒创建对等连接的问题,因为主机位于中间盒通常在其上没有永久可用的公共端口来自其他对等方的传入TCP或UDP连接的Internet可以指挥。RFC 3235 [NAT-APPL]简要解决了这个问题,但不提供任何常规解决方案。

   在本文档中,我们以两种方式解决P2P /中间盒问题。
   
   首先,我们总结了P2P应用程序可以通过哪些已知方法解决中间盒的存在。第二,我们提供一套这些实践的基础上设计应用程序设计准则P2P应用程序在当前部署的应用程序上运行更稳定中间箱。此外,我们提供了未来的设计准则中间盒,使他们能够更多地支持P2P应用程序有效。我们的重点是实现即时和广泛的部署需要遍历中间盒的P2P应用程序。

2.术语

在本节中,我们首先总结一些中间盒术语。我们专注于这里通常会导致P2P问题的两种中间盒上应用程序。

   防火墙功能
      防火墙限制了专用内部设备之间的通信网络和公共互联网,通常是通过丢弃数据包被认为是未经授权的。防火墙检查但不修改IP地址和TCP / UDP端口信息跨越边界的数据包。

   网络地址转换器(NAT)
      网络地址转换器不仅检查而且可以修改跨越边界的数据包中的报头信息,允许NAT后面的许多主机共享较小的主机的使用公共IP地址的数量(通常是一个)。

   网络地址转换器又有两个主要种类:

   基本NAT
      基本NAT将内部主机的专用IP地址映射到公用IP地址,无需更改TCP / UDP端口跨越边界的数据包中的数字。基本NAT通常是仅在NAT具有来自以下地址的公共IP地址池时才有用代表内部主机进行地址绑定。

   网络地址/端口转换器(NAPT)
      到目前为止,最常见的是网络地址/端口转换器检查并修改IP地址和TCP / UDP端口号跨越边界的数据包字段,允许多个内部主机同时共享一个公共IP地址。

   有关以下内容的更多常规信息,请参考[NAT-TRAD]和[NAT-TERM]NAT分类和术语。进一步分类的其他术语在最近的工作[STUN]中定义了NAPT。当内部主机通过网络地址/端口打开传出的TCP或UDP会话转换器,NAPT为会话分配一个公共IP地址,端口号,以便来自外部的后续响应数据包端点可以被NAPT接收,翻译和转发到内部主机。结果是,NAPT建立了一个(专用IP地址,专用端口号)和(公用IP地址,公用端口号)。端口绑定定义NAPT将为会话持续时间。与P2P相关的问题应用程序是内部主机启动时NAT的行为来自单个(私有IP,私有端口)配对到外部网络上的多个不同端点。

   锥体NAT
      建立端口之间的绑定后,(私有IP,私有IP端口)元组和一个(公共IP,公共端口)元组,圆锥体NAT将 将该端口绑定重新用于后续会话应用程序可以从相同的专用IP地址启动,并且端口号,至少使用该端口进行一次会话绑定保持活动状态。

      例如,假设下图中的客户端A启动了两个通过圆锥体NAT同时进行的传出会话内部端点(10.0.0.1:1234)为两个不同外部服务器S1和S2。圆锥NAT仅分配一个公共这两个会话的端点元组155.99.25.11:62000,确保维护客户端端口的“身份”跨地址翻译。由于基本NAT和防火墙都可以在数据包流过时不修改端口号中间盒,这些类型的中间盒可以视为锥形NAT的简并形式。

           服务器S1服务器S2
        18.181.0.31:1235 138.76.29.7:1235
               | |
               | |
               + ---------------------- + ---------------------- +
                                      |
          ^会议1(A-S1)^ | ^会议2(A-S2)^
          | 18.181.0.31:1235 | | | 138.76.29.7:1235 |
          v 155.99.25.11:62000 v | v 155.99.25.11:62000 v
                                      |
                                   锥体NAT
                                 155.99.25.11
                                      |
          ^会议1(A-S1)^ | ^会议2(A-S2)^
          | 18.181.0.31:1235 | | | 138.76.29.7:1235 |
          v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
                                      |
                                   客户A
                                10.0.0.1:1234

   对称NAT
      相反,对称NAT不能保持一致(私有IP,私有端口)与(公共IP,公共会话)。而是分配一个新的每个新会话的公共端口。例如,假设客户A从与上述相同的端口启动两个传出会话,一个与S1和一个与S2。对称NAT可能会分配公用端点155.99.25.11:62000到会话1,然后分配一个不同的公共端点155.99.25.11:62001,当应用程序启动会话2。NAT能够区分在两个会话之间进行翻译,因为会话中涉及的外部端点(S1的端点)和S2)不同,即使客户端的端点标识 跨地址转换边界的应用程序丢失。

           服务器S1服务器S2
        18.181.0.31:1235 138.76.29.7:1235
               | |
               | |
               + ---------------------- + ---------------------- +
                                      |
          ^会议1(A-S1)^ | ^会议2(A-S2)^
          | 18.181.0.31:1235 | | | 138.76.29.7:1235 |
          v 155.99.25.11:62000 v | v 155.99.25.11:62001 v
                                      |
                                 对称NAT
                                 155.99.25.11
                                      |
          ^会议1(A-S1)^ | ^会议2(A-S2)^
          | 18.181.0.31:1235 | | | 138.76.29.7:1235 |
          v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
                                      |
                                   客户A
                                10.0.0.1:1234

   锥形与对称NAT行为的问题同样适用TCP和UDP流量。 

   锥体NAT根据NAT的宽松程度进一步分类接受针对已建立的(公共IP,公共端口)对。此分类通常仅适用于UDP通信,因为NAT和防火墙拒绝传入的TCP除非专门配置为无条件连接尝试否则。

   全锥NAT
      建立新的公共/私有端口绑定后传出会话时,完整的锥体NAT随后将接受从ANY到相应公共端口的传入流量公共网络上的外部端点。全锥NAT是有时也称为“混杂” NAT。

   受限锥NAT
      受限锥形NAT仅转发定向到的入站数据包公共端口(如果其外部(源)IP地址与内部主机先前已发送到的节点的地址一个或多个传出数据包。受限锥NAT有效完善了拒绝未经请求的传入的防火墙原理通过将传入流量限制为一组“已知”流量 外部IP地址。

   端口受限的锥状网络NAT
      反过来,受端口限制的锥形NAT仅转发入站如果其外部IP地址和端口号与内部主机先前拥有的外部端点发送传出数据包。端口受限的锥形NAT提供内部节点具有防止未经请求的相同级别的保护对称NAT所做的传入流量,同时保持整个翻译过程中专用端口的身份。

   最后,在本文档中,我们定义了用于分类的新术语
   中间盒的P2P相关行为:

   P2P应用
      本文档中使用的P2P应用程序是每个P2P参与者向公众注册的注册服务器,随后使用其专用端点或公用端点,或两者同时建立对等会话。

   P2P-中间盒
      P2P中间箱是允许遍历以下内容的中间箱P2P应用程序。 

   P2P防火墙
      P2P防火墙是提供防火墙的P2P中间盒功能,但不执行地址转换。

   对等网络
      P2P-NAT是提供NAT功能的P2P-中间盒,并且还可以提供防火墙功能。至少,P2P-Middlebox必须为UDP流量实现Cone NAT行为,允许应用程序使用以下方式建立可靠的P2P连接UDP打孔技术。

   回送翻译
      当NAT设备专用域中的主机尝试使用以下命令与同一NAT设备后面的另一台主机连接主机的公共地址,NAT设备执行 相当于在数据包上的“两次nat”翻译为如下。原始主机的专用端点已转换到其分配的公共端点,以及目标主机的公共端点在转换为私有端点之前 数据包转发到目标主机。我们参考上面NAT设备执行的转换为“回送转换”。
  
3.中间盒上的P2P通信技术

   本节详细回顾了当前已知的技术在现有的中间盒上实现对等通信,从应用程序或协议设计者的角度来看。

3.1。接力

   实现对等网络的最可靠但效率最低的方法在中间盒的情况下进行对等通信是为了使对等通信看起来像客户端/服务器一样指向网络通过中继进行通信。例如,假设有两个客户主机A和B分别发起了具有以下内容的TCP或UDP连接:具有永久IP地址的知名服务器S。客户驻留在单独的专用网络上,但是它们各自中间盒可防止任何一个客户端直接启动连接到另一个。

                                服务器S
                                   |
                                   |
            + ---------------------- + ---------------------- +
            | |
          NAT A NAT B
            | |
            | |
         客户A客户B

   无需尝试直接连接,两个客户端可以简单地使用服务器S在它们之间中继消息。例如,向客户端B发送消息,客户端A只是将消息发送给服务器S及其已经建立的客户端/服务器连接,以及然后,服务器S使用其现有消息将消息发送到客户端B客户端/服务器与B的连接。

   这种方法的优点是只要两个客户端都可以连接到服务器。很明显缺点是它消耗了服务器的处理能力,并且不必要的网络带宽,以及之间的通信延迟即使服务器运行良好,两个客户端也可能会增加-连接的。TURN协议[TURN]定义了一种实现方法以相对安全的方式进行中继。

3.2。连接反转

   如果只有一个客户在后面,则第二种方法有效中间盒。例如,假设客户端A在NAT之后,但客户端B具有全局可路由的IP地址,如下图所示:

                                服务器S
                            18.181.0.31:1235
                                   |
                                   |
            + ---------------------- + ---------------------- +
            | |
          NAT A |
    155.99.25.11:62000 |
            | |
            | |
         客户A客户B
      10.0.0.1:1234 138.76.29.7:1234

   客户端A具有专用IP地址10.0.0.1,应用程序是使用TCP端口1234。此客户端已与服务器S位于公共IP地址18.181.0.31和端口1235。NAT A具有为TCP端口62000分配了自己的公用IP地址155.99.25.11,用作A会话的临时公共端点地址与S:因此,服务器S认为客户端A在IP地址上155.99.25.11使用端口62000。但是,客户端B有自己的端口永久IP地址138.76.29.7和对等应用程序B上的B接受端口1234上的TCP连接。

   现在,假设客户端B要发起对等与客户端A的通信会话。B可能首先尝试在客户A相信自己的地址联系客户A拥有10.0.0.1:1234,或位于A的地址,如服务器S,即155.99.25.11:62000。但是,无论哪种情况,连接将失败。在第一种情况下,将流量定向到IP地址10.0.0.1只会被网络丢弃,因为10.0.0.1不是可公开路由的IP地址。在第二种情况下来自B的TCP SYN请求将到达指向端口的NAT A62000,但是NAT A将拒绝连接请求,因为仅允许传出连接。

   尝试建立与A的直接连接失败后,客户端B可以使用服务器S将请求中继到客户端A以发起到客户端B的“反向”连接。通过S中继请求,在B的客户端B打开与客户端B的TCP连接公用IP地址和端口号。NAT A允许连接到继续,因为它起源于防火墙,并且客户端B可以接收连接,因为它不在中间盒后面。

   当前的各种对等系统都实现了该技术。当然,它的主要局限在于它只能在一个通信对等方位于NAT之后:在两个对等方都位于NAT之后的常见情况下,该方法将失败。  由于连接反转不是解决问题的一般方法,不建议将其作为主要策略。应用程序可以选择尝试反向连接,但应该能够回退自动在另一种机制上(例如,如果没有可以建立“正向”或“反向”连接。

3.3。UDP打孔

   第三种技术,也是该技术的主要兴趣之一文档,被广泛称为“ UDP打孔”。UDP打孔依靠通用防火墙和锥形NAT的属性来允许适当设计的点对点应用程序以“打洞”通过中间盒并与每个中间盒建立直接连接其他,即使两个通信主机都可能位于中间盒后面。RFC 3027的第5.1节[NAT-PROT],并已在Internet上的其他地方进行了非正式描述[KEGEL],并在最近的一些协议[TEREDO,ICE]中使用。作为名字不幸的是,这意味着该技术仅适用于UDP。

   我们将考虑两个特定的场景,以及如何应用旨在优雅地处理它们。在第一种情况下代表一般情况,两个客户希望直接点对点对等通信位于两个不同的NAT之后。在第二,这两个客户端实际上位于同一个NAT后面,但不一定知道他们这样做。

3.3.1。不同NAT背后的对等体

   假设客户端A和客户端B都有私有IP地址,并且位于后面不同的网络地址转换器。点对点应用运行在客户端A和B以及服务器S上的每个端口都使用UDP端口1234。和B分别发起了与服务器S的UDP通信会话,导致NAT A为A的会话分配其自己的公共UDP端口62000与S,并导致NAT B将其端口31000分配给B的会话与S分别。

                                服务器S
                            18.181.0.31:1234
                                   |
                                   |
            + ---------------------- + ---------------------- +
            | |
          NAT A NAT B

    155.99.25.11:62000 138.76.29.7:31000
            | |
            | |
         客户A客户B
      10.0.0.1:1234 10.1.1.3:1234

   现在假设客户端A要建立UDP通信会话直接与客户端B对话。如果A只是开始发送UDP消息发送到B的公共地址138.76.29.7:31000,则NAT B将通常会丢弃这些传入消息(除非它是完整的NAT),因为源地址和端口号不匹配S,与之建立了原始传出会话。同样,如果B仅开始向A的公众发送UDP消息地址,然后NAT A通常会丢弃这些消息。

   假设A开始向B的公共地址发送UDP消息,同时通过服务器S将请求中继到B,询问B开始将UDP消息发送到A的公共地址。A的外向指向B的公共地址(138.76.29.7:31000)的邮件导致NATA在A的私人地址之间打开新的通信会话和B的公共地址。同时,B向A的公众发送的消息地址(155.99.25.11:62000)导致NAT B打开新B的私人地址与A的公共地址之间的通信会话地址。在每个服务器中打开新的UDP会话后方向,客户端A和客户端B可以直接相互通信而不会对“介绍”服务器S造成更多负担。

   UDP打孔技术具有几个有用的属性。一旦在两个之间建立了直接的对等UDP连接中间盒后面的客户端,该连接上的任何一方都可以接任“介绍人”的角色并帮助另一方与其他对等方建立对等连接,从而最大程度地减少初始导入服务器S上的负载。应用程序执行无需尝试明确检测它是哪种中间盒后面(如果有[STUN]),因为上述步骤将建立对等体-如果有一个或两个客户,则对等通信渠道也同样好不要碰巧在中间盒后面。打孔技术甚至可以自动使用多个NAT,其中一个或两个都使用客户端通过两个或多个级别从公共Internet中删除地址翻译。

3.3.2。同一个NAT背后的对等体

   现在考虑两个客户(可能是在不知不觉中)恰好位于同一NAT后面,因此位于相同的专用IP地址空间中。客户A有与服务器S建立了UDP会话,通用NAT已与该服务器建立会话分配的公共端口号62000。客户端B同样具有与S建立了会话,NAT已将S分配给该会话端口号62001。

                                服务器S
                            18.181.0.31:1234
                                   |
                                   |
                                  NAT
                         AS 155.99.25.11:62000
                         BS 155.99.25.11:62001
                                   |
            + ---------------------- + ---------------------- +
            | |
         客户A客户B
      10.0.0.1:1234 10.1.1.3:1234

   假设A和B使用概述的UDP穿孔技术以上建立使用服务器S作为服务器的通信通道介绍人。然后,A和B将学习彼此的公用IP地址和服务器S观察到的端口号,并开始发送每个这些公共地址上的其他消息。两个客户将只要NAT就能以这种方式相互通信允许内部网络上的主机打开转换的UDP会话
   与其他内部主机,而不仅仅是与外部主机。我们提到这种情况称为“回送转换”,因为数据包到达在NAT上从专用网络进行转换,然后“循环返回到专用网络,而不是通过公共网络。例如,当A向B的公共用户发送UDP数据包时地址,该数据包最初具有源IP地址和端口号10.0.0.1:124的位置和155.99.25.11:62001的目的地。NAT收到此数据包,将其转换为具有155.99.25.11:62000(A的公共地址)和目的地10.1.1.3:1234,然后将其转发到B。即使环回转换支持NAT,此转换和转发在这种情况下,步骤显然是不必要的,并且可能会增加A和B之间对话的延迟以及NAT的负担。

   但是,解决此问题的方法很简单。当A和B最初通过服务器S交换地址信息,应包括其自己的IP地址和端口号为“已观察”以及S观察到的地址。客户端然后同时开始在彼此之间发送数据包他们知道的每个替代地址,并使用第一个导致成功沟通的地址。如果两个客户在同一个NAT之后,然后将数据包定向到其专用地址很可能先到达,从而导致直接不涉及NAT的通信通道。如果两个客户是在不同的NAT之后,然后将数据包定向到其专用地址将完全无法互相访??问,但客户端将希望与他们各自的公众建立联系地址。这些数据包必须在以下位置进行身份验证很重要但是,以某种方式,因为在不同的NAT情况下,这完全是定向到B的私人地址的A的邮件可能到达A的专用网络上其他不相关的节点,反之亦然。

3.3.3。被多个NAT隔开的对等体

   在涉及多个NAT设备的某些拓扑中,它不是两个客户端之间可能建立“最佳” P2P路由他们没有具体的拓扑知识。考虑为示例以下情况。

                                服务器S
                            18.181.0.31:1234
                                   |
                                   |
                                 NAT X
                         AS 155.99.25.11:62000
                         BS 155.99.25.11:62001
                                   |
                                   |
            + ---------------------- + ---------------------- +
            | |
          NAT A NAT B
    192.168.1.1:30000 192.168.1.2:31000
            | |
            | |
         客户A客户B
      10.0.0.1:1234 10.1.1.3:1234

   假设NAT X是由Internet部署的大型工业NAT服务提供商(ISP),将许多客户吸引到一些公共场所IP地址以及NAT A和B是小型消费者NAT网关由ISP的两个客户独立部署以进行多路复用将其专用家庭网络添加到各自的ISP提供的IP上地址。仅服务器S和NAT X具有全局可路由IP地址;NAT A和NAT B使用的“公共” IP地址是实际上是ISP寻址领域的私有对象,而客户端A和B的地址又对NAT A的寻址域是私有的和B分别。每个客户端都发起与之的传出连接服务器S像以前一样,导致NAT A和NAT B各自创建一个公共/私有转换,并导致NAT X建立一个每个会话的公共/私人翻译。

   现在假设客户A和B尝试建立直接的对等UDP连接。最佳方法是让客户A将邮件发送到客户端B在NAT B的公共地址,ISP寻址领域中的192.168.1.2:31000,并且客户端B向NAT B的A的公共地址发送消息,即192.168.1.1:30000。不幸的是,A和B无法学习这些地址,因为服务器S仅看到“全局”公共地址155.99.25.11:62000和155.99.25.11:62001。即使AB有了一些学习这些地址的方法,仍然没有确保它们将可用,因为地址分配ISP的专用寻址领域中的冲突可能与不相关的冲突客户私人领域中的地址分配。客户因此别无选择,只能将其全球公共地址用作被S视为P2P通讯,并依靠NAT X提供回送翻译。

3.3.4。一致的端口绑定

   打孔技术有一个主要警告:仅当这两个NAT都是锥形NAT(或非NAT防火墙),它们维护一个给定(专用IP,专用UDP)之间的一致端口绑定对和一个(公共IP,公共UDP)对(只要该UDP端口长)正在使用中。为每个新会话分配一个新的公共端口,作为对称NAT可以使UDP应用程序无法重用已经建立的翻译以与之通信不同的外部目的地。由于锥状NAT是最多的UDP打孔技术非常广泛适用; 但是,大部分已部署的NAT是对称且不支持该技术。

3.4。UDP端口号预测

   存在上述UDP穿孔技术的一种变体允许在现场创建对等UDP会话一些对称NAT。这种方法有时称为“ N + 1”技术[BIDIR],并由武田[SYM-STUN]详细研究。该方法通过分析NAT的行为并尝试预测将分配给将来会话的公共端口号。再次考虑两个客户A和B各自的情况在一个单独的NAT后面,每个建立的UDP连接都带有一个永久可寻址服务器S:

                                  服务器S
                              18.181.0.31:1234
                                     |
                                     |
              + ---------------------- + ---------------------- +
              | |
       对称NAT A对称NAT B

AS 155.99.25.11:62000 BS 138.76.29.7:31000
              | |
              | |
           客户A客户B
        10.0.0.1:1234 10.1.1.3:1234

   NAT A已为通信分配了其自己的UDP端口62000A和S之间的会话,并且NAT B已将其端口31000分配给B和S之间的会话。通过服务器S,A和B进行通信了解彼此的公共IP地址和端口号客户端A现在开始将UDP消息发送到端口31001地址138.76.29.7(请注意端口号增量)和客户端B同时开始向地址为62001的端口发送消息155.99.25.11。如果NAT A和NAT B将端口号分配给新会话自AS和BS以来没有经过太多时间会议开始,然后进行双向工作应该会在A和B之间形成通道。A向B发送的消息导致NAT A打开一个新会话,NAT A将(希望)分配给该会话公用端口号62001,因为62001是序列号之后的下一个它先前分配给A和之间会话的端口号62000S。同样,B向A发送的消息将导致NAT B打开一个新的会话,它将(希望)为其分配端口号31001。两个客户端都正确猜测了每个NAT分配的端口号到新会话,然后是双向UDP通信通道将如下所示建立。

                                  服务器S
                              18.181.0.31:1234
                                     |
                                     |
              + ---------------------- + ---------------------- +
              | |
            NAT A NAT B
   AS 155.99.25.11:62000 BS 138.76.29.7:31000
   AB 155.99.25.11:62001 BA 138.76.29.7:31001
              | |
              | |
           客户A客户B
        10.0.0.1:1234 10.1.1.3:1234

   显然,有很多事情可以导致此技巧失败。如果任一NAT的预测端口号已经在由不相关的会话使用,则NAT将跳过该端口数字,连接尝试将失败。如果任何一个NAT有时或始终不按顺序选择端口号,则窍门将失败。如果打开了NAT A(或B)后面的另一个客户端A之后建立与任何外部目标的新传出UDP连接(B)建立与S的连接,但在发送第一个前消息发送给B(A),那么无关的客户端将无意间“窃取”所需的端口号。因此,这个技巧要少得多当涉及的任何一个NAT都处于负载状态时,可能会工作。

   由于在实践中实施此技巧的P2P应用程序将如果NAT是锥形NAT,或者一个是锥形NAT,则仍然需要工作而另一个是对称NAT,则应用程序需要预先检测两端都涉及哪种NAT [STUN]并相应地修改其行为,从而增加了算法和网络的总体脆弱性。最后,港口如果任何一个客户落后,号码预测都将无法进行两层或更多层NAT,并且最接近客户端的NAT是对称的。由于所有这些原因,建议您不要应用程序实现了这一技巧;在这里提到历史和信息目的。

3.5。同时打开TCP

   在某些情况下可以使用一种方法来建立直接一对节点之间的对等TCP连接在现有中间盒后面。大多数TCP会话都以一个开始发送SYN数据包的端点,另一方对此进行响应SYN-ACK数据包。然而,对于两个端点通过同时发送对方来启动TCP会话SYN数据包,各方随后对它们进行响应单独的ACK。此过程称为“同时打开”。

   如果中间盒从私有外部接收到TCP SYN数据包网络尝试启动传入的TCP连接,中间盒通常会拒绝其中一个的连接尝试丢弃SYN数据包或发送回TCP RST(连接重置)包。但是,如果SYN数据包与源一起到达,并且对应于TCP的目标地址和端口号中间盒认为已激活的会话,则middlebox将允许数据包通过。特别是如果中间盒最近刚刚看到并传输了传出的SYN具有相同地址和端口号的数据包,则它将认为会话处于活动状态,并允许传入的SYN通过。如果客户端A和B可以正确预测公共端口号它各自的中间盒将分配下一个传出的TCP连接,以及每个客户端是否发起传出TCP连接与另一个客户端定时,以便每个客户端的传出SYN通过在任一SYN到达相反位置之前,通过其本地中间盒中间盒,那么将建立一个有效的对等TCP连接。

   不幸的是,这个技巧可能更加脆弱和时机-比上述UDP端口号预测技巧敏感。首先,除非两个中间盒都是简单的防火墙或实现锥形NAT行为影响其TCP流量,所有相同的东西都可能出错双方都试图预测公共端口号,各个NAT将分配给新会话。另外,如果任一客户的SYN到达对方中间箱的速度都太快了,那么远程中间盒可能会拒绝带有RST数据包的SYN,导致本地中间盒依次关闭新会话并进行将来使用相同的端口号进行SYN重传尝试徒劳的。最后,即使支持同时打开技术上是TCP规范[TCP]的强制性部分,它是在某些常见的操作系统中无法正确实现。为了这原因,仅在历史上同样在此提及此技巧原因;不建议应用程序使用它。应用领域需要高效,直接的对等通信现有的NAT应该使用UDP。

4.应用程序设计准则

4.1。什么适用于P2P中间盒

  由于UDP打孔是最有效的现有方法在两个节点之间建立直接的对等通信它们都位于NAT之后,并且可以与多种现有的NAT,建议应用程序使用如果需要有效的点对点通信, 但要准备好直接使用简单中继无法建立通信。 

4.2。同一个NAT背后的对等体

  实际上,可能有相当多的用户 没有两个IP地址,而是三个或更多。在这些情况下,很难或不可能说出要发送到的地址 注册服务器。应用程序应发送其所有地址,在这种情况下。

4.3。同行发现
 
  应用程序将数据包发送到多个地址以发现哪一个最适合用于给定的对等方可能会变成一个 作为“太空垃圾”的重要来源,对等端可能选择不正确地使用可路由地址,因为内部局域网(例如分配给国防部的11.0.1.1)。因此,应用程序在发送投机问候包。

4.4。TCP P2P应用

  应用程序开发人员广泛使用的套接字API是设计时要考虑客户端-服务器应用程序。在其本机形式,只有单个套接字可以绑定到TCP或UDP港口。一个应用程序不允许有多个套接字绑定到同一端口(TCP或UDP)以启动 与多个外部节点(或)同时进行的会话 使用一个套接字监听端口,使用另一个套接字启动传出会话。

  上面的单插座到端口绑定限制不是但是UDP会出现问题,因为UDP是基于数据报的协议。UDP P2P应用程序设计人员可以使用一个用于从多个端口发送和接收数据报的套接字使用recvfrom()和sendto()调用的对等体。

  TCP并非如此。使用TCP,每个传入和传出连接应与单独的关联插座。Linux套接字API通过借助SO_REUSEADDR选项。在FreeBSD和NetBSD上,选项似乎不起作用;但是,将其更改为使用BSD特定的SetReuseAddress调用(Linux不这样做)在单Unix标准中有和没有)似乎都起作用。Win32 API提供了等效的SetReuseAddress调用。使用上述任何选项,应用程序可以使用多个套接字重用TCP端口。说开将两个TCP流套接字绑定到同一端口,一个监听(),另一个监听(connect)。
 
4.5。使用Midcom协议

  如果应用程序知道中间盒,它们将是遍历,这些中间盒实现了Midcom协议,应用程序可以使用Midcom协议轻松通过中间盒。 

  例如,P2P应用程序要求NAT中间盒保留端点端口绑定。如果在以下位置支持midcom中间盒,P2P应用程序可以控制端口绑定(或地址绑定)参数,例如生存期,最长的时间和方向性,因此应用程序可以连接到外部对等方并接收来自外部同行 并且不需要定期向其发送保持活动消息使端口绑定保持活动状态。当应用程序不再需要时绑定,应用程序可以简单地取消绑定,也使用midcom协议。 

5. NAT设计准则

   本节讨论网络设计中的注意事项
   解决翻译器,因为它们影响点对点应用程序。   

5.1。弃用对称NAT

   对称NAT在客户端服务器中越来越受欢迎只需启动的应用程序(例如Web浏览器)传出连接。但是,最近,P2P即时消息和音频会议等应用程序已经被广泛使用。对称NAT不支持保留端点身份且不适合的概念用于P2P应用程序。弃用对称NAT是建议支持P2P应用程序。 

   P2P中间盒必须为UDP实现锥体NAT行为流量,允许应用程序建立可靠的P2P使用UDP打孔技术的连接性。  理想情况下,P2P中间框还应允许应用程序通过TCP和UDP建立P2P连接。

5.2。向对称NAT设备添加增量式锥形NAT支持

   对称NAT设备将支持扩展到P2P的一种方法应用程序将划分其可分配的端口名称空间,一对一地保留其端口的一部分会话和一对多的一组不同端口会议。

   此外,NAT设备可以显式配置为需要P2P功能的应用程序和主机,因此NAT设备可以从服务器自动分配一个P2P端口右端口块。 

5.3。保持UDP端口的一致端口绑定

   本文档的主要和最重要的建议NAT设计者认为NAT保持一致且稳定给定(内部IP地址,内部UDP之间的端口绑定端口)对和对应的(公用IP地址,公用UDP) 端口)对,只要存在使用该会话的任何活动会话端口绑定。NAT可能会在每个会话,通过检查源和目标每个数据包中的IP地址和端口号。当节点上专用网络启动与新外部设备的连接使用相同的源IP地址和UDP端口作为目标现有的已转换UDP会话,NAT应确保新的UDP会话具有相同的公用IP地址和UDP端口数字作为现有会话。

5.3.1。保留端口号

   一些NAT在建立新的UDP会话时会尝试分配与相应的专用端口号相同的公用端口号,如果该端口号恰好可用。例如,如果客户A在地址10.0.0.1处启动带有数据报的传出UDP会话从端口号1234开始,并且发生NAT的公共端口号1234可用,则NAT在NAT处使用端口号1234公共IP地址作为会话的转换端点地址。此行为可能对某些旧版UDP应用程序有利期望仅使用特定的UDP端口号进行通信,但是不建议应用程序依赖此行为,因为NAT最多只能保留端口号内部网络上的一个节点正在使用该端口号。

   此外,NAT不应尝试将端口号保留在新会议,如果这样做会与维持公共端点和私有端点地址之间的一致绑定。例如,假设内部端口1234上的客户端A建立了一个与外部服务器S的会话,并且NAT A已分配了公共端口62000到此会话,因为NAT上的端口号1234不是当时可用。现在假设NAT上的端口号1234随后变为可用,并且在A和S之间的会话仍处于活动状态,客户端A从相同会话发起一个新会话内部端口(1234)到另一个外部节点B。在这种情况下,因为客户端之间已经建立了端口绑定A的端口1234和NAT的公共端口62000,此绑定应为维护,并且新会话也应该使用端口62000作为客户端A的端口1234对应的公共端口。NAT应该不要仅因为端口将公共端口1234分配给此新会话1234已可用:这种行为不太可能因为应用程序已经以任何方式使应用程序受益使用已翻译的端口号进行操作,它将破坏任何应用程序可能尝试建立对等网络使用UDP打孔技术的连接。

5.4。维护TCP端口的一致端口绑定

   为了与UDP转换的行为保持一致,请使用锥NAT实施者还应在TCP的专用和公用(IP地址,TCP端口号)对连接,方法与上述UDP相同。  始终保持TCP端点绑定将增加NAT与启动的P2P TCP应用程序的兼容性来自同一源端口的多个TCP连接。 

5.5。P2P应用程序超时时间长

   我们建议中间盒实现者使用最小超时对于P2P应用程序而言,大约为5分钟(300秒),即 使用该端口的空闲超时配置中间盒 为P2P使用预留的端口的绑定。中间盒实施者通常倾向于使用较短的实施者,因为他们是目前已经习惯了。但是,超时时间短有问题的。考虑一个涉及16个对等点的P2P应用程序。他们每10个将用keepalive数据包淹没网络避免NAT超时的秒数。之所以这样,是因为发送它们的时间是中间盒超时的5倍 如果保持连接被丢弃在网络中。

5.6。支持环回翻译

   我们强烈建议中间盒实施者支持环回转换,允许中间盒后面的主机通过以下方式与同一个中间框后面的其他主机通信他们的公共端点,可能是翻译端点。支持在这种情况下,回送翻译尤其重要大容量NAT可能会被部署为多级NAT方案的第一级。如中所述第3.3.3节中的主机位于相同的第一层NAT之后,不同的第二层NAT无法与之通信即使所有中间盒都通过UDP打孔实现彼此交互保留端点身份,除非第一级NAT还支持回送翻译。

6.安全注意事项

   遵循本文档中的建议不应本质上会为 应用程序或中间盒。尽管如此,新的安全性如果此处描述的技术可能会造成风险没有足够的注意遵守。本节介绍应用程序可能会无意中造成的安全风险试图支持跨中间盒的P2P通信,对P2P友好的安全策略的影响中间箱。

6.1。IP地址别名

   P2P应用程序必须使用适当的身份验证机制保护他们的P2P连接免受意外混乱其他P2P连接以及恶意连接劫持或拒绝服务攻击。NAT友好的P2P应用程序必须有效地与多个不同的交互 IP地址域,但通常不知道确切的域定义这些地址的拓扑或管理策略域。尝试通过建立P2P连接时UDP打孔,应用程序可能会频繁发送数据包到达与预期主机完全不同的主机。

   例如,许多消费者级NAT设备提供DHCP默认情况下配置为分发本地站点的服务特定地址范围内的IP地址。说,特别消费者NAT设备,默认情况下,分发IP地址开头与192.168.1.100。大多数使用该NAT的私人家庭网络设备将具有带有该IP地址的主机,其中许多网络可能会在地址192.168.1.101上拥有一个主机好。如果主机A在一个专用网络上的地址192.168.1.101尝试通过UDP打孔建立连接 主机B位于另一个专用网络上的192.168.1.100此过程的一部分,主机A将发送发现数据包到 本地网络上的地址192.168.1.100,主机B将发送发现数据包到其网络上的地址192.168.1.101。显然,这些发现数据包将不会到达目标计算机,因为 这两个主机位于不同的专用网络上,但是它们非常可能会到达这些各自网络上的某些计算机此应用程序使用的标准UDP端口号引起混乱。特别是如果应用程序也正在运行在其他计算机上,并且未正确验证其身份消息。

   因此,即使没有恶意攻击者。如果一个端点(例如主机A)实际上是恶意的,然后没有适当的身份验证,攻击者可能导致主机B与专用网络上具有相同IP地址的另一台主机作为攻击者的(声称的)私人地址。由于两个端点主机A和B大概是通过以下方式相互发现的公共服务器S,并且S和B都没有任何方法可以验证A报告的私有地址,所有P2P应用程序都必须承担在成功之前,他们发现任何可疑的IP地址建立经过身份验证的双向通信。

6.2。拒绝服务攻击

   P2P应用程序和支持它们的公共服务器必须保护自己免受拒绝服务攻击,并确保攻击者无法使用它们来安装拒绝服务攻击其他目标。为了保护自己,P2P应用程序和服务器必须避免采取任何要求的措施大量的本地处理或存储资源,直到建立经过身份验证的双向通信。为了避免被用作拒绝服务攻击,P2P应用程序和服务器必须最小化它们发送到的流量的数量和速率任何新发现的IP地址,直到经过双向认证与预期目标建立了沟通。

   例如,向公共集合点注册的P2P应用程序服务器可以声称具有任何私有IP地址,或者可能有多个IP地址。一个连接良好的主机或主机组,可以共同吸引了大量的P2P连接尝试(例如,通过提供提供流行内容的服务)可以安装只需包含C即可对目标主机C进行拒绝服务攻击他们在自己注册的IP地址列表中的IP地址集合服务器。集合服务器无法验证IP地址,因为它们很可能是合法私有的对其他主机有用的网络地址网络本地通信。P2P应用协议必须因此,旨在将流量的大小和速率限制为未经验证IP地址是为了避免这种潜在的破坏 可能引起集中效应。

6.3。中间人攻击

   P2P客户端和服务器之间路径上的任何网络设备集合服务器可以挂载各种中间人伪装成NAT进行攻击。例如,假设主机A尝试向集合服务器S注册,但主机A 网络监听攻击者能够观察到此注册请求。攻击者然后可以向服务器S泛滥请求与客户的原始请求相同,除了修改后的源IP地址,例如攻击者本身。如果攻击者可以说服服务器使用攻击者的IP地址注册客户端,然后攻击者可以使自己成为所有路径上的活跃组件从服务器和其他P2P主机到原始客户端,即使攻击者最初只能监听从客户端到服务器的路径。

   客户端无法通过以下方式保护自己不受此攻击向集合服务器验证其源IP地址,因为为了使NAT友好,应用程序必须允许介入的NAT以静默方式更改源地址。这个似乎是NAT范式固有的安全弱点。应对此类攻击的唯一防御措施是让客户验证并可能加密其实际内容使用适当的更高级别的身份进行通信,以便介入的攻击者无法利用其位置。即使所有应用程序级通信都是经过身份验证和加密,但是这种攻击仍然可能用作流量分析工具,以观察客户端是谁与沟通。

6.4。对中间盒安全性的影响

   设计中间盒以保留端点身份不会削弱中间盒提供的安全性。例如,一个受端口限制的圆锥体NAT本质上不再“混杂” 比对称NAT在其策略中允许通过中间盒的传入或传出流量。只要启用了传出UDP会话和中间盒在内部和外部之间保持一致的绑定UDP端口,中间盒将过滤掉所有传入的UDP数据包与从内部发起的活动会话不匹配的会话飞地。在维护的同时积极过滤传入流量一致的端口绑定因此可以使中间盒成为“对等友好”,但不损害原则拒绝未经请求的传入流量。

   保持一致的端口绑定可能会增加通过揭示来自中间盒的流量的可预测性不同UDP会话之间的关系,因此飞地中运行的应用程序的行为。这个可预见性可能对攻击者有用利用其他网络或应用程序级别的漏洞。如果特定部署方案的安全性要求如此关键,以至于如此微妙的信息渠道但是,那么中间盒几乎肯定不应该配置为允许在第一名。这样的中间盒应该只允许通信源于特定端口的特定应用程序,或通过严格控制的应用程序级网关。在这个这种情况下,没有通用,透明的对等网络的希望中间盒(或透明客户端/服务器)之间的连接与此相关的连接性);中间盒必须实施适当的特定于应用程序的行为或禁止完全沟通。

7.致谢

   作者希望感谢Henrik,Dave和Christian Huitema获得宝贵的反馈。

8.参考

8.1。规范性引用

[BIDIR] NAT /防火墙工作委员会对等工作小组,“具有插入功能的双向对等通信防火墙和NAT”,2001年8月。
           http://www.peer-to-peerwg.org/tech/nat/

[KEGEL] Dan Kegel,“ NAT和对等网络”,1999年7月。
           http://www.alumni.caltech.edu/~dank/peer-nat.html

[MIDCOM] P. Srisuresh,J。Kuthan,J。Rosenberg,A。Molitor和A. Rayhan,“ Middlebox通信体系结构和框架”,RFC 3303,2002年8月。

[NAT-APPL] D. Senie,“网络地址转换器(NAT)友好应用设计指南”,RFC 3235,2002年1月。

[NAT-PROT] M. Holdrege和P. Srisuresh,“协议并发症使用IP网络地址转换器”,RFC 3027,2001年1月。

[NAT-PT] G. Tsirtsis和P. Srisuresh,“网络地址转换-协议转换(NAT-PT)”,RFC 2766,2000年2月。

[NAT-TERM] P. Srisuresh和M. Holdrege,“ IP网络地址转换器(NAT)术语和注意事项”,RFC2663,1999年8月。

[NAT-TRAD] P. Srisuresh和K. Egevang,“传统IP网络地址转换器(传统NAT)”,RFC 3022,2001年1月。

[STUN] J. Rosenberg,J。Weinberger,C。Huitema和R. Mahy,“ STUN-用户数据报协议(UDP)的简单遍历通过网络地址转换器(NAT)”,RFC 3489,2003年3月。

8.2。信息参考

[ICE] J. Rosenberg,“交互式连接建立(ICE):网络地址转换器(NAT)遍历的方法会话启动协议(SIP)”,rosenberg-sipping-ice-00草案(进行中),2003年2月。

[RSIP] M. Borella,J。Lo,D。Grabelsky和G.Montenegro,“领域特定的IP:框架”,RFC 3102,2001年10月。

[SOCKET] M. Leech,M。Ganis,Y。Lee,R。Kuris,D。Koblas和L. Jones,“ SOCKS协议版本5”,RFC 1928,1996年3月。

[SYM-STUN] Y. Takeda,“使用STUN进行对称NAT遍历”,draft-takeda-symmetric-nat-traversal-00.txt(在进展),2003年6月。

[TCP]“传输控制协议”,RFC 793,1981年9月。

[TEREDO] C. Huitema,“ Teredo:通过NAT通过UDP隧道IPv6”,draft-ietf-ngtrans-shipworm-08.txt(正在进行的工作),2002年9月。

[TURN] J. Rosenberg,J。Weinberger,R。Mahy和C. Huitema,“使用中继NAT进行遍历(TURN)”,draft-rosenberg-midcom-turn-01(正在进行中),2003年3月。

[UPNP] UPnP论坛,“ Internet网关设备(IGD)标准化设备控制协议V 1.0”,2001年11月。
           http://www.upnp.org/standardizeddcps/igd.asp

9.作者的地址

   布莱恩·福特计算机科学实验室麻省理工学院马萨诸塞州大街77号。
   马萨诸塞州剑桥02139
   电话:(617)253-5261
   电子邮件:baford@mit.edu
   网址:http://www.brynosaurus.com/

   Pyda SrisureshCaymas Systems,Inc.北麦克道尔大道11799-A。
   加利福尼亚Petaluma 94954
   电话:(707)283-5063
   电子邮件:srisuresh@yahoo.com

   丹·凯格尔
   Kegel.com
   901 S.Sycamore Ave.
   加利福尼亚州洛杉矶90036
   电话:323 931-6717    
   电子邮件:dank@kegel.com
   网址:http://www.kegel.com/

完整的版权声明

   版权所有(C)互联网协会(2003)。版权所有。

   本文档及其翻译本可以复制并提供给其他,以及评论或以其他方式对其进行解释的衍生作品或协助其实施,可以准备,复制,发布不受任何限制地全部或部分分发,前提是上述版权声明和本段为包括在所有此类副本和衍生作品中。但是这个不得以任何方式修改文档本身,例如通过删除版权声明或对互联网协会或其他机构的引用互联网组织,除非出于以下目的需要在这种情况下制定互联网标准的程序互联网标准流程中定义的版权必须是遵循,或根据要求将其翻译为非以下语言英语。

   上面授予的有限权限是永久性的,不会由互联网协会或其后继者或受让人撤销。

   本文档及其中包含的信息在“按现状”基础以及互联网社会和互联网工程任务组不作任何明示或暗示的担保,包括但不限于使用该信息的任何保证在此不会侵犯任何权利或默示担保特定目的的适销性或适用性。

该评论在 2021/2/2 11:02:40 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved