Transmisja po UDP a NAT

0

Robię na własne potrzeby eksperymenty z transmisją UDP w C++/Qt.

Napisałem sobie aplikację bazując na https://stackoverflow.com/questions/20125641/udp-server-client-chat-in-c-qt (jest prawie kopią przedstawionego kodu) i ona działa poprawnie. W moim przypadku, aplikacja ma jedno gniazdo, a jak chcę zrealizować test połączenia, to uruchamiam dwie instancje.

U mnie wszystko działa, jednak stwierdziłem, żeby działało, to na obu aplikacjach muszę uruchomić socket i zbindować na innym porcie, a przy wysyłaniu lub odbieraniu muszę znać adres IP i numer drugiego portu komputera.

UDP ma tą cechę, że powoduje mniejsze obciążenie sieci i komputerów w zamian za brak gwarancji dotarcia każdego komunikatu i brak kontroli zachowania kolejności komunikatów.

Załóżmy, że chciałbym zrealizować komunikację UDP między dwoma komputerami w takiej kolejności:
Jest router 1, który ma serwer DHCP, do niego podłączony jest komputer A i router 2. Natomiast router dwa ma swój serwer DHCP i do siebie podłączony komputer B. Router 1 rozdaje adresy z puli 192.168.1.x i komputer A dostał 192.168.1.2, a router 2 dostał 192.168.1.3. Router 2 rozdaje adresy z puli 192.168.2.z i komputer B dostał adres 192.168.2.2. Na routerach nie są ustawione żadne przekierowania transmisji.

W przypadku TCP, przy takiej konfiguracji, jak komputer A jest serwerem, to komputer B może się podłączyć posługując się adresem 192.168.1.2, natomiast jak komputer B jest serwerem, to komputer A nie może się podłączyć, bo komputer A nie dotrze pod adres 192.168.2.2, a pod adres 192.168.1.3 nie dotrze do komputera B, bo na routerze nie ma przekierowania, nie ma ograniczeń puli otwartych portów.

Jednak po uzyskaniu połączenia można wysyłać komunikaty w obie strony.

Natomiast przy UDP nie ma czegoś takiego, jak połączenie. W tej konfiguracji udało mi się wysłać pakiet z B do A, ale z A do B nie udało mi się wysłać.

Skoro wszystkie media strumieniowe działają w oparciu o UDP, to musi dać się zrealizować komunikację UDP w obie strony przy przedstawionej konfiguracji, po poprzez analogię, przy odbiorze radia internetowego, komputer A będzie serwerem rozgłośni, a komputer B będzie moim komputerem, na którym słucham radia, więc musi dać się wysłać UDP z A do B.

W jaki sposób można zrealizować transmisje UDP w obie strony w podanej konfiguracji bez żadnych zmian w połączeniach i konfiguracji sieci?

2

Nie wiem czy do końca zrozumiałem, co chcesz zrobić, ale załóżmy, ze tak.

andrzejlisek napisał(a):

U mnie wszystko działa, jednak stwierdziłem, żeby działało, to na obu aplikacjach muszę uruchomić socket i zbindować na innym porcie, a przy wysyłaniu lub odbieraniu muszę znać adres IP i numer drugiego portu komputera.

Tu jest moim zdaniem problem. Otóż w takiej konfiguracji masz 2 serwery. Czyli komunikację peer-to-peer, a nie klient-serwer. Chyba, że źle rozumiałem, i ty bindujesz ten sam socket, którym wysyłasz.

Jednak po uzyskaniu połączenia (TCP) można wysyłać komunikaty w obie strony.

Natomiast przy UDP nie ma czegoś takiego, jak połączenie. W tej konfiguracji udało mi się wysłać pakiet z B do A, ale z A do B nie udało mi się wysłać.

A tu jest błąd w rozumowaniu. To nie dzięki połączeniu TCP możesz wysyłać komunikaty w obie strony. To zasługa mapowania pomiędzy siecią wewnętrzną a zewnętrzną, którą dynamicznie robi moduł NAT twojego routera.

