TCP Hole Punching - bezpośrednie połączenie urządzeń korzystających z NAT'a

0

W ramach eksperymentu próbuję połączyć bezpośrednio do siebie dwa urządzenia żeby wymienić między nimi jakieś dane. Urządzenia docelowo mają być w dwóch zupełnie oddzielnych sieciach, mają zmienne IP i powinny móc skomunikować się ze sobą bez potrzeby otwierania portów na routerze. Moim celem jest przesłanie pliku z telefonu podłączonego do BTS'a (sieci komórkowej) na komputer w domowej sieci lokalnej.

Opierając się na jakichś szczątkowych wskazówkach z internetu, dowiedziałem się że potrzebny jest publiczny serwer VPS który przesyłałby publiczne IP obu chcących połączyć się urządzeń. Tak też zrobiłem: urządzenia A i B łączą się z serwerem S, S przesyła w odpowiedzi publiczny adres B do A i adres A do B. Według tego co wyczytałem, A wysyłając pakiet do S otwiera dostępny publicznie port na routerze, port ten może być wykorzystany przez B do "oszukania" NAT'a. U mnie działa to tak, że A i B próbują, od razu po otrzymaniu adresu i portu z serwera, nawiązać połączenie TCP ze sobą używając tych danych.

Niestety powyżej opisany sposób nie działa. Z tego co widzę w Wiresharku, oba urządzenia wysyłają poprawnie pakiet SYN do poprawnego adresu i portu, jednak nie otrzymują żadnej odpowiedzi.

Byłbym wdzięczny za jakieś wskazówki. Istnieje jakiś sposób żeby sprawdzić w czym problem? Dlaczego pakiety są odrzucane? Albo czy jest inny sposób na nawiązanie połączenia?

0

Musisz użyć omijania NAT-u dla UDP, np. STUN i poziom wyżej zaimplementować te funkcje TCP, które są potrzebne, może RUDP wystarczy.

Bezpośrednie użycie TCP może zadziałać tylko wtedy, gdy router przepuszcza błędne pakiety, takie jak SYN w odpowiedzi na inny SYN.

Inną kwestią jest to, że nie wiem, czy wszędzie dbasz o prawidłowe numery portów źródłowych, a od nich zależy wszystko. Oczywiście jeżeli NAT jest symetryczny, możesz mieć bardzo słabą kontrolę (jeżeli między przepływami UDP minie niewiele czasu, drugi z tego samego wewnętrznego źródłowego portu będzie miał zewnętrzny źródłowy port o kilka większy niż pierwszy, określenie wartości "kilka" wymaga spróbowania różnych, ale lepsze to niż całkowicie nieznany) lub żadnej (jeżeli NAT zastępuje porty losowymi).

0

Musisz użyć omijania NAT-u dla UDP, np. STUN i poziom wyżej zaimplementować te funkcje TCP, które są potrzebne, może RUDP wystarczy.

Czy STUN nie służy po prostu do wykrywania publicznego adresu IP i portu hosta? To mam już zrobione, serwer przesyła hostom nawzajem ich adresy IP. Czyli powinienem w tym przypadku zamiast komunikacji TCP użyć UDP i ten sam sposób który opisałem powyżej zadziała?

Inną kwestią jest to, że nie wiem, czy wszędzie dbasz o prawidłowe numery portów źródłowych, a od nich zależy wszystko. Oczywiście jeżeli NAT jest symetryczny, możesz mieć bardzo słabą kontrolę (jeżeli między przepływami UDP minie niewiele czasu, drugi z tego samego wewnętrznego źródłowego portu będzie miał zewnętrzny źródłowy port o kilka większy niż pierwszy, określenie wartości "kilka" wymaga spróbowania różnych, ale lepsze to niż całkowicie nieznany) lub żadnej (jeżeli NAT zastępuje porty losowymi).

Tu też nie do końca rozumiem. Co to znaczy "prawidłowe" numery portów źródłowych? Powinienem użyć jakichś konkretnych? I o co chodzi z "przepływami UDP"? Każdy wysłany przeze mnie pakiet będzie miał inny port źródłowy?

3
iteredi napisał(a):

Czy STUN nie służy po prostu do wykrywania publicznego adresu IP i portu hosta? To mam już zrobione, serwer przesyła hostom nawzajem ich adresy IP. Czyli powinienem w tym przypadku zamiast komunikacji TCP użyć UDP i ten sam sposób który opisałem powyżej zadziała?

