Здесь, всё-таки, придётся лезть теорию.
В общем виде, технология называется TCP Hole Punching:
https://en.wikipedia.org/wiki/TCP_hole_punching
Работает она так:
Есть маршрутизатор с внешним ip-адресом 1.2.3.4 и спрятанной за ним сетью 192.168.0.0/24.
Допустим, находящееся за NAT устройство с Ip-адресом 192.168.0.100 решило вылезти в Интернет и подключиться к серверу с ip-адресом 5.6.7.8 к порту 8080.
Устройство поднимает TCP-соединение: со стороны устройства пара ip-адрес:порт будет, допустим, 192.168.0.100: 1111, а со стороны принимающей стороны 5.6.7.8:8080.
Маршрутизатор, пропуская сквозь себя эти пакеты, подменяем ip-адрес и, что важно, порт спрятанного за ним устройства: на, предположим, 1.2.3.4:7890.
Если на маршрутизаторе включен TCP Hole Punching, то он начинает преобразовывать пары адрес-порт 192.168.0.100: 1111 -> 1.2.3.4:7890 не только для соединений, исходящих изнутри, но и для входящих соединений снаружи.
Это значит, что, начиная с момента установки соединения "изнутри наружу", запрос на TCP-соединение из интернета к 1.2.3.4:7890 будет прокинут маршрутизатором до спрятанного устройства, и дойдёт до 192.168.0.100: 1111.
Получается, клиентская сторона для приёма соединений из-за NAT должна подготовиться следующим образом:
1. Установить соединение с каким-то узлом в Интернет. Вообще неважно с кем: нам надо просто "пробить дырку" в NAT для приёма входящих соединений. Установив соединение, мы
запоминаем source port.
2. Создаём Listening socket на порту, запомненном в предыдущем шаге. Теперь мы можем принимать входящие соединения из интернет!
И всё бы здорово, но возникает следующий вопрос: номер порта, на котором мы слушаем, маршрутизатор подменит совершенно непредсказуемым образом.
Если мы хотим установить прямое соединение с устройством, которое тоже находится за NAT, как нам узнать, к какой публичной паре Ip-адрес:порт нам цепляться? Ведь каждый раз номер порта может непредсказуемо меняться!
Ответ здесь один: нужен посредник - что-то типа каталога. Сервер с публичным ip-адресом, на котором можно зарегистрироваться. Ну никак без него!
Работать это может так:
1. Есть каталог с ip-адресом, допустим, 11.12.13.14, принимающий входящие соединения на порту 80.
2. Устройство А с ip-адресом 192.168.0.100, находящееся за NAT 1.2.3.4, готовится к приёму входящих соединений. Оно устанавливает TCP-соединение с каталогом:
192.168.0.100:1111 ===> 11.12.13.14:80.
NAT A в проходящих сквозь него ip-пакетах преобразует ip-адрес и номер порта для спрятанного за ним устройства, и запоминает это соответствие:
192.168.0.100:1111 <-> 1.2.3.4:1112.
С точки зрения каталога, он принял такое входящее TCP-соединение:
1.2.3.4:1112 ===> 11.12.13.14:80, ибо реальную адресацию устройства A ему узнать просто неоткуда. Ну и ладно: всё и так прекрасно работает!
3. Устройство B с ip-адресом 192.168.2.200, находящееся за NAT 5.6.7.8, устанавливает TCP-соединение с каталогом:
192.168.2.200:2221 ===> 11.12.13.14:80.
NAT B в проходящих сквозь него ip-пакетах преобразует ip-адрес и номер порта для спрятанного за ним устройства, и запоминает это соответствие:
192.168.2.200:2221 <-> 5.6.7.8:2222.
4. Устройство B спрашивает у каталога: "А кто к тебе ещё подключен?"
Каталог ответчает: "Да вот есть один такой, 1.2.3.4:1112"
5. Устройство B устанавливает TCP-соединение с полученным от каталога ресурсом:
192.168.2.200:9876 ===> 1.2.3.4:1112
NAT B подменяет и запоминает ip-адрес и номер порта устройства B: 192.168.2.200:9876 -> 5.6.7.8:4321
Соответственно, на NAT A прилетает запрос на установление соединения:
5.6.7.8:4321 ===> 1.2.3.4:1112
NAT A смотрит в таблицу NAT-трансляций, видит там знакомый номер порта, и подставляет из таблицы ip-адрес и номер порта устройства A: 1.2.3.4:1112 -> 192.168.0.100:1111
6. Устройство A получает запрос на подключение:
5.6.7.8:4321 ===> 192.168.0.100:1111
Собственно, в зависимости от требований, сервер каталога можно:
1. Написать самому
2. А можно не изобретать велосипед, и воспользоваться готовыми реализациями того же протокола STUN