TCP Hole Punching

  • C#
  • .NET (FX) 4.5–4.8

Es gibt 18 Antworten in diesem Thema. Der letzte Beitrag () ist von Quadsoft.

    TCP Hole Punching

    Guten Abend,

    ich versuche tcp hole punching anzuwenden.
    Soweit ich nachvollzogen habe existieren jeweils drei Komponenten:
    Server, Peer 1 und Peer 2.

    Peer 1 und Peer 2 verbinden zum Server und teilen diesen ihre private und öffentliche Endpunkte mit.
    Wenn nun ein Peer sich mit einem anderen verbinden möchte sendet der Server die öffentliche IP-Adresse samt Port des jeweils anderen Peers an den anfordernden Peer.
    Simultan sendet der Server die öffentliche IP-Adresse samt Port des anfordernden Peers an den anderen.

    Im Peer lauscht während-dessen ein Socket an dem Port, an dem es sich mit dem Server verband.
    Im Hintergrund läuft ein Thread das periodisch versucht die Peers zu punch-holen.

    Wenn dann ein ankommende Anfrage akzeptiert wurde besteht eine Verbindung.
    Alles sehr logisch; nun meine Frage:
    Wieso funktioniert das erst nach mehreren Versuchen?

    Danke.
    Und Gott alleine weiß alles am allerbesten und besser.
    Guten Abend,
    hat nicht so geklappt wie erhofft.

    Ich verwende folgenden Client:

    C#-Quellcode

    1. bool tryPunchHoling = true;
    2. bool listenOnPort = false;
    3. BinaryReader binRHolePunched = null;
    4. BinaryWriter binWHolePunched = null;
    5. BinaryReader binIpReader = null;
    6. IPEndPoint establishedIPeP = null;
    7. Socket mainSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    8. mainSocket.Connect(new IPEndPoint(IPAddress.Parse("WAN IP"), 8000));
    9. Console.WriteLine("OWN: " + (establishedIPeP = (IPEndPoint) mainSocket.LocalEndPoint));
    10. binIpReader = new BinaryReader(new NetworkStream(mainSocket));
    11. new Thread(new ThreadStart(() =>
    12. {
    13. IPEndPoint ipep = null;
    14. while (mainSocket.Connected)
    15. {
    16. string peer_ip = binIpReader.ReadString();
    17. ipep = new IPEndPoint(IPAddress.Parse(peer_ip.Split(':')[0]), int.Parse(peer_ip.Split(':')[1]));
    18. break;
    19. }
    20. listenOnPort = true;
    21. new Thread(new ParameterizedThreadStart((c) =>
    22. {
    23. Socket listener = (Socket)c;
    24. Socket punch_holed_client = null;
    25. listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    26. listener.Bind(establishedIPeP);
    27. listener.Listen(1);
    28. while (listenOnPort)
    29. {
    30. punch_holed_client = listener.Accept();
    31. tryPunchHoling = false;
    32. Console.WriteLine("CLIENT RECEIVED!");
    33. listenOnPort = false;
    34. break;
    35. }
    36. binRHolePunched = new BinaryReader(new NetworkStream(punch_holed_client));
    37. binWHolePunched = new BinaryWriter(new NetworkStream(punch_holed_client));
    38. Console.ForegroundColor = ConsoleColor.Green;
    39. Console.WriteLine("IOs INITIATED!");
    40. Console.ForegroundColor = ConsoleColor.Gray;
    41. })).Start(mainSocket);
    42. Socket punch_holer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    43. while (tryPunchHoling)
    44. {
    45. Console.WriteLine("Attempting to connect to " + ipep + "...");
    46. try
    47. {
    48. punch_holer.Connect(ipep);
    49. Console.ForegroundColor = ConsoleColor.Green;
    50. Console.WriteLine("SUCCEED!");
    51. Console.ForegroundColor = ConsoleColor.Gray;
    52. }
    53. catch
    54. {
    55. Console.ForegroundColor = ConsoleColor.Red;
    56. Console.WriteLine("NO CONNECTION...");
    57. Console.ForegroundColor = ConsoleColor.Gray;
    58. }
    59. }
    60. })).Start();
    61. Console.Read();


    Erläuterung:
    Es wird ein Socket generiert das sich beim Server verbindet.
    Wenn der Server mindestens zwei Clients in der Liste hat werden die IP-Adressen jeweils ausgetauscht.

    Ein neuer Thread wird generiert und wartet dann bis der BinaryReader die Ip-Adresse des jeweiligen anderen Peers hat.
    Wenn nun BinaryReader .ReadString() "getriggert" wird, wird die Ausführung des Threads fortgeführt und die Schleife wird verlassen.
    Dann wird ein neuer Thread gestartet in der mainSocket neu belegt wird ( weil ein bereits verbundener Socket nicht zum Lauschen verwendet werden darf).
    Dieser Thread kontrolliert dann den Socket "punch_holer" der periodisch versucht mit dem anderen Peer sich zu verbinden.
    Das machen beide eben quasi simultan.

    Lokal geht es natürlich.
    Doch das will nicht mit der öffentlichen IP-Adresse funktionieren.

    Liegt es etwa an meinem Router, oder habe ich etwas gänzlich missverstanden?

    Liebe Grüße..
    Und Gott alleine weiß alles am allerbesten und besser.
    Ich bin absolut nicht mehr in der Materie drin, aber hau trotzdem einfach mal raus was ich gerade denke.
    Ich glaube es liegt am Router, aber nicht speziell an deinem sondern allgemein. Wenn du eine Verbindung nach draußen aufbaust Speichert sich der Router glaube auch wohin die Verbindung geht und lässt auf diesem Port auch nur Pakete wieder rein, die auch von diesem Ziel kommen. Deine Brücke wäre also in diesem Fall leider nicht möglich da du ein Paket in Richtung anderen Client schicken musst aber dadurch wieder eine neues Loch schlägst, von dem du wieder den Port nicht kennst.
    Ich dachte auch bei z.B. UDP Hole Punching schickt Client1 (nach dem IP austausch) zu Client2 ein Pakte um den Port in diese Richtung zu öffnen und Client 2 ballert dann Brute-Force mäßig einfach auf alle Ports Client1 mit Paketen zu. Eins davon kommt dann mal durch und Client1 kann dann zurück schreiben, "Jo das war's". Damit steht dann die Verbindung. Aber ist wie gesagt, gerade nur Vermutung.

    Bluespide schrieb:

    Speichert sich der Router glaube auch wohin die Verbindung geht und lässt auf diesem Port auch nur Pakete wieder rein, die auch von diesem Ziel kommen.
    Nennt sich Statefull Inspection.
    In general (across programming languages), a pointer is a number that represents a physical location in memory. A nullpointer is (almost always) one that points to 0, and is widely recognized as "not pointing to anything". Since systems have different amounts of supported memory, it doesn't always take the same number of bytes to hold that number, so we call a "native size integer" one that can hold a pointer on any particular system. - Sam Harwell
    Das Problem ist SPI. Anders ausgedrückt gehört nicht nur das Port-Mapping zu einer TCP-Verbindung über ein NAT, sondern auch die jeweils zugehörige IP-Adresse. Daher gilt:

    Wikipedia schrieb:

    All TCP NAT traversal and Hole Punching techniques have to solve the port prediction problem.

    Die Serververbindung dient nur dem Austausch der Verbindungsdaten. Dadurch kennen Peer A und B die öffentliche IP-Adresse des jeweils anderen. Gegeben seien also die Peers A und B sowie der Server S. S kennt jeweils die öffentliche IP (IP_A, IP_B) sowie den Quellport der Verbindungen (P_A, P_B) von A und B. Diese Informationen werden den Peers mitgeteilt. Die Serververbindung kann beendet werden.

    Jetzt passiert folgendes (in beide Richtungen):
    - Peer A sendet ein SYN an Peer B. Folge: NAT A öffnet Port P. P ist unbekannt.
    - Peer A erstellt einen Listener auf dem soeben geöffneten Endpunkt (ggf. mit Option SO_REUSEADDR). Wichtig ist hier, dass A auf demselben Port hört, auf dem das SYN gesendet wurde. Dieser Port ist nicht derselbe wie P! Er kann auf Peer A mit netstat (oder im Programm als Socket-Eigenschaft) ermittelt werden.
    - Peer B versucht, P zu erraten. Zum Beispiel könnte NAT A die Ports aufsteigend vergeben, dann liegt P mit hoher Wahrscheinlichkeit nur wenige Ports über P_A.
    - Peer B versucht, eine Verbindung zu (IP_A, P) aufzubauen. Hat er P richtig geraten, lässt NAT A die Anfrage durch, welche den Listener erreicht.

    Das Ganze nennt sich "simultaneous open" und ist im verlinkten Artikel nochmals beschrieben. Das macht den Server jedoch nicht überflüssig, weil dieser zuvor das Erraten des Ports P unterstützen muss.
    Gruß
    hal2000
    Hallo, danke für diesen sehr informativen Beitrag!
    Wie sende ich denn SYNS in C#?

    Das Problem liegt ja genau hier.
    Wenn "connect()" nicht funktioniert, gibt es auch keinen Endpunkt. (Selbst mit .SetSocketOption(ReuseAddress) und Raw-Sockets selbst lassen dies auch nicht zu (INTER_NETWORK, RAW, TCP)).
    Und Gott alleine weiß alles am allerbesten und besser.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „φConst“ ()

    Ein Client der eine Verbindung zu einem Server via TCP aufbauen will sendet an diesen ein TCP Packet, in dem das SYN Flag True ist, ein Packet mit SYN und ACK Flag gesetzt kommt zurueck woraufhin noch mal vom Clienten ein ACK gesendet wird und dann steht die Verbindung. Somit geschieht das bei deinem Code von allein.

    Das nennt sich dann 3 Way Handshake.

    Schau hier nach "Connection establishment"
    en.wikipedia.org/wiki/Transmission_Control_Protocol

    Hier siehst du den Aufbau eines TCP-Headers
    de.wikipedia.org/w/index.php?title=Datei:TCP_Header.svg
    And i think to myself... what a wonderfuL World!
    connect() bzw. das .NET-Pendant Socket.Connect() funktioniert immer. Manchmal bekommt es jedoch keine Antwort von der Gegenstelle - dann bleibt es beim SYN, dem ersten Schritt des Handshakes --> Ziel erreicht, SYN gesendet und das lokale NAT ist offen. Der Trick ist, den Socket erneut zu verwenden, bevor der Timeout für das Portmapping im NAT abläuft, der die halboffene Verbindung wieder einkassieren würde.
    Gruß
    hal2000
    Um dir deine SYN Anfrage zu basteln, kannst du das Projekt PacketDotNet verwenden.
    Packet.Net is a high performance .Net assembly for dissecting and constructing
    network packets such as ethernet, ip, tcp, udp etc.

    In general (across programming languages), a pointer is a number that represents a physical location in memory. A nullpointer is (almost always) one that points to 0, and is widely recognized as "not pointing to anything". Since systems have different amounts of supported memory, it doesn't always take the same number of bytes to hold that number, so we call a "native size integer" one that can hold a pointer on any particular system. - Sam Harwell
    Gut, dann habe ich bereits in die richtige Richtung gedacht... nur noch eine Sache:
    Du schreibst das man nach dem Senden des SYN-Packets auf dem nun offenen Port lauschen müsse.
    Wie lautet dieser denn nun?

    Wohl kaum der Port mit dem man sich zum Server verband.
    Und Gott alleine weiß alles am allerbesten und besser.
    Edit:
    Hab eine Extension geschrieben die trotz nicht vorhandenen LocalEndPoint den Port wiedergibt , durch den die SYN gesendet wurde( netstat):


    Die Extension:

    C#-Quellcode

    1. public static int SendSYN(this Socket socket, EndPoint ipep)
    2. {
    3. Task.Run(() =>
    4. {
    5. socket.Connect(ipep);
    6. });
    7. System.Diagnostics.Process proc = new System.Diagnostics.Process();
    8. proc.StartInfo.FileName = "netstat";
    9. proc.StartInfo.UseShellExecute = false;
    10. proc.StartInfo.RedirectStandardOutput = true;
    11. proc.Start();
    12. string[] results = proc.StandardOutput.ReadToEnd().Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
    13. string raw_port = results[results.Length - 2].Split(':')[1];
    14. string digits = "";
    15. for (int i = 0; i < raw_port.ToCharArray().Length; i++)
    16. {
    17. if (char.IsDigit(raw_port.ToCharArray()[i]))
    18. digits += raw_port.ToCharArray()[i];
    19. else break;
    20. }
    21. return int.Parse(digits);
    22. }


    EDIT II. : Lese gerade das dieser Port nicht der Port sein darf auf dem gelauscht werden solle... verdammt.
    Welcher Port ist denn nun relevant?
    Und Gott alleine weiß alles am allerbesten und besser.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „φConst“ ()

    Beim Öffnen einer Verbindung entsteht ein lokaler Endpunkt, ein Paar aus IP-Adresse und Port. Auf diesem Port musst du lauschen. Verbinde z.B. zu 1.2.3.4:80 (Remote-Endpunkt). Dein Rechner (mit IP w.x.y.z) belegt dabei einen zufälligen Port, z.B. 2304. Der lokale Endpunkt ist dann w.x.y.z:2304.

    Lade mal eine Webseite und rufe in einer Konsole netstat -an auf, dann siehst du, was ich meine:

    Quellcode

    1. Proto Local Address Foreign Address State
    2. TCP 192.168.2.80:20168 81.7.19.124:443 ESTABLISHED


    Edit: Hier ein Beispiel:

    VB.NET-Quellcode

    1. ' This is peer A.
    2. Dim s As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP)
    3. Dim rep As New IPEndPoint(IPAddress.Parse("1.2.3.4"), 5555) ' Peer B / public IP of NAT_B
    4. Dim e As New SocketAsyncEventArgs
    5. e.RemoteEndPoint = rep
    6. Dim listenPort As Int32
    7. Try
    8. s.ConnectAsync(e)
    9. listenPort = DirectCast(s.LocalEndPoint, IPEndPoint).Port
    10. Catch ex As SocketException
    11. ' Connect() wird scheitern weil NAT_B die Verbindung blockiert - egal.
    12. End Try
    13. ' Lausche auf listenPort, weil NAT_A gerade dieses Mapping erzeugt hat:
    14. ' Private_IP(Peer A):listenPort -- Public_IP(NAT_B):5555 on Public_IP(NAT_A):P
    15. ' oder kürzer: Private_IP(Peer A):listenPort on P.
    16. ' [...]
    17. ' Peer B kann nun auf Public_IP(NAT_A):P verbinden. Hat Peer B den Wert von P
    18. ' richtig geraten, ist die Verbindung erfolgreich und NAT_A leitet die Verbindung
    19. ' auf Private_IP(Peer A):listenPort weiter, wo Peer A lauscht.

    Gruß
    hal2000

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „hal2000“ ()

    Guten Abend,
    ja genau diesen Port hat die Extension aus netstat extrahiert:

    Der Befehl netstat -an gibt folgenden Wert wieder:



    Und die Extension ebenfalls.
    Also doch richtig.

    EDIT: haha, deines ist eleganter.
    Mit async funktioniert es natürlich auch:

    C#-Quellcode

    1. public static int SendSYN(this Socket socket, EndPoint ipep)
    2. {
    3. SocketAsyncEventArgs e = new SocketAsyncEventArgs();
    4. e.RemoteEndPoint = ipep;
    5. try
    6. {
    7. socket.ConnectAsync(e);
    8. IPEndPoint ipEndP = (IPEndPoint)socket.LocalEndPoint;
    9. return ipEndP.Port;
    10. }
    11. catch { return -1; }
    12. }



    Und Gott alleine weiß alles am allerbesten und besser.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „φConst“ ()

    Guten Abend,
    eine reine Interessensfrage :
    Wieso darf ich denn dem Socket von dem aus die SYN gesendet wird keinen festen Port zuweisen mittels .Bind()?
    Zwangsläufig muss ja dann die NAT exakt diesen Port freigeben.. somit erübrigen sich doch theoretisch die anderen Schritte?

    EDIT: Nicht möglich, habe ich gerade feststellen können.
    Der lokale Port ist nicht identisch mit dem vom NAT geöffneten Port.

    Der Client:

    C#-Quellcode

    1. static void Main(string[] args)
    2. {
    3. int syn_port = 0;
    4. bool continuePunching = true;
    5. Socket socketPuncher = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    6. Socket connectorSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    7. Socket acceptorSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    8. Socket acceptedSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    9. IPEndPoint ownEndpoint = null;
    10. IPEndPoint peerEndpoint = null;
    11. connectorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    12. acceptorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    13. // CONNECT SOCKET TO SERVER AND CREATE OPEN PORT
    14. connectorSocket.Connect(new IPEndPoint(IPAddress.Parse("WAN"), 8000));
    15. ownEndpoint = (IPEndPoint)connectorSocket.LocalEndPoint;
    16. while (!connectorSocket.Connected) { }
    17. Console.ForegroundColor = ConsoleColor.Green;
    18. Console.WriteLine("Peer successfully connected to server.");
    19. Console.ForegroundColor = ConsoleColor.Gray;
    20. BinaryReader peerEndPointReader = new BinaryReader(new NetworkStream(connectorSocket));
    21. while (connectorSocket.Connected)
    22. {
    23. string incoming = peerEndPointReader.ReadString();
    24. peerEndpoint = new IPEndPoint(IPAddress.Parse(incoming.Split(':')[0]), int.Parse(incoming.Split(':')[1]));
    25. break;
    26. }
    27. Console.ForegroundColor = ConsoleColor.Green;
    28. Console.WriteLine("RemoteIP received: " + peerEndpoint);
    29. Console.WriteLine("Starting punching!");
    30. Console.ForegroundColor = ConsoleColor.Gray;
    31. connectorSocket.Close();
    32. connectorSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    33. connectorSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
    34. Task.Run(() =>
    35. {
    36. while (continuePunching)
    37. {
    38. syn_port = socketPuncher.SendSYN(new IPEndPoint(peerEndpoint.Address, peerEndpoint.Port)).Port;
    39. }
    40. });
    41. while (syn_port == 0) { }
    42. Task.Run(() =>
    43. {
    44. while (continuePunching)
    45. {
    46. for (int i = -10; i < 10; i++)
    47. {
    48. if (continuePunching)
    49. {
    50. try
    51. {
    52. connectorSocket.Connect(new IPEndPoint(ownEndpoint.Address, peerEndpoint.Port + i));
    53. Console.ForegroundColor = ConsoleColor.Green;
    54. Console.WriteLine("CLIENT CONNECTED!");
    55. Console.ForegroundColor = ConsoleColor.Gray;
    56. BinaryReader binReaderIn = new BinaryReader(new NetworkStream(connectorSocket));
    57. BinaryWriter binWriterIn = new BinaryWriter(new NetworkStream(connectorSocket));
    58. binWriterIn.Write("HELLO BRO!");
    59. binWriterIn.Flush();
    60. }
    61. catch
    62. {
    63. }
    64. }
    65. }
    66. }
    67. });
    68. Console.ForegroundColor = ConsoleColor.Green;
    69. Console.WriteLine("STARTING!");
    70. Console.ForegroundColor = ConsoleColor.Gray;
    71. acceptorSocket.Bind(new IPEndPoint( ownEndpoint.Address , syn_port));
    72. acceptorSocket.Listen(1);
    73. Console.WriteLine("Listening on :" + syn_port);
    74. while (true)
    75. {
    76. acceptedSocket = acceptorSocket.Accept();
    77. Console.ForegroundColor = ConsoleColor.Green;
    78. Console.WriteLine("CLIENT RECEIVED!");
    79. Console.ForegroundColor = ConsoleColor.Gray;
    80. break;
    81. }
    82. continuePunching = false;
    83. BinaryReader binReader = new BinaryReader(new NetworkStream(acceptedSocket));
    84. BinaryWriter binWriter = new BinaryWriter(new NetworkStream(acceptedSocket));
    85. while (acceptedSocket.Connected)
    86. {
    87. Console.WriteLine(binReader.ReadString());
    88. }
    89. Console.Read();
    90. }
    Und Gott alleine weiß alles am allerbesten und besser.

    Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „φConst“ ()

    MSDN schrieb:

    If you are using a connection-oriented protocol and did not call Bind before calling Connect, the underlying service provider will assign the local network address and port number.

    Heißt im Umkehrschluss: Du kannst den lokalen Port festlegen. Das nützt dir aber nichts, weil du den NAT-Port trotzdem nicht kennst. NAT ersetzt nicht nur die IP, sondern auch den Port. Das ist übrigens der Grund, warum Menschen, die das OSI-Modell verstehen, NAT nicht mögen: Es greift unberechtigterweise in eine höhere Protokollschicht (hier Layer 4) ein, obwohl es auf Layer 3 arbeitet (network address translation).
    Gruß
    hal2000

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „hal2000“ ()

    hal2000 schrieb:

    Das ist übrigens der Grund, warum Menschen, die das OSI-Modell verstehen, NAT nicht mögen: Es greift unberechtigterweise in eine höhere Protokollschicht (hier Layer 4) ein, obwohl es auf Layer 3 arbeitet (network address translation).


    Das ist der Grund, warum die Leute, die die Protokolle definieren, dafür auch einen richtigen Namen vergeben haben:

    There are two variations to traditional NAT, namely Basic NAT and
    NAPT (Network Address Port Translation).

    RFC2663