Tutaj ten port jest bardzo ważny, a nie wiem, czy w ogóle zwracasz na niego uwagę. Niezależnie od wszystkiego, w praktyce musisz użyć UDP, bo w TCP musiałbyś liczyć na to, że któryś z routerów przepuści nieprawidłowy pakiet (pierwszy przepuści SYN/ACK bez wcześniejszego SYN, ewentualnie drugi przepuści SYN jako odpowiedź na SYN) i jeszcze byłyby potrzebne raw sockets, a podczas komunikacji UDP nie ma różnicy między pierwszym a drugim pakietem, a całe omijanie NAT-u opiera się na oszukiwaniu go, że coś jest odpowiedzią na wcześniej wysłany pakiet, pomimo tego że naprawdę nie jest.

Musisz uważać, skąd wysyłasz pakiet do serwera STUN - raczej nie z portu, który system operacyjny wybrał automatycznie, chyba że śledzisz, jaki wybrał.

Jeżeli założymy, że nie trzeba obsługiwać symetrycznych NAT-ów, tylko wszystkie inne, z których najgorszym rodzajem jest port-restricted:

  • musisz wymyślić jakieś dwa porty źródłowe, załóżmy że są to 11000 i 13000, nie możesz pozwalać systemowi na ich wybór
  • załóżmy, że pierwsze urządzenie ma wewnętrzny IP 192.168.1.5 i zewnętrzny 87.205.x.x
  • załóżmy, że drugie urządzenie ma zewnętrzny IP 192.168.60.2 i zewnętrzny 188.99.x.x
  • na pierwszym urządzeniu musisz wysłać do serwera STUN żądanie, ustawiając źródłowy IP/port gniazda na 192.168.1.5/11000, dostaniesz odpowiedź, że żądanie przyszło z IP i portu 87.205.x.x/13521 (tak, router może zmienić port, chociaż najpopularniejszy netfilter tego nie robi, gdy nie musi)
  • na drugim urządzeniu musisz wysłać do serwera STUN żądanie, ustawiając źródłowy IP/port gniazda na 192.168.60.2/13000, dostaniesz odpowiedź, że żądanie przyszło z IP i portu 188.99.x.x/14222
  • urządzenia muszą przez jakiś serwer wymienić się tymi zewnętrznymi IP i portami
  • pierwsze urządzenie musi z gniazda przypisanego funkcją bind() lub podobną do 192.168.1.5/11000 wysłać coś do 188.99.x.x/14222, router przed pierwszym urządzeniem zrobi z tego 87.205.x.x/13521 ->188.99.x.x/14222, router po drugiej stronie nie będzie wiedział, co z tym zrobić i odrzuci, ale wysłanie spowoduje że router przed pierwszym urządzeniem na chwilę "przekieruje port" w celu odebrania potencjalnych odpowiedzi, ale jako że jest port-restricted, to przekierowanie będzie obowiązywało tylko dla datagramów przychodzących od 188.99.x.x/14222
  • drugie urządzenie musi z gniazda przypisanego do 192.168.60.2/13000 wysłać coś do 87.205.x.x/13521, router przed drugim urządzeniem zrobi z tego 188.99.x.x/14222 -> 87.205.x.x/13521, router po drugiej stronie potraktuje to jako odpowiedź na ten wcześniej wysłany datagram i przekieruje do pierwszego urządzenia, router przed drugim urządzeniem na chwilę "przekieruje port", aby odbierać odpowiedzi na to, co właśnie wysłał
  • pierwsze urządzenie odbierze na swoim porcie 11000 to, co w powyższym punkcie wysłało drugie urządzenie
  • teraz pierwsze urządzenie może coś wysłać z gniazda przypisanego do 192.168.1.5/11000 do 188.99.x.x/14222, router przed pierwszym urządzeniem zrobi z tego 87.205.x.x/13521 ->188.99.x.x/14222, router po drugiej stronie potraktuje jako odpowiedź i przekaże do urządzenia drugiego
  • drugie urządzenie odbierze na swoim porcie 13000 to, co w powyższym punkcie wysłało pierwsze urządzenie
    i masz dwukierunkową komunikację UDP - cztery wyższe punkty możesz powtarzać, możesz też wysyłać więcej niż jeden datagram bez czekania za każdym razem na odpowiedź. Teraz przydałoby się użyć jakiegoś RUDP, QUIC czy innego protokołu robiącego ponad UDP coś podobnego do TCP - numerującego pakiety, ponownie wysyłającego w przypadku nieodebrania, regulującego szybkość itp.