W przypadku TCP, ale także UDP, NAT działający na routerze zapamiętuje krotkę (ip_src, port_src, ip_dst, port_dst, proto) z pakietu wychodzącego ze swojej sieci. W przypadku pakietów pofragmentowanych będzie musiał skorzystać z pola identification nagłówka pakietu ip aby poskłądać dane warstwy 4. Następnie router podmienia adres źródłowy oraz port w pakietach wychodzących na swój własny, czyli wychodzące dane mają wpisane (ip_r, port_r, ip_dst, port_dst, proto).

Krotki (ip_src, port_src, ip_dst, port_dst, proto) i (ip_r, port_r, ip_dst, port_dst, proto) są ze sobą połączone, router będzie je trzymał w tablicy, aby potem w łatwy sposób zidentyfikować hosta w sieci wewnętrznej. Odbiorca będzie odsyłał pakiety na adres routera. Router widząc pakiety od odbiorcy zidentyfikuje prawdziwego hosta w swojej sieci wewnętrznej, wystarczy że sprawdzi wszystkie połączenia (tzw. flowtable) i znajdzie to odpowiednie. Prosta podmiana adresów i portów, i dane są przekazywane do hosta w sieci wewnętrznej.

Teraz, jeżeli A i B nasłuchują na swoich adresach, to niestety, ale podwoiłeś ilość portów potrzebnych do tej komunikacji. Host A używa jednego portu dla swojego serwera, ale też łącząc się do hosta B, otrzymuje następny port, który używany jest w komunikacji zainicjowanej z A do B . I to samo po stronie B. Przez to NAT nie ma pełnego mapowania, bo ruch jest niesymetryczn ... tzn posługujesz się jedną parę (ip, port1) do wysyłania, a inną, nadaną automatycznie, tj (ip, port2), do odbierania. To nie będzie działać zarówno na UDP jak i TCP jeżeli w grę wchodzi NAT.

Skoro wszystkie media strumieniowe działają w oparciu o UDP, to musi dać się zrealizować komunikację UDP w obie strony przy przedstawionej konfiguracji, po poprzez analogię, przy odbiorze radia internetowego, komputer A będzie serwerem rozgłośni, a komputer B będzie moim komputerem, na którym słucham radia, więc musi dać się wysłać UDP z A do B.

Działają, ponieważ masz komunikację klient-serwer. Przy takim schemacie NAT jest w stanie zmapować połączenia zewnętrze i wewnętrzne.

W jaki sposób można zrealizować transmisje UDP w obie strony w podanej konfiguracji bez żadnych zmian w połączeniach i konfiguracji sieci?

Nie bindować osobnych adresów dla każdego hosta. Albo raczej posługiwać się jedną parę.
Tzn jeżeli A wysłał coś do B, to niech używa tego samego socketa do odbierania wiadomości od A. Bind jest tutaj zbędny by odbierać pakiety od B. Przyda się jedynie w przypadku gdy ktoś inny łączy się bezpośrednio do A.

W praktyce, jeżeli oba hosty są za NAT, to należy skorzystać z pomocy trzeciego hosta, który pomoże nawiązać "połączenie" (tak, nawet w przypadku UDP ...). Tak naprawdę rolą takiego trzeciego hosta jest zadbanie o to, by oba routery miały odpowiednie wpisy w NAT. Technika taka nazywa się **hole punching **.

0

Skoro wszystkie media strumieniowe działają w oparciu o UDP, to musi dać się zrealizować komunikację UDP w obie strony

A to niby czemu? Od kiedy strumieniowanie potrzebuje dwustronnej komunikacji? :) Ba, czasem robi się to w ogóle multicastem nawet!

1

No nie skomunikują się bo nie są w tej samej sieci. Jeśli na prawde nie chcesz tam wcisnąć jakiegoś protokołu rutowania dynamicznego, to ja to ciężko widzę. @Shalom juz wspomniał o multicast może to jest to czego szukasz.

0
Shalom napisał(a):

Skoro wszystkie media strumieniowe działają w oparciu o UDP, to musi dać się zrealizować komunikację UDP w obie strony

A to niby czemu? Od kiedy strumieniowanie potrzebuje dwustronnej komunikacji? :) Ba, czasem robi się to w ogóle multicastem nawet!

Nie chodzi o strumieniowanie w dwie strony, tylko o to, że strumieniowanie jest zwykle w kierunku od komputera widocznego w sieci do komputera bezpośrednio niewidocznego w sieci.

