/// <summary>Allows traversal of routers/firewalls to this host.</summary> /// <param name="privatePort">The port the host is listening from.</param> /// <returns>If successful a INatTraversalSession is returned, otherwise null.</returns> /// <remarks>This method should be called by a host after having started a DirectPlay session.</remarks> public static INatTraversalSession EnableNatTraversal(int privatePort) { NatTraversalSession session = new NatTraversalSession(privatePort); using (session.NatTraversalEnabled = new ManualResetEvent(false)) { new Thread(new ParameterizedThreadStart(notificationLoop)).Start(session); session.NatTraversalEnabled.WaitOne(2200, false); session.NatTraversalEnabled = null; } return(session); }
private static unsafe void notificationLoop(object natTraversalSession) { NatTraversalSession session = (NatTraversalSession)natTraversalSession; Timer keepAliveTimer = null; try { IPEndPoint serviceEndPoint = null; // connect to service to open a port on the NAT devices using (Socket notificationSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { notificationSocket.Connect(natTraversalServiceUrl, natTraversalServicePort); serviceEndPoint = (IPEndPoint)notificationSocket.RemoteEndPoint; session.LocalEndPoint = (IPEndPoint)notificationSocket.LocalEndPoint; } // create a new socket in listening mode using the port just opened using (Socket notificationSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) { notificationSocket.ReceiveTimeout = 1000; notificationSocket.Bind(session.LocalEndPoint); // send "open notification channel" message { MsgOpenNotificationChannel msg = new MsgOpenNotificationChannel(session.SessionId); byte[] datagram = new byte[sizeof(MsgOpenNotificationChannel)]; for (int i = 0; i < datagram.Length; ++i) { datagram[i] = ((byte *)&msg)[i]; } notificationSocket.SendTo(datagram, serviceEndPoint); } while (true) { // block until a message is received or 1 second has elapsed EndPoint any = new IPEndPoint(IPAddress.Any, 0); byte[] bytesReceived = new byte[1024]; int bytesReceivedLength = notificationSocket.ReceiveFrom(bytesReceived, ref any); IPEndPoint remoteEndPoint = (IPEndPoint)any; // test for stop condition: a message send from this local endpoint to itself if (remoteEndPoint.Equals(session.LocalEndPoint)) { break; } else if (remoteEndPoint.Equals(serviceEndPoint)) { // handle the message if (bytesReceivedLength >= 3 && bytesReceived[0] == 'z' && bytesReceived[1] == 't') { switch (bytesReceived[2]) { case (byte)MessageType.AckOpenNotificationChannel: if (bytesReceivedLength == sizeof(MsgAckOpenNotificationChannel)) { MsgAckOpenNotificationChannel msg = new MsgAckOpenNotificationChannel(); for (int i = 0; i < sizeof(MsgAckOpenNotificationChannel); ++i) { ((byte *)&msg)[i] = bytesReceived[i]; } // handle MsgAckOpenNotificationChannel // send "host" message, from the same port as DirectPlay (spoofed) IPEndPoint spoofedEndPoint = new IPEndPoint(session.LocalEndPoint.Address, session.PrivatePort); MsgHost msgHost = new MsgHost(session.SessionId, session.LocalEndPoint.Address, (ushort)session.PrivatePort); byte[] datagram = new byte[sizeof(MsgHost)]; for (int i = 0; i < datagram.Length; ++i) { datagram[i] = ((byte *)&msgHost)[i]; } PortSpoofing.Send(spoofedEndPoint, serviceEndPoint, datagram); } break; case (byte)MessageType.AckHost: if (bytesReceivedLength == sizeof(MsgAckHost)) { if (keepAliveTimer == null) // ignore if already handled { MsgAckHost msg = new MsgAckHost(); for (int i = 0; i < sizeof(MsgAckHost); ++i) { ((byte *)&msg)[i] = bytesReceived[i]; } // handle MsgAckHost // record public address/port session.PublicIpAddress = new IPAddress((uint)IPAddress.HostToNetworkOrder((int)msg.HostPublicIp)).ToString(); session.PublicPort = msg.HostPublicPort; // notify main thread that NAT traversal is enabled session.Enabled = true; if (session.NatTraversalEnabled != null) { session.NatTraversalEnabled.Set(); } // set reception so it can't time-out notificationSocket.ReceiveTimeout = 0; // send a keep alive message every 19 seconds KeepAliveSettings settings = new KeepAliveSettings(); settings.SpoofedLocalEndPoint = session.LocalEndPoint; settings.ServiceEndPoint = serviceEndPoint; MsgKeepAlive msgKeepAlive = new MsgKeepAlive(session.SessionId); settings.Datagram = new byte[sizeof(MsgKeepAlive)]; for (int i = 0; i < settings.Datagram.Length; ++i) { settings.Datagram[i] = ((byte *)&msgKeepAlive)[i]; } keepAliveTimer = new Timer(new TimerCallback(keepAlive), settings, 19000, 19000); } } break; case (byte)MessageType.Connect: if (bytesReceivedLength == sizeof(MsgConnect)) { MsgConnect msg = new MsgConnect(); for (int i = 0; i < sizeof(MsgConnect); ++i) { ((byte *)&msg)[i] = bytesReceived[i]; } // handle MsgConnect // punch hole targeting client's public and private addresses IPAddress publicClientAddress = new IPAddress((uint)IPAddress.HostToNetworkOrder((int)msg.ClientPublicIp)); IPAddress privateClientAddress = new IPAddress((uint)IPAddress.HostToNetworkOrder((int)msg.ClientPrivateIp)); IPEndPoint spoofedEndPoint = new IPEndPoint(session.LocalEndPoint.Address, session.PrivatePort); byte[] emptyDatagram = new byte[0]; PortSpoofing.Send(spoofedEndPoint, new IPEndPoint(publicClientAddress, msg.ClientPublicPort), emptyDatagram); PortSpoofing.Send(spoofedEndPoint, new IPEndPoint(privateClientAddress, msg.ClientPrivatePort), emptyDatagram); // send acknowledgement through notification channel MsgAckConnect msgAckConnect = new MsgAckConnect(session.SessionId, publicClientAddress, msg.ClientPublicPort); byte[] datagram = new byte[sizeof(MsgAckConnect)]; for (int i = 0; i < datagram.Length; ++i) { datagram[i] = ((byte *)&msgAckConnect)[i]; } notificationSocket.SendTo(datagram, serviceEndPoint); } break; } } } } } } catch { if (!session.Enabled && session.NatTraversalEnabled != null) { session.NatTraversalEnabled.Set(); } } finally { if (keepAliveTimer != null) { keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); keepAliveTimer.Dispose(); } } }