Z symetrycznym NAT-em (w konsolach do gier oznaczonym "Type 3" lub "Strict"), a operator komórkowy może takiego używać, problem jest taki, że przy używaniu STUN-a przetłumaczy np. 192.168.1.5/11000 na 87.205.x.x/13521, ale przy późniejszej właściwej komunikacji przetłumaczy np. 192.168.1.5/11000 na 87.205.x.x/13530, co jeszcze można rozwiązać jak Skype, próbując wszystkie porty od 13521 do np. 13541, ale jeżeli wybiera całkowicie losowo, nic się nie uda, stąd też symetryczny NAT jest w ogólności nie do obejścia, chyba że są specyficzne przypadki, np. router po drugiej stronie nie jest port-restricted, tylko traktuje jako odpowiedzi pakiety z niepasującym portem źródłowym (odpowiedzią na pakiet A:B->C:D jest dla niego pakiet C:cokolwiek->A:B, niekoniecznie C:D->A:B albo nawet jest full cone, czyli przyjmuje cokolwiek:cokolwiek->A:B), ale takich routerów jest niewiele. Warto zauważyć, że przy rzadkiej sytuacji: po jednej stronie NAT symetryczny ("Type 3" w PlayStation), po drugiej niesymetryczny inny niż port-restricted ("Type 1" w PlayStation) komunikację musi rozpocząć urządzenie za niesymetrycznym NAT-em (wysyłając coś na dowolny zamknięty port) - jeżeli zastosowanie wymaga, żeby było odwrotnie, trzeba zrobić callback przez serwer, tzn. jedno urządzenie prosi serwer, żeby poinformował drugie, że drugie musi nawiązać komunikację z pierwszym.

Co to znaczy "prawidłowe" numery portów źródłowych? Powinienem użyć jakichś konkretnych?

Musisz uważać na numery portów źródłowych, podałem przykład wyżej, inaczej nic się nie uda.

Przypominam, że pakiet ma IP źródłowy, port źródłowy, IP docelowy, port docelowy. Jeżeli, tak jak jest w większości programów klienckich, nie ustawiasz tych dwóch pierwszych bind() lub czymś podobnym, system ustawia: IP karty sieciowej powiązanej z bramą domyślną i automatycznie wybrany wysoki port.

I o co chodzi z "przepływami UDP"?

Przepływ to "połączenie", tylko nie można go tak nazwać, bo UDP jest bezpołączeniowy. Router z NAT-em musi wiedzieć, czy coś jest odpowiedzią, czy czymś całkowicie niepowiązanym. Dla większości routerów przepływem jest zbiór pakietów mających taką samą czwórkę IP źródłowy, port źródłowy, IP docelowy, port docelowy, wraz z odpowiedziami, czyli pakietami mającymi zamienione IP/porty źródłowe z docelowymi, gdzie nie może być za dużego odstępu czasowego np. większego niż 3-minutowy bez żadnego ruchu. W związku z tym będziesz też musiał zrobić jakiś keep-alive, tzn. przesyłanie śmieciowych pakietów np. co 30 sekund, jeżeli chcesz żeby router nie przestawał odbierać przychodzących danych.

2

Cześć
@valdemar bardzo dobra odpowiedź. Przebicie UDP działa i działało u mnie tak samo jak to opisałeś. Natomiast jeśli chodzi o TCP Możliwe że ja coś źle piszę ale nie jestem początkującym programistą.

Oczywiście skrócę trochę, zakładamy że coś tak trywialnego jak połączenie z serwerem zewnętrznym już mamy :)

Użyjemy oczywiście TcpListener

public void InitializeServer(IPAddress address, int port)
        {
            try
            {
                // 127.0.0.1 accept only local connections, 0.0.0.0 is open for whole internet connections

                listener = new TcpListener(address, port);
                socket = listener.Server;

                // Enable NAT Translation
                listener.AllowNatTraversal(true);

                // Start listening for example 10 client requests.
                listener.Start(listenQueue);

                Debug.Log($"[L{socket.LocalEndPoint}]Server start... ", EDebugLvl.Log);

                OnServerInitialize(true);

                // Enter the listening loop.
                StartListener();
            }
            catch (SocketException e)
            {
                Debug.LogError($"SocketException: {e}", EDebugLvl.Error);
                OnServerInitialize(false);
            }
        }

