Java UDP Hole Punching

tsserver

Cadet 3rd Year
Registriert
Juli 2009
Beiträge
35
Hallo zusammen

Ein Kolleg und ich probieren gerade, mit Java einen P2P-Chat übers Internet (2 verschiedene NATs) hinzukriegen. Das Ziel dabei ist, eine funktionierende P2P-Connection hinzukriegen, die wir dann in Zukunft für andere Funktionen brauchen wollen, bei denen es mehr Sinn macht als bei einem Chat (bitte also keine Einwände, mit welcher anderen Technik man den Chat besser zum Laufen bringen könnte - es geht nicht um den Chat, sondern um die P2P-Verbindung).
Wir haben uns im Internet über UDP HP informiert und entsprechend einen Rendez-vous-Server mit Java eingerichtet, der den beiden Chatclients die externe IP-Adresse und den externen Port des jeweils anderen Chatclients mitteilt. Beide Chatclients beginnen daraufhin, dem jeweils anderen Chatclient regelmässig (ca. 2 Mal pro Sekunde) Nachrichten zu schicken, damit der Router die entsprechende UDP-Verbindung für eingehende Pakete öffnet (so zumindest nach der Theorie des UDP Hole Punching). Leider kommen im Client eingegebene Chatnachrichten, die ebenfalls direkt an den anderen Client geschickt werden, bei diesem nicht an. Erst wenn der Client, der die Chatnachricht erhalten sollte, eine Portweiterleitung beim Router vornimmt, funktionierts - diese Portweiterleitung wollen wir ja aber gerade vermeiden, denn das ist ja auch der Sinn des UDP HP, soweit wir es verstanden haben.

Hat jemand eine Idee, wieso der Router die Chatnachricht-Pakete trotz UDP HP blockiert ?

Danke im Voraus
Tsserver
 
Es könnte sein, dass das NAT auch die Ports verändert, dann kann nix ankommen.
Wenns nur einer der beiden Router macht, kann man Tricksen (Stichwort IMCP, IMCP Paketew werden von (fast) allen Routern weitergeleitet).

Ich empfehl euch Wireshark, damit ihr seht, was übers Netzwerk läuft.

BTW: Ich hoff, ihr habt kein Logikfehler drin:
Ihr müsst jeweils Quell und Zielport richtig setzten. Der Router filtert die Pakete auf Basis dieser Portpaare.
 
Zuletzt bearbeitet:
Erstmals danke für die Antwort. Wir haben nun herausgefunden, dass das NAT effektiv die Ports verändert, d.h. pro Zielip wird ein anderer externer Port ausgewählt (bei beiden Routern wird immer die nächsthöhere Portnummer ausgewählt).

Wie ist es möglich, den externen Port für eine bestimmte Zielip herauszufinden ? Theoretisch wäre es ja möglich, die nächsthöheren Ports auszuprobieren, aber gibt es hier eine bessere Möglichkeit ?
 
Nein, gibt es nicht...
Das ist ja das blöde an NAT, du kannst keine gescheite Verbindung aufbauen.
Ein Ausweg wäre die Verwendung von IPv6, da musst du dir keine Gedanken machen (dann geht auch TCP).

EDIT:
Idee:
Bei einem ICMP Paket, das zurückkommt wegen einer zu kleinen TTL (Traceroute-Funktion), steht doch theoretisch der Port drin, sowohl Quelle als auch Ziel, dadurch hast du von einer Seite schon einmal das Portpaar. Jetzt hast du für die andere Seite nur noch max. 65535 Ports durchzuprobieren, bis der Router diese richtig übersetzt.

Anhang:
Ihr seid euch sicher, dass der Router die Ports verändert und ihr sie nicht ausversehen gar nicht festlegt (die Quellports).
 
Zuletzt bearbeitet:
Wäre mal gut, wenn du relevante Teile aus dem Code posten könntest. Man kann so echt nicht feststellen, ob ihr vllt. was falsch implementiert habt oder ob es wirklich am Router liegt etc.
 
tsserver schrieb:
Wir haben nun herausgefunden, dass das NAT effektiv die Ports verändert, d.h. pro Zielip wird ein anderer externer Port ausgewählt (bei beiden Routern wird immer die nächsthöhere Portnummer ausgewählt).
Wenn ich mir das durchlese und mit dieser Übersicht vergleiche, hört sich das für mich nach symmetrischem NAT an. Da hat UDP HP keine Chance. Die eigentliche Kommunikation muss in dem Fall dann auch über den Rendez-vouz-Server laufen.
 