0

W tym wszystkim nie chodzi o studiowanie, jak działa TCP i UDP, co nie zmienia faktu, że lepiej wiedzieć niż nie wiedzieć, tylko realizację dwustronnej komunikacji w aplikacji korzystając z API udostępnianym przez QT. Porobiłem parę prób i mam to, co chciałem.

Udało mi się do tego dojść w następujący sposób:

  1. Na obu komputerach utworzyłem gniazdo, zbindowałem z adresem 0.0.0.0, na komputerze A port 8000, na komputerze B port 8001 (jak na obu dałem ten sam port, to robił się bałagan, komputer A przyjmował wysłane przez siebie komunikaty, które nie dochodziły do B).
  2. Komputer A wysyła komunikaty na adres routera 2 i port 8001, natomiast komputer B wysyła komunikaty na adres komputera A.
  3. Od momentu, gdy komputer B wyśle komunikat, wtedy zaczyna działać komunikacja dwustronna, czyli oba komputery mogą się porozumieć.
    Czy to właśnie tak się robi (oczywiście adresy i numery portów mogą być inne)?

Zrobiłem też taką próbę, że na obu komputerach wpisałem adres siebie (dla komputera B próbowałem zarówno adres kompa, jak i adres routera 2) i jedynie udało mi się uzyskać komunikację od A do B, w drugą stronę już nie.

Wcześniej bawiłem się z TCP, również w Qt i tam zdarzało się, że przy przesyłaniu pakietów większych od ok. 64kB, komputer odbierający odbierał dwa komunikaty zamiast jednego. Oczywiście kolejność została zachowana i dotarły wszystkie części, bo TCP to gwarantuje. Przy częstym wysyłaniu, komputer odbierający sklejał dwa otrzymane komunikaty w jeden. To już można sobie programowo poradzić z obydwoma przypadkami traktując odbierane komunikaty jako niekończący się strumień bajtowy doładowywany kolejnymi komunikatami, z którego trzeba wyłowić poszczególne komunikaty i odpowiednio zareagować.

Doczytałem, że komunikat UDP ma długość maksymalnie 65535, ale danych może mieć 65527, a jak się doposaży w nagłówek IP (komunikat między hostami), to dane mogą mieć tylko 65507 bajtów. Czy to prawda, że jak jeden komunikat będzie mieć maksymalnie 65500 bajtów danych, to mam gwarancję, ze albo dotrze cały, albo nie dotrze wcale, a jak spróbuje się wysłać większy komunikat, to część może dotrzeć w innej kolejności, a część nie dotrze wcale i informacja po drugiej stronie będzie zniekształcona lub niepełna?

2
andrzejlisek napisał(a):

W tym wszystkim nie chodzi o studiowanie, jak działa TCP i UDP, co nie zmienia faktu, że lepiej wiedzieć niż nie wiedzieć, tylko realizację dwustronnej komunikacji w aplikacji korzystając z API udostępnianym przez QT. Porobiłem parę prób i mam to, co chciałem.

Tyle, że to ma znaczenie. Albo wiesz co robisz, albo nie.

Udało mi się do tego dojść w następujący sposób:

  1. Na obu komputerach utworzyłem gniazdo, zbindowałem z adresem 0.0.0.0, na komputerze A port 8000, na komputerze B port 8001 (jak na obu dałem ten sam port, to robił się bałagan, komputer A przyjmował wysłane przez siebie komunikaty, które nie dochodziły do B).
  2. Komputer A wysyła komunikaty na adres routera 2 i port 8001, natomiast komputer B wysyła komunikaty na adres komputera A.