Rozpoczynamy nasłuch

private void StartListener()
        {
            Debug.Log("\nWaiting for a connection... ");
            listener.BeginAcceptTcpClient(AcceptCallback, listener);
        }

W momencie gdy serwer odbierze połączenie tworzymy nowe gniazdo

private void AcceptCallback(IAsyncResult ar)
        {
            TcpListener server = (TcpListener)ar.AsyncState;
            TcpClient newClient = null;

            try
            {
                newClient = server.EndAcceptTcpClient(ar);
            }
            catch (Exception e)
            {
                Debug.LogError(e.ToString());
            }

            if (newClient != null && newClient.Connected)
            {

                //...

                client.StartRead();
            }

            //Loop
            StartListener();
        }

U klienta tworzymy nowe gniazdo i próbujemy nawiązać połączenie

public void Connect(IPEndPoint remote, IPEndPoint bind = null, bool reuseAddress = false)
        {
            if (bind == null)
            {
                client = new TcpClient();
            }
            else
            {
                client = new TcpClient(bind);
            }
            
            socket = client.Client;

            if (reuseAddress)
            {
                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, reuseAddress);
                //To mi wyrzuca błąd.
                //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseUnicastPort, reuseAddress);
            }

            client.BeginConnect(remote.Address, remote.Port, ConnectCallback, null);
        }

Połączenie działa bez problemu oraz przesyłanie danych.

Niestety tutaj musimy rozpocząć nowym gniazdem nasłuch na tym samym adresie oraz porcie które zostało utworzone przy połączeniu z serwerem. Robię to u każdego klienta.

public void StartHost(Client server)
        {
            if (server != null && server.socket.Connected)
            {
                IPEndPoint localHost = (IPEndPoint)server.socket.LocalEndPoint;
                InitializeHost(localHost.Address, localHost.Port);
            }
        }
public void InitializeHost(IPAddress address, int port, bool reuse = false)
        {
            try
            {
                listener = new TcpListener(address, port);
                socket = listener.Server;

                if (reuse)
                {
                    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseUnicastPort, true);
                }

                // Enable NAT Translation
                listener.AllowNatTraversal(true);

                // Start listening for example 10 client requests.
                listener.Start(listenQueue);

                Debug.Log($"\n[L{socket.LocalEndPoint}]Host start... ", EDebugLvl.Log);

                OnServerInitialize(true);

                // Enter the listening loop.
                StartListener();
            }
            catch (SocketException e)
            {
                Debug.LogError($"SocketException: {e}", EDebugLvl.Error);
                OnServerInitialize(false);
            }
        }
private void StartListener()
        {
            Debug.Log("\nWaiting for a connection... ");
            listener.BeginAcceptTcpClient(AcceptCallback, listener);
        }
private void AcceptCallback(IAsyncResult ar)
        {
            TcpListener server = (TcpListener)ar.AsyncState;
            TcpClient newClient = null;

            try
            {
                newClient = server.EndAcceptTcpClient(ar);
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            if (newClient != null && newClient.Connected)
            {

                //...

                client.StartRead();
            }

            //Loop
            StartListener();
        }

A więc tak jak piszą wszędzie... klient B wysyła do serwera pakiet że chce nawiązać połączenie, serwer wysyła do klienta A informacje o kliencie B i na odwrót

Po czym oboje próbują nawiązać połączenie nowym gniazdem

public void Connect(IPEndPoint remote, IPEndPoint bind = null, bool reuseAddress = false)
        {
            if (bind == null)
            {
                client = new TcpClient();
            }
            else
            {
                client = new TcpClient(bind);
            }
            
            socket = client.Client;

            if (reuseAddress)
            {
                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, reuseAddress);
                //socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseUnicastPort, reuseAddress);
            }

            client.BeginConnect(remote.Address, remote.Port, ConnectCallback, null);
        }
private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                client.EndConnect(ar);
            }
            catch (Exception e)
            {
                Debug.LogError(e.ToString(), EDebugLvl.ConnectionError);
            }
            if (client.Connected)
            {
                Debug.Log($"[P{socket.RemoteEndPoint}, L{socket.LocalEndPoint}]Connected", EDebugLvl.ConnectionLog);
                stream = new NetworkStream(socket, FileAccess.ReadWrite, true);
                StartRead();
            }
            ConnectedComplete(this, socket.Connected);
        }

