UDP Holepunching за NAT

Я пытаюсь реализовать простой эскиз UDP-Holepunching в Java, чтобы проверить его концепцию и использовать его в своем приложении C/С++ позже.

Концепция:

Как из Википедии я понял эту концепцию следующим образом: Пусть A и B являются клиентами за сетевой структурой undefined, а C - общедоступным общедоступным сервером.

  • A отправляет пакет на сервер C, сервер сохраняет его IP-адрес и порт. C получит публичный IP-адрес NAT. Выполняя это, NAT перед А создаст маршрут, который передаст все пакеты на этом порту в.
  • B делает то же, что и A, отправляя пакет на сервер C, который затем сохраняет его адрес и порт, B NAT создает маршрут и т.д.
  • На этом этапе C знает как адрес, так и порт каждого клиента. C отправит адрес и порт B в и от A до B.
  • A отправляет пакет в B, который будет отклонен B NAT, но при этом откроется "дыра" в NAT, позволяя дальнейшим пакетам с B пройти.
  • B отправляет пакет в A, который достигнет A, поскольку "дыра" была "пробита" раньше. Это также откроет "отверстие" в B NAT, позволяя дальнейшим пакетам с прохода A.
  • Теперь выполняется пробой, и A и B должны иметь возможность связываться друг с другом P2P

Все это хорошо работает над локальным хостом (что не является таким большим сюрпризом), но в реальном примере это не удается.

Проблема:

A и B могут подключаться к серверу C, который получает свои пакеты, сохраняет свой адрес и порт и передает его другому клиенту. Но на данный момент это терпит неудачу. A и B не могут общаться друг с другом. Поэтому я спрашиваю себя, где я ошибся. Я потратил несколько дней на поиск рабочих примеров в google и 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".

Прошу прощения за длинный код, но я хотел сохранить его.

Я был бы рад, если бы кто-нибудь из вас мог указать мне на ошибки, которые я делаю, объясните мне, почему это не работает, дайте мне рабочий пример или, по крайней мере, укажите мне альтернативу.

Ответ 1

Ваш код кажется правильным. Я тестировал ваш код, и он отлично работает. Концепция также правильна. Но, пожалуйста, проверьте, находятся ли оба клиента, которые вы запускаете, в одном и том же устройстве NAT или разных устройствах NAT. Если вы используете оба клиента под одним и тем же устройством NAT, это может не сработать, потому что не все устройства NAT поддерживают привязку волос, то есть оба клиента отправляют пакеты на внешний IP-адрес NAT, который необходимо передать самому себе. Для получения дополнительной информации см. Эту ссылку: http://tools.ietf.org/html/rfc4787#section-6

Ответ 2

Учитывая ваш концептуальный план, я думаю, что в пункте 4 есть проблема. Хотя A ударяет дыру через свой собственный NAT, когда B пытается достичь этого отверстия, он не знает о порту на NAT (или более правильно/обычно - NAPT), и, следовательно, NAT удаляет пакет, когда B пытается связаться.

Ответ 3

Только примечание для тех, кто следит за этой замечательной публикацией, обратите внимание, что на стороне сервера второй полученный UDP-пакет объявляется как: System.out.println( "(2-й) Полученный сервер:" + новая строка (байты) + "from" + address1 + "на порту" + port1); Он должен быть System.out.println( "(2-й) Полученный сервер:" + новая строка (байты) + "от" + адрес2 + "на порту" + port2); Это не имеет большого значения с момента его единственного информационного сообщения, но это заставило меня потерять некоторое время, просто задаваясь вопросом, как черт на земле маршрутизатор выдавал один и тот же порт двум различным коммуникациям: P