LINUX.ORG.RU

tcp p2p за NAT при помощи внешнего сервера. Реально?

 , , ,


0

2

Заинтересовался сабжем. Дабы проверить - написал такой быдлокод, который делает (должен делать) следующее:

1) Сервер ждет соединений на порту 10222

2) Клиент подключается к серверу, узнает порт своего исходящего соединения и поднимает на этом порту свой сервер

3) Сервер пытается установить соединение с IP и портом создавшим это соединение (здесь IP и порт вашего провайдера, который, теоретически, должен нисмотря ни на что ссылаться на ваш внутренний IP и порт, это соединение открывший)

К сожалению, при запуске клиента за NAT получаю Connection Refused

Часть 1 - серверная сторона:

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;


public class SimpleServer {
    ServerSocket ss;
    
    public void startServer() throws IOException{
        ss = new ServerSocket(10222);
        System.out.println("Сервер запущен по адресу: " + ss.getLocalSocketAddress());
    }
    
    public void stopServer() throws IOException{
        ss.close();
    }
    
    public void waitConnection() throws IOException, InterruptedException{
            Socket s = ss.accept();
            
            int remotePort = s.getPort();
            InetAddress host = s.getInetAddress();

            System.out.println("Пытаюсь установить соедиение с " + host.getHostAddress() + " на порт: " + remotePort);
            
            //попытка установления соединения с клиентом, который в этот момент ожидает соединения
            Thread.sleep(1000);
            Socket socket = new Socket(host, remotePort);
            System.out.println("Соединение установлено, отправляем тестовые данные");
            
            //Отправка данных
            OutputStream os = socket.getOutputStream();
            os.write("Hello, client!".getBytes());
            
            s.close();

    }
    
    public static void main(String[] args) throws InterruptedException, IOException{
        SimpleServer ss = new SimpleServer();
        ss.startServer();
        ss.waitConnection();
        ss.stopServer();
    }
}

