NAT 后面的 UDP 打孔

Posted

技术标签:

【中文标题】NAT 后面的 UDP 打孔【英文标题】:UDP Holepunching behind NAT 【发布时间】:2013-10-05 21:24:51 【问题描述】:

我正在尝试在 Java 中实现一个简单的 UDP-Holepunching 草图来测试它的概念并稍后在我的 C/C++ 应用程序中使用它。

概念:

根据***,我将这个概念理解为: 假设 A 和 B 是一个未定义的网络结构背后的客户端,而 C 是一个众所周知的公共可达服务器。

    A 向服务器 C 发送一个数据包,服务器保存它的 IP 地址和端口。 C 将获得 A 的 NAT 的公共 IP 地址。这样做,A 前面的 NAT 将创建一个路由,将这个端口上的所有数据包传递给 A。 B 和 A 做同样的事情,向服务器 C 发送一个数据包,然后服务器 C 会保存它的地址和端口,B 的 NAT 创建一个路由等等。 此时,C 知道每个客户端的地址和端口。 C 将 B 的地址和端口发送给 A,从 A 发送给 B。 A 向 B 发送一个数据包,该数据包将被 B 的 NAT 拒绝,但这样做会在 A 的 NAT 中打开一个“漏洞”,让更多来自 B 的数据包通过。 B 向 A 发送一个数据包,该数据包将到达 A,因为之前“打孔”了一个“洞”。这样做也会在 B 的 NAT 中打开一个“洞”,让更多来自 A 的数据包通过。 打孔现已完成,A 和 B 应该能够相互 P2P 通信

这一切都在 localhost 上运行良好(这并不令人意外),但在现实世界的示例中却失败了。

问题:

A 和 B 都能够连接到服务器 C,服务器 C 获取它们的数据包,存储它们的地址和端口并将其传输给另一个客户端。 但在这一点上它失败了。 A 和 B 无法相互通信。 所以我问自己哪里做错了。我花了几天时间在 google 和 *** 中搜索工作示例,但我偶然发现的是使用 STUN 的建议,这不是我想要的。

实施:

下面我将用 Java 发布我的草图,因为我不知道我的概念或实现是否有问题。

这是服务器的代码:

public class Server

    public static void main(String[] args)
    
        int port1 = 0, port2 = 0;
        String address1 = null, address2;
        byte[] bytes = new byte[1024];
        try
        
            System.out.println("Server waiting");
            DatagramSocket ds = new DatagramSocket(789);
            while(!Thread.interrupted())
            
                DatagramPacket p = new DatagramPacket(bytes, bytes.length);
                ds.receive(p);
                if(port1 == 0)
                
                    port1 = p.getPort();
                    address1 = p.getAddress().getHostAddress();
                    System.out.println("(1st) Server received:" + new String(bytes) + " from " + address1 + " on port " + port1);
                
                else
                
                    port2 = p.getPort();
                    address2 = p.getAddress().getHostAddress();
                    System.out.println("(2nd) Server received:" + new String(bytes) + " from " + address1 + " on port " + port1);
                    sendConnDataTo(address1, port1, address2, port2, ds);
                    sendConnDataTo(address2, port2, address1, port1, ds);
                
            
            ds.close();
        
        catch(Exception e)
        
            e.printStackTrace();
        
    

    public static void sendConnDataTo(String a1, int p1, String a2, int p2, DatagramSocket ds)
    
        byte[] bA, bP;
        bA = a1.getBytes();
        bP = Integer.toString(p1).getBytes();
        DatagramPacket pck;
        try
        
            pck = new DatagramPacket(bA, bA.length, InetAddress.getByName(a2), p2);
            ds.send(pck);
            pck = new DatagramPacket(bP, bP.length, InetAddress.getByName(a2), p2);
            ds.send(pck);
        
        catch(Exception e)
        
            e.printStackTrace();
        
    

请注意,这只是一些草图,没有真正的应用。服务器应该只接收来自两个客户端的数据包,保存它们的地址和端口并将其传递给另一个客户端。

这是客户端的代码:

public class Client

    private DatagramSocket socket;
    private int init = 0;
    private String target;
    private int port;

    public Client()
    
        try
        
            socket = new DatagramSocket();
        
        catch(SocketException e)
        
            e.printStackTrace();
        
        Thread in = new Thread()
        
            public void run()
            
                while(true)
                
                    byte[] bytes = new byte[1024];
                    DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
                    try
                    
                        socket.receive(packet);
                        bytes = Arrays.copyOfRange(bytes, 0, packet.getLength());
                        String s = new String(bytes);
                        System.out.println("Received: " + s);
                        if(init == 0)
                        
                            target = s;
                            System.out.println("Target: " + target);
                            init++;
                        
                        else if(init == 1)
                        
                            port = Integer.parseInt(s);
                            System.out.println("Port: " + port);
                            init++;
                        
                        else System.out.println(new String(bytes));
                    
                    catch(IOException e)
                    
                        e.printStackTrace();
                    
                
            
        ;
        in.start();
        connectToSupervisor();
    

    private void connectToSupervisor()
    
        byte[] bytes = new byte[1024];
        System.out.println("Greeting server");
        System.arraycopy("EHLO".getBytes(), 0, bytes, 0, 4);
        try
        
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("localhost"), 789);
            socket.send(packet);
            System.out.println("Greetings sent...");
        
        catch(IOException e)
        
            e.printStackTrace();
        
        send();
    

    private void send()
    
        while(init != 2)
        
            try
            
                Thread.sleep(20L);
            
            catch(InterruptedException e)
            
                e.printStackTrace();
            
        
        System.out.println("Init completed!");
        while(true)
        
            byte[] b2 = "Hello".getBytes();
            byte[] b1 = new byte[6];
            System.arraycopy(b2, 0, b1, 0, b2.length);
            try
            
                DatagramPacket packet = new DatagramPacket(b1, b1.length, InetAddress.getByName(target), port);
                socket.send(packet);
            
            catch(Exception e)
            
                e.printStackTrace();
            
        
    

    public static void main(String[] args)
    
        new Client();
    

客户端只会向服务器发送一个数据包,监听来自它的数据包,从另一个客户端获取连接数据,然后会不断地向它发送包含“Hello”的数据包。

很抱歉代码太长,但我想保持完整。

如果你们中的任何人能指出我正在做的错误,解释我为什么这不起作用,给我一个可行的例子,或者至少给我指出一个替代方案,我会很高兴。

【问题讨论】:

【参考方案1】:

您的代码似乎是正确的。我测试了你的代码,它工作正常。这个概念也是正确的。但请检查您运行的两个客户端是在同一个 NAT 设备内还是在不同的 NAT 设备内。如果您在同一个 NAT 设备下运行两个客户端,那么它可能无法工作,因为并非所有 NAT 设备都支持发夹,即两个客户端都将数据包发送到 NAT 的外部 IP,该外部 IP 需要传递给它自己。有关更多信息,请参阅此链接: https://www.rfc-editor.org/rfc/rfc4787#section-6

【讨论】:

【参考方案2】:

鉴于您的概念大纲,我认为第 4 点存在问题。虽然 A 在自己的 NAT 上打了一个洞,但当 B 试图到达这个洞时,它并不知道 A 的 NAT 上的端口(或更正确/通常- NAPT),因此当 B 尝试通信时,A 的 NAT 会丢弃数据包。

【讨论】:

【参考方案3】:

对于那些关注这篇精彩文章的人来说,请注意,在服务器端,收到的第二个 UDP 数据包被宣布为:System.out.println("(2nd) Server received:" + new String( bytes) + " from " + address1 + " on port " + port1); 应该是 System.out.println("(2nd ) 服务器收到:" + new String(bytes) + " from " + address2 + " on port " + port2); 这没什么大不了的只是一条信息性消息,但它让我失去了一些时间,只是想知道路由器到底是如何将同一个端口提供给 2 个不同的通信:P

【讨论】:

以上是关于NAT 后面的 UDP 打孔的主要内容,如果未能解决你的问题,请参考以下文章

UDP 打孔在非对称 NAT 上失败

UDP打孔到期[关闭]

libutp (µTP) 和 NAT 遍历(UDP 打孔)

NAT 后面的 TCP 连接

NAT 后面的 UDP P2P 连接

用于 UDP NAT 打孔的 PHP 和 Java...?