Nie ważne ile razy będę próbował, połączenie jest odrzucane... adresy wszędzie się zgadzają a mimo to nie działa.
Działa tylko w tej samej sieci NAT . Niestety zauważyłem że na tym samym NAT stworzyło aż dwa połączenia. Jedno jest wynikiem próby połączenia nowym gniazdem A z B a drugie jest wynikiem odebrania nowego połączenia z B do A więc każdy z klientów posiada niepotrzebne jedno gniazdo. Więc całe przebicie NAT TCP/IP u mnie nie działa. Faktycznie mogę użyć UDP ale koniecznie potrzebne mi TCP. Siedzę nad tym od kilku miesięcy w wolnym czasie ale nigdzie nie mogę znaleźć przykładu z kodu a nie teorii czego jest bardzo dużo. Zgromadziłem sporo wiedzy przez 8 lat a od 2 piszę aplikacje używając gniazd aż w końcu potrzebne mi przebicie. Czemu nie użyję gotowego rozwiązania ? Potrzebuję własnego które jest w pełni otwarte używając tylko UDP i TCP ponieważ niektóre urządzenia docelowe wspierają tylko te protokoły. Używałem również klasy Socket ale i ta nie dała mi działającego egzemplarza. Może będziesz wstanie mi pomóc za co bym był bardzo wdzięczny.
Pozdrawiam

2

Z omijaniem NAT-u dla TCP jest tak, że powinieneś znaleźć sposób uniknięcia konieczności robienia tego:

  • może któryś z routerów obsługuje UPnP i możesz zażądać od niego, żeby przekierował port
  • jeżeli nie potrzebujesz konkretnie TCP, tylko czegoś zachowującego się jak TCP, rozwiązaniem będzie nałożenie dodatkowej warstwy na UDP robiącej to, co normalnie robi TCP
  • jeżeli są same komputery z Windows, możesz sobie włączyć Teredo: netsh int ipv6 set teredo enterpriseclient teredo.remlab.net (może z własnym serwerem miredo na Linuksie, a nie publicznym), w systemach pojawią się karty sieciowe Teredo Tunneling Pseudo-Interface mające adres IPv6, jest to tunel IPv6 wewnątrz IPv4 oparty na UDP, obsługujący omijanie NAT-u, tam możesz robić co chcesz, np. nasłuchiwać na IPv6 na jednym komputerze, podłączać się do niego na drugim komputerze, tak jakbyś miał sieć IPv6 bez żadnego NAT-u, a nie IPv4 z NAT-em, tylko Windows do nasłuchiwania na Teredo wymaga tego AllowNatTraversal(true), który już ustawiłeś

A gdy już naprawdę musisz używać omijania NAT-u dla TCP, pozostają rozwiązania mające współcześnie niską skuteczność, głównie oparte na simultaneous open: https://bford.info/pub/net/p2pnat/#sec-tcp - dwa komputery zachowują się tak, jakby były klientami, a nie serwerami i pierwszy klient próbuje podłączyć się do portu klienckiego drugiego, a drugi klient do portu klienckiego pierwszego w tym samym czasie. Nie zadziała we wszystkich sytuacjach, kiedy omijanie NAT-u dla UDP nie działa i dodatkowo w wielu innych, takich jak:

  • gdy router zamiast odrzucać pakiet bez żadnej informacji informuje flagą RST, że port jest zamknięty, powoduje to usuwanie mapowania z NAT-u po drugiej stronie, można próbować z ustawieniem dla pierwszego pakietu niskiego TTL, żeby nie dotarł do końca, ale zdarza się, że dla routera zmieniony lokalny TTL oznacza inny przepływ i tak utworzone mapowanie jest przy dalszych pakietach nieważne, a dalsze nie mogą już mieć niskiego TTL, bo muszą docierać do końca
  • gdy router nie obsługuje SYN jako odpowiedzi na SYN, czyli właśnie tego simultaneous open, tylko wymaga normalnej odpowiedzi SYN-ACK, nic się nie uda, będą powstawały dwa mapowania zamiast jednego
  • różne TCP offload engines dostępne w kartach sieciowych i innym sprzęcie często nie obsługują simultaneous open

W internecie znajdziesz też STUNT, coś podobnego do STUN, ale dla TCP, ale jest to oparte na podszywaniu się pod inne IP, co we współczesnym internecie ze względu na uRPF i podobne zabezpieczenia nie zadziała sensownie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1