Der Client führt folgenden Code nur einmal bei Programmstart aus:

Code:
udp = new Udp();
UDPHandler udpHandler;
udp.addUdpListener(udpHandler = new UDPHandler(udp));

Die selber geschrieben Udp-Klasse macht im Wesentlichen Folgendes (leicht abgekürzt):
Code:
public class Udp implements ActionListener {
public Udp() throws SocketException, ParserConfigurationException {
		this.udp = new JS_UDP(true, 0);
		this.udp.addActionListener(this);
		this.listeners = new ArrayList<UdpListenerI>();
}
}

Die ebenfalls selber geschriebene JS_UDP-Klasse tut folgendes im Konstruktor:
Code:
public JS_UDP(boolean onlyPrimitives, int port) throws SocketException {
		this.onlyPrimitives = onlyPrimitives;
		receavedMessages = new ArrayList<Message>();
		this.listeners = new ArrayList<ActionListener>();
		if (port != 0)
			this.socket = new DatagramSocket(port);
		else
			this.socket = new DatagramSocket();
		this.socket.setSendBufferSize(16384);
		this.socket.setReceiveBufferSize(16384);
		this.sendBufferSize = this.socket.getSendBufferSize();
		String threadname = Thread.currentThread().getName();
		this.reader = new UDPReader(this, threadname, this.onlyPrimitives);
	}

Wenn der Chatclient eine Nachricht an den anderen Chatclient schicken möchte, wird die Nachricht mit folgendem Code an den anderen Client übermittelt:
Code:
DatagramPacket dpacket = new DatagramPacket(data, data.length,
				targetIp, port);
		try {
			this.socket.send(dpacket);
		} catch (IOException e) {
			if (printErrorStackTrace)
				e.printStackTrace();
			return false;
		}
		return true;

Zum Empfangen von UDP-Nachrichten (also auch von Chatnachrichten des anderen Clients) wird folgender Code verwendet:
Code:
protected class UDPReader extends Thread {

		private JS_UDP udp;
		private int currentEventId;
		private boolean onlyPrimitives;

		public UDPReader(JS_UDP udp, String threadname, boolean onlyPrimitives) {
			super(threadname + " (UDPReader)");
			this.setDaemon(true);
			this.onlyPrimitives = onlyPrimitives;
			this.udp = udp;
			this.currentEventId = 1;
			this.start();
		}

		public void run() {
			int size;
			try {
				size = this.udp.socket.getReceiveBufferSize();
			} catch (SocketException e1) {
				size = 8192;
			}
			DatagramPacket packet = new DatagramPacket(new byte[size], size);
			boolean threadDone = false;
			while (!threadDone) {
				try {
					packet.setData(new byte[size]);
					this.udp.socket.receive(packet);
					Message p = new Message(packet, this.onlyPrimitives);
					synchronized (this.udp.receavedMessages) {
						this.udp.receavedMessages.add(p);
					}
					ActionEvent e = new ActionEvent(this.udp,
							this.currentEventId, "packet receaved");
					this.currentEventId++;
					this.udp.notifyListeners(e);
				} catch (SocketException e) {
					threadDone = true;
				} catch (IOException e) {
					if (printErrorStackTrace)
						e.printStackTrace();
				}
			}
		}
	}

Gibt es eine Möglichkeit, eine P2P-Verbindung auch ohne Port Prediction und ohne Portforwarding im NAT der beiden Peers (wohl aber im NAT des Rendez-vous-Servers) herzustellen ?
 
Zuletzt bearbeitet:
Es reicht wenn einer der beiden Endpunkte eine Portfreigabe nutzt. Nur muss jener Endpunkt dann als erstes kontaktiert werden. Ich würde dir raten dich in die genaue Vorgehensweise von Torrent-Netzwerken einzulesen. Da sind Übertragungen auch ohne bei einer Seite möglich, allerdings mit Geschwindigkeitseinbußen durch die Netzwerktechnik.

Portfreigaben bei NAT sind insofern nötig, damit der Router eingehende Verbindungen (sofern sie ohne vorherige ausgehende Verbindung zu dem Endpunkt angekündigt wurden) blind einem (/dem, weil es nur einen geben kann) Clienten zuordnen kann.
 
Zuletzt bearbeitet:
Blitzmerker schrieb:
@Cobinja:
Außer, der Router zählt die Ports immer stur um 1 nach oben, dann kann man zeitkritisch eine Verbindung aufbauen.
Stimmt.
Aber auch nur, wenn zwischen zwei Versuchen keine andere UDP-Verbindung von dem Rechner nach irgendwohin über den Router aufgebaut wurde (wodurch ein weiterer Port hochgezählt würde).
Das hat nichts mit Sicherheit zu tun, sondern mit Glück.
 
