10

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 case

How do I do TCP hole punching?

  • Did you set up the local port of `o` to be the 8090? You talk about it but the code doesn't match. Please update the code.; Remove all undefined variables such as bytearray and port.; wget needs to connect from port 123. Did you make it do that? If not use a different client. – usr Nov 10 '14 at 16:26
  • @usr: I wrote `wget http://mylocalip:8090/test` The `TcpClient o` is to make a dummy outgoing connection. port is = 8090. I thought if the local port of the outgoing connection is the local port i'm listening on then maybe when the remote connection tries to connect using the same port the router will let it through. Maybe it does work but not on my router. BUT there are software that successfully punch a hole in my router. –  Nov 10 '14 at 21:36
  • That's new information but my questions are still partially unanswered: Did you set up the local port of o to be the 8090? You talk about it but the code doesn't match. Please update the code.; When you "call out" using `o` you enable the tuple (your-ip, your-port, remote-ip, remote-port) to communicate. This means that all further communication must use those values. wget must use port 123. o must use port 8090. In case you aren't aware: outgoing connections can control the local port as well. This is just uncommon. – usr Nov 10 '14 at 22:33
  • Hmm, I tried making the local endpoint 'any' and the listening port. https://gist.github.com/anonymous/3b8164854ca87652725b I got the exception `Only one usage of each socket address (protocol/network address/port) is normally permitted` cc @usr –  Nov 11 '14 at 02:43
  • You are doing things concurrently which is not necessary. Do them one after the other: Bind, connect, dispose. Bind, listen, accept. The first part of the sequence establishes the routing info in the router. You can delete the reuse and threading stuff.; I recommend that you use telnet to test the reverse connection because nothing ca go wrong (telnet yourIp args[2]). – usr Nov 11 '14 at 09:47
  • Apparently, telnet on Windows does not support binding to a port. wget apparently neither. You'll probably have to write your own test client in a few lines. – usr Nov 11 '14 at 09:54
  • http://www.bford.info/pub/net/p2pnat/index.html what I propose is the simpler sequential hole punching technique described here. If you don't want that disregard my advice. – usr Nov 11 '14 at 10:37
  • 1
    @usr: After some fiddling I notice the code in my question works. I just need to choose the linux port carefully. It appears not to work if I block incoming connections on my linux box (to simulate router blocking a connection) I used this iptable rule https://gist.github.com/anonymous/514cdd23f749ca5d2385 So now I'm wondering if I need to be able to connect to the remote party before I can hole punch. Which defeats the point really since this problem assumes two parties can't communicate and a 3rd part is used to help punch the hole. Maybe I can ask a friend to run the code later –  Nov 11 '14 at 12:15
  • 1
    @usr: That link is long. I'll try to read it before the bounty is up. Also I forgot to mention if I don't block the connection but forget to send a connection it doesn't work (which makes sense). My gut suggest maybe the REJECT and DROP in iptables sends some kind of signal which ruins the hole punch –  Nov 11 '14 at 12:16
  • I just implemented hole punching myself. 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. Crazy ISP.) I can post the code to an answer if you like.; In the article I linked I mainly consider the section "sequential hole punching" important. It is not necessary for hole punching to do anything simultaneously (that is a meaningless notion in distributed systems anyway). – usr Nov 11 '14 at 13:17
  • @usr: Go for it. I don't have any answers. You'll probably be auto accepted as the bounty winner if you do it in time (and I dont notice it in time). Theres 3hrs remaining –  Nov 12 '14 at 00:31
  • Run a wireshark trace and see what's happening... – JWP Nov 12 '14 at 01:32
  • 1
    @usr If you want a simple test program which can listen to a port, [socat](http://www.dest-unreach.org/socat/) is handy, and has a windows port... It's like telnet or netcat, but with more interesting options... – Stobor Nov 12 '14 at 02:11

1 Answers1

12

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.

Ivan G.
  • 700
  • 8
  • 19
usr
  • 168,620
  • 35
  • 240
  • 369
  • This 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 –  Nov 12 '14 at 21:29
  • 2
    @acidzombie24 great, good to know! Finally, there is clean hole punching code on the web. All code that I found was awful. – usr Nov 12 '14 at 21:56
  • Here is a clean [hello world like hole punch you may like](https://gist.github.com/anonymous/03a3eaafd6839236dcc4). 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 –  Nov 13 '14 at 02:11
  • I'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 Nov 13 '14 at 12:30
  • @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. – usr Mar 04 '17 at 15:03