routera 2 i port 8001 ? Nie znasz tego adresu dopóki B się nie skomunikuje z A. Nie powinieneś wysyłać nic bezpośrednio na adres rutera. (chyba, że zrobiłeś ręczne mapowanie w NAT/DMZ)
Jakby A w tym momencie wysłał coś na adres B, to by się "odbił". Jakby A wysłał na adres routera B (2), zanim B się połączył z A to też by się "odbił".
Dopiero gdy B się podłączy do A, to A może wysyłać coś na ten adres. I tu jedna uwaga, to mapowanie i port wygenerowany przez ruter są tymczasowe. Jak komunikacja zaniknie, to router w końcu sprzątnie tymczasowe połączenie. W przypadku TCP pomaga keep-alive, które utrzyma mapowanie na NAT. W przypadku UDP trzeba by co jakiś czas coś wysyłać.

  1. Od momentu, gdy komputer B wyśle komunikat, wtedy zaczyna działać komunikacja dwustronna, czyli oba komputery mogą się porozumieć.
    Czy to właśnie tak się robi (oczywiście adresy i numery portów mogą być inne)?

Tak, bo wysłanie tego pakietu UDP z B do A skutkuje stworzeniem mapowania pomiędzy adresem zewnętrznym a wewnętrznym na routerze, za którym stoi B. Od tego momentu router wie, że gdy dochodzą wiadomości na adres, który stworzył na potrzeby mapowania, to ma go przekazać hostowi w sieci prywatnej.

Zrobiłem też taką próbę, że na obu komputerach wpisałem adres siebie (dla komputera B próbowałem zarówno adres kompa, jak i adres routera 2) i jedynie udało mi się uzyskać komunikację od A do B, w drugą stronę już nie.

Nie rozumiem, co to znaczy adres siebie.

Wcześniej bawiłem się z TCP, również w Qt i tam zdarzało się, że przy przesyłaniu pakietów większych od ok. 64kB, komputer odbierający odbierał dwa komunikaty zamiast jednego. Oczywiście kolejność została zachowana i dotarły wszystkie części, bo TCP to gwarantuje.

Mylisz się. Na poziomie aplikacji nie wiesz ile segmentów TCP otrzymałeś. Wiesz jedynie ile razy odczytałeś część strumienia z jadra systemu.

Przy częstym wysyłaniu, komputer odbierający sklejał dwa otrzymane komunikaty w jeden. To już można sobie programowo poradzić z obydwoma przypadkami traktując odbierane komunikaty jako niekończący się strumień bajtowy doładowywany kolejnymi komunikatami, z którego trzeba wyłowić poszczególne komunikaty i odpowiednio zareagować.

Bo taka jest istota TCP. Wysyłasz ciągły strumień bajtów. Jak chcesz mieć podział na wiadomości, to robisz to ponad TCP korzystając z jakiegoś sposobu kodowania wiadomości.

Doczytałem, że komunikat UDP ma długość maksymalnie 65535, ale danych może mieć 65527, a jak się doposaży w nagłówek IP (komunikat między hostami), to dane mogą mieć tylko 65507 bajtów.

Trochę pomyliłeś pojęcia. 65535 to maksymalny rozmiar pakietu ip v4. Wynika z tego, że pole Total Length z nagłówku pakietu ipv4 ma 16 bitów. Pakiet ip v4 w sekcji danych zawiera nagłówek UDP wraz z danymi warstwy 4. Wszystko musi się zmieścić, bo UDP nie dzieli wiadomości (wiadomości dzieli warstwa 3, jeden pakiet ip może być rozbity na wiele fragmentów, ale łączny limit jest taki jak wyżej).

Czy to prawda, że jak jeden komunikat będzie mieć maksymalnie 65500 bajtów danych, to mam gwarancję, ze albo dotrze cały, albo nie dotrze wcale, a jak spróbuje się wysłać większy komunikat, to część może dotrzeć w innej kolejności, a część nie dotrze wcale i informacja po drugiej stronie będzie zniekształcona lub niepełna?

Po kolei:

  1. Nie da się wysłać większego komunikatu niż 65535 po UDP.
  2. Raczej starałbym się ograniczyć do MTU w sieci, albo nawet na ścieżce. Fragmentacja pakietów ip poważnie ogranicza transfer.
  3. Zawsze masz taką gwarancję, że segment UDP dotrze cały, albo wcale. Jakby część została stracona z powodu fragmentacji, to w aplikacji nie otrzymałbyś żadnej części tego segmentu.
  4. Zanim dane z segmentu UDP zostaną przekazane do aplikacji, to muszą być poskładane z fragmentów pakietu ip. Aplikacja nigdy nie dostanie tylko części danych wysłanych w pojedynczym segmencie.
  5. Co do tego limitu danych, wątpię. Jest mnóstwo innych czynników, które mogą pomniejszyć maksymalny rozmiar danych. Żeby nie szukać daleko, enkapsulacja ipv4 w ipv4.
