在NAT后进行UDP打洞

13

我正在尝试在Java中实现UDP-Holepunching的简单草图,以测试其概念并在以后将其用于我的C/C++应用程序。

概念:

根据维基百科,我理解的概念如下: 假设A和B是位于未定义网络结构背后的客户端,而C是众所周知的公共可达服务器。

  1. A向服务器C发送一个数据包,服务器保存其IP地址和端口。 C将获取A的NAT的公共IP地址。通过这样做,A前面的NAT将创建一条路由,该路由将所有经过此端口的数据包传递给A。
  2. B执行与A相同的操作,向服务器C发送一个数据包,然后服务器将其地址和端口保存下来,B的NAT创建路由,依此类推。
  3. 此时,C知道每个客户端的地址和端口。 C将把B的地址和端口发送到A,从A发送到B。
  4. A发送一个数据包到B,该数据包将被B的NAT拒绝,但这样做将在A的NAT中打开一个“洞口”,使得来自B的进一步数据包可以通过。
  5. B向A发送一个数据包,该数据包将到达A,因为先前已经“打了一个洞”。这样做也将在B的NAT中打开一个“洞口”,使得来自A的进一步数据包可以通过。
  6. 现在完成了Holepunch,A和B应该能够进行点对点通信

这在本地主机上效果很好(这不是很出乎意料),但在真实世界的例子中会失败。

问题:

A和B都能连接到服务器C,服务器接收它们的数据包,存储它们的地址和端口,并将其传输给另一个客户端。 但在此时,它失败了。 A和B无法相互通信。 所以我想知道我做错了什么。 我花了几天时间在谷歌和stackoverflow上寻找可用的示例,但所有我遇到的都建议使用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”的数据包。

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

如果你们中任何人能指出我犯的错误,解释为什么这样不起作用,给我一个可行的例子或至少指引我一条替代之路,我将不胜感激。

3个回答

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

2

注意,对于那些正在关注这篇很棒的文章的人,需要在服务器端注意第二个接收到的UDP数据包是这样宣布的:System.out.println("(2nd) Server received:" + new String(bytes) + " from " + address1 + " on port " + port1); 应该是这样的:System.out.println("(2nd) Server received:" + new String(bytes) + " from " + address2 + " on port " + port2); 尽管这只是一条信息提示,但它让我浪费了一些时间,因为我不知道路由器是如何将相同的端口分配给两个不同的通信的:P


2

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接