Часть 2 - сторона клиента:

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleClient implements Runnable{
    SimpleClient(){}
    
    //
    SimpleClient(Socket socket){
        this.socket = socket;
        Thread t = new Thread(this);
        t.start();
    }
    Socket socket;
    
    String server = "192.168.10.10";
    int port;
    public void connect(){
        try {
            //Подключаемся к серверу
            Socket socket = new Socket(server, 10222);
            port = socket.getLocalPort();
            
            System.out.println("Обнаружен локальный порт: " + port);
            socket.setReuseAddress(true);
            
            //Создаем свой сервер на порту, использованному при исходящем соединении
            ServerSocket serverSocket = new ServerSocket(port);
            
            //Ждем соединений с созданного сервера
            while(true){
                System.out.println("Ожидание соединения на порту: " + port);
                Socket gotSocket = serverSocket.accept();
                System.out.println("Получено соединение");
                new SimpleClient(gotSocket); //Запустить поток, который будет читать сокет
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    //Чтение данных из сокета
    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            int c;
            while( (c = is.read()) != -1 ){
                System.out.print((char) c);
            }
        } catch (IOException e) {
            System.err.println("Ошибка ввода вывода");
        }
    }
    
    public static void main(String[] args){
        new SimpleClient().connect();
    }
}

★★★★★

Ты делаешь дичь. Вообще то, что ты хочешь называется hole punching и с tcp это не то, чтоб сложно делать, но можно по граблям походить да.

anonymous ()
Ответ на: комментарий от anonymous

как эти грабли хоть найти, чтобы по ним походить?

Пока что имею только Connection refused

r0ck3r ★★★★★ ()
Ответ на: комментарий от r0ck3r

Попробуй дропать RST на время установления соединения там, где у тебя «connection refused» вылезает, вот так:

iptables -I INPUT -p tcp --dport 10222 --tcp-flags RST RST -j DROP
Также для пробивания ната надо инициировать соединение с обеих сторон, предварительно забиндив порт. Если нат не будет подменять src порты, то должно сработать.

Также могут быть проблемы, если оба ната отвечают RST на попытки соединения.

slvrn ★★★ ()

Нужен именно TCP?

У меня получалось установить UDP-«соединение», т.е. пошли пакеты в обе стороны у двух клиентов за NAT (мобильный Мегафон и Йота). Тестировал nc и tcpdump-ом, запускал их прямо на трех хостах и смотрел порты.

Но это, мне кажется, игрушки - как только один из провайдеров изменит алгоритм ната, все развалится. Гонять весь трафик через «сервер», раз уж он есть, надежнее

vmx ★★ ()
Ответ на: комментарий от slvrn

с этим правилом и если установить исходящий порт на сервере при открытии соединения 10222 получаю Connection timeout...А при инициализации соединения с любого непривилегированного порта - Connection refused

r0ck3r ★★★★★ ()
Ответ на: комментарий от vmx

Хотелось бы именно TCP...А с UDP, насколько я понимаю, проблем вообще быть не должно, ведь торрент-клиенты работают именно таким образом? Мне кажется, что изменение алгоритма ната врядли все разрушит

Понимаю что надежнее через сервер, просто если гонять кучу данных через сервер - он быстро загнется..

Для укрепления знаний Java планирую заняться разработкой мессенджера, в котором, для пересылки файлов хотелось бы использовать p2p

r0ck3r ★★★★★ ()
Ответ на: комментарий от Radjah

для STUN нужен сервак с терабайтами лимита. У нас tcp HP работал между 2 конторами. Но поипстить пришлось, да. Мое мнение - проще поднять udp канал с hole punching (я кста всего один раз видел, когда эта штука не работала, так что про «изменят нат» - возможно, но маловероятно) и в нее уже завернуть vpn.

anonymous ()
Ответ на: комментарий от Radjah

STUN это же уже готовая реализация. Мне было интересно сделать вручную

Крафтовый STUN, да

vmx ★★ ()
Ответ на: комментарий от anonymous

stun вроде же только должен сообщить ip и порт? Зачем там терабайты?

r0ck3r ★★★★★ ()
Ответ на: комментарий от r0ck3r

А соединение у тебя с двух сторон инициируется, или только с одной?

slvrn ★★★ ()
Ответ на: комментарий от r0ck3r
#!/usr/bin/python2

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((BIND_IP,10222))
sock.connect((REMOTE_IP,10222))
sock.send("hello")
data=sock.recv(1024)
print data
sock.close()

Такой код пробивает нат, результат - «hello» в консоли. Запускать надо с двух сторон. BIND_IP - локальный адрес, с которого соединение уходит и на который должно придти, REMOTE_IP - внешний адрес удаленного ната. Один из натов дропает весь «неизвестный» трафик, поэтому не посылает RST, поэтому надо начинать с попытки приконнектиться к нему.

Оба ната работают на линуксе, за другие ОС говорить не берусь.

slvrn ★★★ ()
Последнее исправление: slvrn (всего исправлений: 1)
Ответ на: комментарий от slvrn

Соединение инициализируется с одной стороны. Алгоритм следующий:

PC - компьютер за натом

VPS - ВПС с выделенным IP

1) PC инициирует подключение к VPS на порт 10222

2) VPS получает IP и порт подключения (remoteHost и remotePort)

3) PC получает порт исходящего соединения и начинает ждать соединений на этом порту

4) VPS пытается установить соединение с remoteHost и remotePort, что приводит либо к Connection refused, если он используют динамический исходящий порт, либо к Connection timeout, если он использует исходящий 10222 порт

r0ck3r ★★★★★ ()
Последнее исправление: r0ck3r (всего исправлений: 2)
Ответ на: комментарий от slvrn

Вот это странно

насколько мне известно нат не гарантирует сохранения номера исходящего порта...То есть, если мы с порта 40567 своего PC открываем соединение с linux.org.ru:443, то на linux.org.ru:443 придет соединение с порта NAT, скажем, 37833