0
nalik napisał(a):
andrzejlisek napisał(a):

W tym wszystkim nie chodzi o studiowanie, jak działa TCP i UDP, co nie zmienia faktu, że lepiej wiedzieć niż nie wiedzieć, tylko realizację dwustronnej komunikacji w aplikacji korzystając z API udostępnianym przez QT. Porobiłem parę prób i mam to, co chciałem.

Tyle, że to ma znaczenie. Albo wiesz co robisz, albo nie.

Z tego właśnie powodu, jak napisałem, lepiej wiedzieć niż nie wiedzieć.

Zrobiłem też taką próbę, że na obu komputerach wpisałem adres siebie (dla komputera B próbowałem zarówno adres kompa, jak i adres routera 2) i jedynie udało mi się uzyskać komunikację od A do B, w drugą stronę już nie.

Nie rozumiem, co to znaczy adres siebie.

Adres siebie, czyli na komputerze A binduję z adresem IP komputera A, a na komputerze B binduję z adresem IP komputera B lub adresem IP routera 2 po stronie WAN.

0
andrzejlisek napisał(a):

Zrobiłem też taką próbę, że na obu komputerach wpisałem adres siebie (dla komputera B próbowałem zarówno adres kompa, jak i adres routera 2) i jedynie udało mi się uzyskać komunikację od A do B, w drugą stronę już nie.

Nie rozumiem, co to znaczy adres siebie.

Adres siebie, czyli na komputerze A binduję z adresem IP komputera A, a na komputerze B binduję z adresem IP komputera B lub adresem IP routera 2 po stronie WAN.

Do bindowania zawsze powinieneś podawać adres siebie. Jak podajesz "0.0.0.0", to tak naprawdę nasłuchujesz na wszystkich możliwych adresach hosta (a więc także localhost).

Podawanie adresu rutera podczas bindowania jest bez sensu. Bindowanie to wywołanie systemowe, które informuje jądro, że aplikacja spodziewa się otrzymywać wiadomości pod wskazanym (adresem, portem, protokołem). Dzięki temu jądro wie, że w momencie gdy z karty sieciowej zostanie odczytany pakiet zaadresowany takimi wartościami, to jest aplikacja, która potrzebuje tych pakietów. Ruter nie ma tutaj nic do rzeczy.

0

W takim razie, jeśli chodzi o UDP to temat łączności mam ogarnięty. Jednak w wątku przewinął się jeszcze jedna sprawa, jaką jest hole punching. Załóżmy, że teraz mam bardziej rozbudowaną konfigurację:

                ╔═════════╗
                ║Router 1 ║
                ║10.10.0.2║
                ╚══╤═╤═╤══╝
                   │ │ │
      ┌────────────┘ │ └────────────┐
      │              │              │
╔═════╧═════╗  ╔═════╧═════╗  ╔═════╧═════╗
║192.168.1.3║  ║192.168.1.2║  ║192.168.1.4║
║ Router 2  ║  ║  Host A   ║  ║ Router 3  ║
╚═════╤═════╝  ╚═══════════╝  ╚═════╤═════╝
      │                             │
      │                             │
╔═════╧═════╗                 ╔═════╧═════╗
║192.168.2.2║                 ║192.168.3.2║
║  Host B   ║                 ║  Host C   ║
╚═══════════╝                 ╚═══════════╝

W przypadku TCP i UDP to połączenie A z B oraz z A do C nie stanowi żadnego problemu pod warunkiem, że w przypadku TCP serwer będzie na A, a klient na B lub C, a w przypadku UDP, pierwszy komunikat będzie z B do A lub z C do A, wtedy komunikacja w obie strony będzie działać.

