How do I do TCP hole punching?
I'd use the "sequential hole punching technique" detailed in http://www.bford.info/pub/net/p2pnat/index.html. It seems much simpler to do that simultaneous connections and socket reuse. It is not necessary for hole punching to do anything exactly simultaneously (that is a meaningless notion in distributed systems anyway).
I have implemented hole punching. My router seems not to like it. Wireshark shows the outbound hole punching SYN is correct but the remote party can't get through to me. I verifies all ports with TcpView.exe and disabled all firewalls. Must be a router issue. (It is a strange and invasive router.)
class HolePunchingTest
{
IPEndPoint localEndPoint;
IPEndPoint remoteEndPoint;
bool useParallelAlgorithm;
public static void Run()
{
var ipHostEntry = Dns.GetHostEntry("REMOTE_HOST");
new HolePunchingTest()
{
localEndPoint = new IPEndPoint(IPAddress.Parse("LOCAL_IP"), 1234),
remoteEndPoint = new IPEndPoint(ipHostEntry.AddressList.First().Address, 1235),
useParallelAlgorithm = true,
}.RunImpl();
}
void RunImpl()
{
if (useParallelAlgorithm)
{
Parallel.Invoke(() =>
{
while (true)
{
PunchHole();
}
},
() => RunServer());
}
else
{
PunchHole();
RunServer();
}
}
void PunchHole()
{
Console.WriteLine("Punching hole...");
using (var punchSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
EnableReuseAddress(punchSocket);
punchSocket.Bind(localEndPoint);
try
{
punchSocket.Connect(remoteEndPoint);
Debug.Assert(false);
}
catch (SocketException socketException)
{
Console.WriteLine("Punching hole: " + socketException.SocketErrorCode);
Debug.Assert(socketException.SocketErrorCode == SocketError.TimedOut || socketException.SocketErrorCode == SocketError.ConnectionRefused);
}
}
Console.WriteLine("Hole punch completed.");
}
void RunServer()
{
using (var listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
EnableReuseAddress(listeningSocket);
listeningSocket.Bind(localEndPoint);
listeningSocket.Listen(0);
while (true)
{
var connectionSocket = listeningSocket.Accept();
Task.Run(() => ProcessConnection(connectionSocket));
}
}
}
void ProcessConnection(Socket connectionSocket)
{
Console.WriteLine("Socket accepted.");
using (connectionSocket)
{
connectionSocket.Shutdown(SocketShutdown.Both);
}
Console.WriteLine("Socket shut down.");
}
void EnableReuseAddress(Socket socket)
{
if (useParallelAlgorithm)
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
}
You can try both values for useParallelAlgorithm
. Both should work.
This code is for the server. It punches a hole into the local NAT. You can then connect from the remote side using any client that allows to pick the local port. I used curl.exe
. Apparently, telnet on Windows does not support binding to a port. wget apparently neither.
Verify that the ports are correct on both sides using TcpView or Process Explorer. You can use Wireshark to verify packets. Set a filter like tcp.port = 1234
.
When you "call out" to punch a hole you enable the tuple (your-ip, your-port, remote-ip, remote-port) to communicate. This means that all further communication must use those values. All sockets (inbound or outbound) must use these exact port numbers. In case you aren't aware: outgoing connections can control the local port as well. This is just uncommon.
Admin
Updated on July 24, 2022Comments
-
Admin almost 2 years
Question is below. Here is my current test code which did not succeed.
static void Main(string[] args) { if (args.Count() != 3) { Console.WriteLine("Bad args"); } var ep = new IPEndPoint(IPAddress.Parse(args[0]), int.Parse(args[1])); var lp = new IPEndPoint(IPAddress.Any, int.Parse(args[2])); var s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); s.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); s.Bind(lp); var c = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); c.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); c.Bind(lp); Task.Run(() => { try { c.Connect(ep); } catch { } }); s.Listen(10); var v = s.Accept(); v.Close(); }
How do I do TCP hole punching? I am testing using a remote server. I'm running
wget local_public_ip:port/test
. I have my router setup for port 80 so it doesn't need a hole punch. My code got a connection. Now I try on other ports and I can't exactly figure out how to punch the hole.What I have done is (C# code)
var l = new TcpListener(8090); l.Start(); try { var o = new TcpClient(); o.Connect("myserverip", 123); } catch(Exception ex) {} var e = l.AcceptSocket(); Console.WriteLine(e.RemoteEndPoint.AddressFamily);
I thought maybe I need to setup the local endpoint on the out tcp connection.
TcpClient(new System.Net.IPEndPoint(new System.Net.IPAddress(bytearray), port));
I made a mistake and got this exception
The requested address is not valid in its context
Fixing up the byte array to
192,168,1,5
it appears to make outgoing connects correctly. Now that I have a out connection to the remote IP using my listening port I thought wget would be able to connect to me. It wasn't the caseHow do I do TCP hole punching?
-
Admin over 9 yearsThis appears to work. I tested it using
useParallelAlgorithm=false
. I waited for the hole to be punched before running it on my remote server. It works. It appears to punch a hole through iptables as well -
usr over 9 years@acidzombie24 great, good to know! Finally, there is clean hole punching code on the web. All code that I found was awful.
-
Admin over 9 yearsHere is a clean hello world like hole punch you may like. It only uses TcpClient and TcpListener. No reuse address or raw sockets. Are you sure your router doesn't like/support the hole punch? if you have a remote linux box to test with I can hand you some IPTable rules to test the block
-
usr over 9 yearsI'm not 100% sure it's the router but I cannot think of any other explanation. I have a Windows server connected directly to the Internet. I used it to test this.
-
usr about 7 years@DiaaEddin it does not work for me either for unknown reasons but it did work for someone else. If you ever find the reason for it not working please comment.