r0ck3r ★★★★★ ()
Ответ на: комментарий от r0ck3r

VPS пытается установить соединение с remoteHost и remotePort

Так не получится. С точки зрения сетевого стека ты пытаешься в текущем соединении установить его еще раз (IP-адреса и порты одни и те же). Или, если соединение уже разорвано, запись в NAT-таблице удалена.

Даже с UDP не очень просто.

Вот я щас попробовал NAT все того же Мегафона, если через 5 секунд после первого UDP-пакета не было ответа, запись не создается, пакет назад не доходит. Ну, может быть это только при инициализации, дальше сессия держится дольше. Но все равно придется слать heartbeats, подбирать таймауты. Плюс мобильные устройства часто рвут соединение и меняют адреса, это тоже придется как-то обрабатывать.

То есть я не отговариваю, задача интересная. Но все равно лучше оставить fallback для клиентов которые не смогут пробить NAT. В корпоративных сетях часто встречаются параноидальные настройки фильтрации и ната.

vmx ★★ ()
Ответ на: комментарий от r0ck3r

Вот это странно

насколько мне известно нат не гарантирует сохранения номера исходящего порта...То есть, если мы с порта 40567 своего PC открываем соединение с linux.org.ru:443, то на linux.org.ru:443 придет соединение с порта NAT, скажем, 37833

Не гарантирует. Но может получиться, что номер порта не изменится при трансляции, тут уже надо экспериментировать с каждым натом. По идее, внешний сервер можно использовать как раз для таких тестов, т.е. для выяснения паттернов трансляций. Но для провайдерского ната с кучей клиентов позади все равно может не прокатить.

Соединение инициализируется с одной стороны. Алгоритм следующий:

PC - компьютер за натом

VPS - ВПС с выделенным IP

1) PC инициирует подключение к VPS на порт 10222

2) VPS получает IP и порт подключения (remoteHost и remotePort)

3) PC получает порт исходящего соединения и начинает ждать соединений на этом порту

4) VPS пытается установить соединение с remoteHost и remotePort, что приводит либо к Connection refused, если он используют динамический исходящий порт, либо к Connection timeout, если он использует исходящий 10222 порт

Так не получится. В основе tcp hole punching лежит tcp simultaneous open, когда два пира, грубо говоря, одновременно коннектятся друг к другу. Это можно сэмулировать и для впс с публичным адресом, но гораздо проще поднять там сервер.

Еще есть такая штука, как UPnP, оно позволит тебе пробросить порт до твоего приложения. Это обычно работает только в домашних сетках, при условии, что роутер его поддерживает, а также получает публичный адрес от провайдера.

В общем, в условиях интернета ты не всегда сможешь пробить нат. Печально, но правда.

С UDP данный принцип вообще не будет работать через наты на линуксе. Связано это с тем, что нат по-разному создает трансляции для TCP и UDP. Вначале я думал, что и для TCP не прокатит, и весьма удивился, когда все это заработало.

slvrn ★★★ ()

Способ, который ты описал должен работать для UDP, но не для TCP. (Подозреваю, это связано с тем, что TCP - это протокол, с запоминаемым состоянием).

Для перфорации TCP-натов обычно используют дополнительные SYN-пакеты. Подробно это технология гуглится. Вот например скачай PDF:
https://www.researchgate.net/publication/221137405_SYNI_-_TCP_hole_punching_b...

Novator ★★★★ ()
Ответ на: комментарий от vmx

сегодня-завтра найду время и буду пробовать реализовать тоже самое через UDP

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

r0ck3r ★★★★★ ()
Ответ на: комментарий от slvrn

грустно...все равно буду пробовать смастерить свой велосипед, а не получится - буду искать готовые реализации

r0ck3r ★★★★★ ()
Ответ на: комментарий от Novator

Чтиво скачал - премного благодарен. Попробую для начала с UDP, чтобы оно работало, потом все-таки займусь TCP

r0ck3r ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.