W takim razie w jaki sposób nawiązuje się połączenie między B i C? Rozumiem, że do takiego celu stosuje się hole punching. Z tego, co wyczytałem, to polega to na tym, że na A musi być uruchomiona aplikacja, która uruchamia dwa serwery, na jednym przyjmuje połączenie od B, na drugim przyjmuje połączenie od C. Na podstawie tych połączeń ta aplikacja wie, jaki jest adres i port hostów B i C z perspektywy hostu A i tym samym routera 1, w międzyczasie routery 2 i 3 ustawiły translację NAT stosownie do tych połączeń. Potem A przekazuje do B informację o adresie C, a do C przekazuje informacje o B. W drugiej kolejności, jak na B będzie uruchomiony serwer, to C będzie mógł się podłączyć. Czy tak to właśnie wygląda?

W takim razie, czy używając interfejsów gniazd TCP i UDP dostępne w Qt i większości innych API, można zrealizować połączenie P2P pomiędzy B i C? Chodzi o coś na wzór popularnych niegdyś sieci wymiany plików P2P i tu należy skorzystać z hole punching.

0
andrzejlisek napisał(a):

W takim razie, jeśli chodzi o UDP to temat łączności mam ogarnięty. Jednak w wątku przewinął się jeszcze jedna sprawa, jaką jest hole punching. Załóżmy, że teraz mam bardziej rozbudowaną konfigurację:

                ╔═════════╗
                ║Router 1 ║
                ║10.10.0.2║
                ╚══╤═╤═╤══╝
                   │ │ │
      ┌────────────┘ │ └────────────┐
      │              │              │
╔═════╧═════╗  ╔═════╧═════╗  ╔═════╧═════╗
║192.168.1.3║  ║192.168.1.2║  ║192.168.1.4║
║ Router 2  ║  ║  Host A   ║  ║ Router 3  ║
╚═════╤═════╝  ╚═══════════╝  ╚═════╤═════╝
      │                             │
      │                             │
╔═════╧═════╗                 ╔═════╧═════╗
║192.168.2.2║                 ║192.168.3.2║
║  Host B   ║                 ║  Host C   ║
╚═══════════╝                 ╚═══════════╝

(...)
W takim razie w jaki sposób nawiązuje się połączenie między B i C? Rozumiem, że do takiego celu stosuje się hole punching. Z tego, co wyczytałem, to polega to na tym, że na A musi być uruchomiona aplikacja, która uruchamia dwa serwery, na jednym przyjmuje połączenie od B, na drugim przyjmuje połączenie od C.

Jedna aplikacja, które zadaniem jest rozesłanie znanych zewnętrznych ip i zewnetrznych portów, użwanych przez innych klientów, którzy się połączyli. Na jednym adresie i porcie przecież można obsłużyć wiele połączeń.

Na podstawie tych połączeń ta aplikacja wie, jaki jest adres i port hostów B i C z perspektywy hostu A i tym samym routera 1, w międzyczasie routery 2 i 3 ustawiły translację NAT stosownie do tych połączeń.

Tu się zgadza.

Potem A przekazuje do B informację o adresie C, a do C przekazuje informacje o B. W drugiej kolejności, jak na B będzie uruchomiony serwer, to C będzie mógł się podłączyć. Czy tak to właśnie wygląda?

Tu się nie zgadza. B nie tyle przekazuje informację o adresie, co próbuje się połączyć, zakładając, że ip i port zewnętrzne, którym posługiwała się aplikacja B nie uległy zmianie.
W momencie kiedy pakiet opuszcza sieć wewnętrzną, NAT tworzy odpowiednie mapowania. Gdy mapowania są już utworzone na obu NAT, to pakiety będą przechodzić granicę urządzeń robiących translację.

Niestety wiele zależy od typu NAT. Jeżeli mapowanie portu zewnętrznego i wewnętrznego jest niezmienne to jest OK. Jeżeli port jest randomizowany per połączenie albo jest wbity na stałe na zasadzie DMZ, to prawdopodobnie technika nie zadziała. Tutaj nie jestem pewien, ponieważ nigdy nie widziałem implementacji tego mechanizmu.

Za to odnalazłem materiały, które być może Ci pomogą: https://bford.info/pub/net/p2pnat/

W takim razie, czy używając interfejsów gniazd TCP i UDP dostępne w Qt i większości innych API, można zrealizować połączenie P2P pomiędzy B i C? Chodzi o coś na wzór popularnych niegdyś sieci wymiany plików P2P i tu należy skorzystać z hole punching.

Tak.

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