Was stehen mir denn für Möglichkeiten offen, ohne Portfreigabe auf dem NAT eines der beiden Peers eine P2P-Verbindung aufzubauen ? Wie sieht es mit UpNP aus, das wird doch von den meisten Routern unterstützt ?
Msn z.B. kriegt - Irrtum vorbehalten - auch eine P2P-Verbindung ohne Portforwarding hin, wenn einander Dateien geschickt werden (dann bekomme ich nämlich jeweils eine Meldung von der Firewall, die mich fragt, ob ich eine Verbindung zum die Datei verschickenden Rechners zulassen will - es erscheint dabei seine Ip-Adresse und sein PC-Name in der Meldung).
Gerade auch für so Dinge wie Dateiübertragung erscheint mir eine P2P-Verbindung als unumgänglich. Es wäre also sehr schön, wenn es da eine Lösung ohne Portfreigabe auf dem NAT eines der beiden Peers gäbe.
 
Du könntest dir noch Adobe Stratus ansehen, das ist auch UDP P2P nur dass zuerst ein Server kontaktiert wird mit dem die UDP-Ports ausgetauscht werden (und somit das Loch in die Firewall gesetzt wird) und der Server dann den Clients die jeweils anderen Ip-Adressen und Ports mitteilt, die nun von außen erreichbar sind.

Von Skype gab es irgendwann auch mal eine gute Dokumentation, wie die das UDP Holepunching machen, die hatten da auch noch 2-3 Kniffe.
 
@Blitzmerker:
Es gibt keinen Konstruktor der Klasse DatagramSocket mit 2 int-Argumenten, höchstens "int port, InetAddress laddr".

Der Port war bisher nicht festgelegt. Aber auch, wenn ich den lokalen Port beim erstellen der UDP-Verbindung festlege, verwendet das NAT immer völlig andere externe Ports, die nicht voraussehbar sind und nichts mit den lokalen zu tun haben. Das Problem ist eben, dass das NAT pro Zieladresse einen anderen externen Port wählt. Meistens wählt das NAT zwar den nächsthöheren Port wie beschrieben, doch auch das ist nicht garantiert.

@ice-breaker:
Ich werde mir die Dokumentation von Skype und Adobe Stratus mal genauer anschauen, danke für den Tipp.
 
Zuletzt bearbeitet:
Es reicht wenn einer der beiden Endpunkte eine Portfreigabe nutzt. Nur muss jener Endpunkt dann als erstes kontaktiert werden. Ich würde dir raten dich in die genaue Vorgehensweise von Torrent-Netzwerken einzulesen

Ich kenne keinen Torrent Client der funzt, wenn man auf Port Forwarding verzichtet.
 
Das ganze geht relativ ähnlich auch mit TCP (allerdings nicht ganz so einfach und effizient), falls ihr dadran interesse habt, kann ichs mal raussuchen.

Würde euch eh TCP empfehlen UDP macht für nen Chat keinen Sinn.
Sonst wundert ihr euch plötzlich wieso manchmal halbe Nachrichten verschwinden...
 
Zuletzt bearbeitet:
Für UPnP soll angeblich die entsprechende Library von Azureus/Vuze recht gut sein, vielleicht findet man da auch ein paar Tips für NAT-traversal.
 
alotofblabla schrieb:
Sonst wundert ihr euch plötzlich wieso manchmal halbe Nachrichten verschwinden...

halbe Nachrichten können nicht verschwinden, entweder eine UDP-Nachricht kommt komplett fehlerfrei an oder nicht, einen Zwischenweg gibt es nicht.
 
das kommt drauf an, ob sie in einem paket verschickt wird, oder nicht
Aber da Chatnachrichten meistens in ein Paket passen, ist das Unwahrscheinlich, das stimmt.

EDIT:
"Die maximale Größe eines UDP-Datagrammes beträgt 65.535 Bytes, da das Length-Feld des UDP-Headers 16 Bit lang ist und die größte mit 16 Bit darstellbare Zahl gerade 65.535 ist. Solch große Segmente werden jedoch von IP fragmentiert übertragen."

Selbst wenn wir von nem viertel ausgehen, dh 16kb dürfte das dicke für chatnachrichten reichen. ;-)

Das Problem ist eher, dass Nachrichten komplett verlorengehen können...
 
Zuletzt bearbeitet:
Zurück
Oben