570 lines
21 KiB
C#
570 lines
21 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
|
|
namespace TCMPortMapper
|
|
{
|
|
class NATPMPPortMapper
|
|
{
|
|
public delegate void PMDidFail(NATPMPPortMapper sender);
|
|
public delegate void PMDidGetExternalIPAddress(NATPMPPortMapper sender, IPAddress ip);
|
|
public delegate void PMDidBeginWorking(NATPMPPortMapper sender);
|
|
public delegate void PMDidEndWorking(NATPMPPortMapper sender);
|
|
public delegate void PMDidReceiveBroadcastExternalIPChange(NATPMPPortMapper sender, IPAddress ip, IPAddress senderIP);
|
|
|
|
public event PMDidFail DidFail;
|
|
public event PMDidGetExternalIPAddress DidGetExternalIPAddress;
|
|
public event PMDidBeginWorking DidBeginWorking;
|
|
public event PMDidEndWorking DidEndWorking;
|
|
public event PMDidReceiveBroadcastExternalIPChange DidReceiveBroadcastExternalIPChange;
|
|
|
|
private Object multiThreadLock = new Object();
|
|
private Object singleThreadLock = new Object();
|
|
|
|
private volatile ThreadID threadID;
|
|
private volatile ThreadFlags refreshExternalIPThreadFlags;
|
|
private volatile ThreadFlags updatePortMappingsThreadFlags;
|
|
|
|
private Timer updateTimer;
|
|
private uint updateInterval;
|
|
|
|
private UdpClient udpClient;
|
|
|
|
private IPAddress lastBroadcastExternalIP;
|
|
|
|
private enum ThreadID
|
|
{
|
|
None = 0,
|
|
RefreshExternalIP = 1,
|
|
UpdatePortMappings = 2
|
|
}
|
|
|
|
[Flags]
|
|
private enum ThreadFlags
|
|
{
|
|
None = 0,
|
|
ShouldQuit = 1,
|
|
ShouldRestart = 2
|
|
}
|
|
|
|
// Standard routine:
|
|
//
|
|
// Refresh -> triggers RefreshExternalIPThread -> Upon completion of thread, UpdatePortMappings is called.
|
|
// Case 1: No threads are running -> perfect, trigger thread as planned
|
|
// Case 2: RefreshExternalIPThread is running -> this thread is aborted, and then Refresh is called again
|
|
// Case 3: UpdatePortMappingsThread is running -> this thread is aborted, and then Refresh is called again
|
|
//
|
|
// UpdatePortMappings -> triggers UpdatePortMappingsThread -> Upon completion of thread, AdjustUpdateTimer is called
|
|
// Case 1: No threads are running -> perfect, trigger thread as planned
|
|
// Case 2: RefreshExternalIPInThread is running -> That's fine, we do nothing
|
|
// Case 3: UpdatePortMappingsInThread is running -> this thread is aborted, and restarted from beginning
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#region Public API
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
public NATPMPPortMapper()
|
|
{
|
|
// Until I find a way around this bug, there's no reason to setup the udp client...
|
|
|
|
// Add UDP listener for public ip update packets
|
|
// udpClient = new UdpClient(5351);
|
|
|
|
// Note: The following code throws an exception for some reason.
|
|
// The JoinMulticastGroup works fine for every multicast address except 224.0.0.1
|
|
// Another reason why windows sucks.
|
|
// udpClient.JoinMulticastGroup(IPAddress.Parse("224.0.0.1"));
|
|
|
|
// So basically, the udpClient won't be receiving anything.
|
|
// I consider this to be a bug in Windows and/or .Net.
|
|
|
|
// udpClient.BeginReceive(new AsyncCallback(udpClient_DidReceive), null);
|
|
}
|
|
|
|
public void Refresh()
|
|
{
|
|
updateInterval = 3600 / 2;
|
|
refreshExternalIPThreadFlags = ThreadFlags.None;
|
|
updatePortMappingsThreadFlags = ThreadFlags.None;
|
|
|
|
//threadID = ThreadID.RefreshExternalIP;
|
|
|
|
RefreshExternalIPThread();
|
|
|
|
//if (threadID == ThreadID.RefreshExternalIP)
|
|
//{
|
|
// refreshExternalIPThreadFlags = ThreadFlags.ShouldQuit | ThreadFlags.ShouldRestart;
|
|
//}
|
|
//else if (threadID == ThreadID.UpdatePortMappings)
|
|
//{
|
|
// updatePortMappingsThreadFlags = ThreadFlags.ShouldQuit;
|
|
//}
|
|
}
|
|
|
|
public void UpdatePortMappings()
|
|
{
|
|
updatePortMappingsThreadFlags = ThreadFlags.None;
|
|
|
|
//threadID = ThreadID.UpdatePortMappings;
|
|
|
|
UpdatePortMappingsThread();
|
|
|
|
//if (threadID == ThreadID.UpdatePortMappings)
|
|
//{
|
|
// updatePortMappingsThreadFlags = ThreadFlags.ShouldQuit | ThreadFlags.ShouldRestart;
|
|
//}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
if (updateTimer != null)
|
|
{
|
|
updateTimer.Dispose();
|
|
updateTimer = null;
|
|
}
|
|
// Restart update to remove mappings before stopping
|
|
UpdatePortMappings();
|
|
}
|
|
|
|
public void StopBlocking()
|
|
{
|
|
refreshExternalIPThreadFlags = ThreadFlags.ShouldQuit;
|
|
updatePortMappingsThreadFlags = ThreadFlags.ShouldQuit;
|
|
|
|
NATPMP.natpmp_t natpmp = new NATPMP.natpmp_t();
|
|
NATPMP.initnatpmp(ref natpmp);
|
|
|
|
List<PortMapping> mappingsToRemove = PortMapper.SharedInstance.PortMappingsToRemove;
|
|
while (mappingsToRemove.Count > 0)
|
|
{
|
|
PortMapping pm = mappingsToRemove[0];
|
|
|
|
if (pm.MappingStatus == PortMappingStatus.Mapped)
|
|
{
|
|
RemovePortMapping(pm, ref natpmp);
|
|
}
|
|
|
|
mappingsToRemove.RemoveAt(0);
|
|
}
|
|
|
|
List<PortMapping> mappingsToStop = PortMapper.SharedInstance.PortMappings;
|
|
for (int i = 0; i < mappingsToStop.Count; i++)
|
|
{
|
|
PortMapping pm = mappingsToStop[i];
|
|
|
|
if (pm.MappingStatus == PortMappingStatus.Mapped)
|
|
{
|
|
RemovePortMapping(pm, ref natpmp);
|
|
}
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#endregion
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#region Delegate Methods
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
protected virtual void OnDidFail()
|
|
{
|
|
if (DidFail != null)
|
|
{
|
|
PortMapper.SharedInstance.Invoke(DidFail, this);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnDidGetExternalIPAddress(IPAddress ip)
|
|
{
|
|
if (DidGetExternalIPAddress != null)
|
|
{
|
|
DidGetExternalIPAddress(this, ip);
|
|
//PortMapper.SharedInstance.Invoke(DidGetExternalIPAddress, this, ip);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnDidBeginWorking()
|
|
{
|
|
if (DidBeginWorking != null)
|
|
{
|
|
// This is thread safe, so there's no need to Invoke it
|
|
DidBeginWorking(this);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnDidEndWorking()
|
|
{
|
|
if (DidEndWorking != null)
|
|
{
|
|
// This is thread safe, so there's no need to Invoke it
|
|
DidEndWorking(this);
|
|
}
|
|
}
|
|
|
|
protected virtual void OnDidReceiveBroadcastExternalIPChange(IPAddress externalIP, IPAddress senderIP)
|
|
{
|
|
if (lastBroadcastExternalIP == null)
|
|
{
|
|
lastBroadcastExternalIP = externalIP;
|
|
}
|
|
else
|
|
{
|
|
if (lastBroadcastExternalIP == externalIP)
|
|
{
|
|
// To accommodate packet loss, the NAT-PMP protocol may broadcast
|
|
// an external IP address change up to 10 times.
|
|
// We only need to broadcast it once.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (DidReceiveBroadcastExternalIPChange != null)
|
|
{
|
|
DidReceiveBroadcastExternalIPChange(this, externalIP, senderIP);
|
|
//PortMapper.SharedInstance.Invoke(DidReceiveBroadcastExternalIPChange, this, externalIP, senderIP);
|
|
}
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#endregion
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#region Private API
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
private bool AddPortMapping(PortMapping portMapping, ref NATPMP.natpmp_t natpmp)
|
|
{
|
|
return ApplyPortMapping(portMapping, false, ref natpmp);
|
|
}
|
|
|
|
private bool RefreshPortMapping(PortMapping portMapping, ref NATPMP.natpmp_t natpmp)
|
|
{
|
|
return ApplyPortMapping(portMapping, false, ref natpmp);
|
|
}
|
|
|
|
private bool RemovePortMapping(PortMapping portMapping, ref NATPMP.natpmp_t natpmp)
|
|
{
|
|
return ApplyPortMapping(portMapping, true, ref natpmp);
|
|
}
|
|
|
|
private bool ApplyPortMapping(PortMapping portMapping, bool remove, ref NATPMP.natpmp_t natpmp)
|
|
{
|
|
NATPMP.natpmpresp_t response = new NATPMP.natpmpresp_t();
|
|
int r;
|
|
Win32.TimeValue timeout = new Win32.TimeValue();
|
|
Win32.FileDescriptorSet fds = new Win32.FileDescriptorSet(1);
|
|
|
|
if (!remove)
|
|
{
|
|
portMapping.SetMappingStatus(PortMappingStatus.Trying);
|
|
}
|
|
PortMappingTransportProtocol protocol = portMapping.TransportProtocol;
|
|
|
|
for (int i = 1; i <= 2; i++)
|
|
{
|
|
PortMappingTransportProtocol currentProtocol;
|
|
if (i == 1)
|
|
currentProtocol = PortMappingTransportProtocol.UDP;
|
|
else
|
|
currentProtocol = PortMappingTransportProtocol.TCP;
|
|
|
|
if (protocol == currentProtocol || protocol == PortMappingTransportProtocol.Both)
|
|
{
|
|
r = NATPMP.sendnewportmappingrequest(ref natpmp,
|
|
(i == 1) ? NATPMP.PROTOCOL_UDP : NATPMP.PROTOCOL_TCP,
|
|
portMapping.LocalPort, portMapping.DesiredExternalPort, (uint)(remove ? 0 : 3600));
|
|
|
|
do
|
|
{
|
|
fds.Count = 1;
|
|
fds.Array[0] = (IntPtr)natpmp.s;
|
|
NATPMP.getnatpmprequesttimeout(ref natpmp, ref timeout);
|
|
|
|
Win32.select(0, ref fds, IntPtr.Zero, IntPtr.Zero, ref timeout);
|
|
|
|
r = NATPMP.readnatpmpresponseorretry(ref natpmp, ref response);
|
|
}
|
|
while (r == NATPMP.ERR_TRYAGAIN);
|
|
|
|
if (r < 0)
|
|
{
|
|
portMapping.SetMappingStatus(PortMappingStatus.Unmapped);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (remove)
|
|
{
|
|
portMapping.SetMappingStatus(PortMappingStatus.Unmapped);
|
|
}
|
|
else
|
|
{
|
|
updateInterval = Math.Min(updateInterval, response.pnu_newportmapping.lifetime / 2);
|
|
if (updateInterval < 60)
|
|
{
|
|
DebugLog.WriteLine("NAT-PMP: ApplyPortMapping: Caution - new port mapping had a lifetime < 120 ({0})",
|
|
response.pnu_newportmapping.lifetime);
|
|
|
|
updateInterval = 60;
|
|
}
|
|
portMapping.SetExternalPort(response.pnu_newportmapping.mappedpublicport);
|
|
portMapping.SetMappingStatus(PortMappingStatus.Mapped);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#endregion
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#region Refresh External IP Thread
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
private void RefreshExternalIPThread()
|
|
{
|
|
OnDidBeginWorking();
|
|
|
|
NATPMP.natpmp_t natpmp = new NATPMP.natpmp_t();
|
|
NATPMP.natpmpresp_t response = new NATPMP.natpmpresp_t();
|
|
int r;
|
|
Win32.TimeValue timeout = new Win32.TimeValue();
|
|
Win32.FileDescriptorSet fds = new Win32.FileDescriptorSet(1);
|
|
bool didFail = false;
|
|
|
|
r = NATPMP.initnatpmp(ref natpmp);
|
|
if (r < 0)
|
|
{
|
|
didFail = true;
|
|
}
|
|
else
|
|
{
|
|
r = NATPMP.sendpublicaddressrequest(ref natpmp);
|
|
if (r < 0)
|
|
{
|
|
didFail = true;
|
|
}
|
|
else
|
|
{
|
|
do
|
|
{
|
|
fds.Count = 1;
|
|
fds.Array[0] = (IntPtr)natpmp.s;
|
|
NATPMP.getnatpmprequesttimeout(ref natpmp, ref timeout);
|
|
|
|
Win32.select(0, ref fds, IntPtr.Zero, IntPtr.Zero, ref timeout);
|
|
|
|
r = NATPMP.readnatpmpresponseorretry(ref natpmp, ref response);
|
|
if (refreshExternalIPThreadFlags != ThreadFlags.None)
|
|
{
|
|
DebugLog.WriteLine("NAT-PMP: RefreshExternalIPThread quit prematurely (1)");
|
|
|
|
if ((refreshExternalIPThreadFlags & ThreadFlags.ShouldRestart) > 0)
|
|
{
|
|
Refresh();
|
|
}
|
|
NATPMP.closenatpmp(ref natpmp);
|
|
OnDidEndWorking();
|
|
return;
|
|
}
|
|
}
|
|
while (r == NATPMP.ERR_TRYAGAIN);
|
|
|
|
if (r < 0)
|
|
{
|
|
didFail = true;
|
|
DebugLog.WriteLine("NAT-PMP: IP refresh did time out");
|
|
}
|
|
else
|
|
{
|
|
IPAddress ipaddr = new IPAddress((long)response.pnu_publicaddress.addr);
|
|
OnDidGetExternalIPAddress(ipaddr);
|
|
}
|
|
}
|
|
}
|
|
|
|
NATPMP.closenatpmp(ref natpmp);
|
|
|
|
if (refreshExternalIPThreadFlags != ThreadFlags.None)
|
|
{
|
|
DebugLog.WriteLine("NAT-PMP: RefreshExternalIPThread quit prematurely (2)");
|
|
|
|
if ((refreshExternalIPThreadFlags & ThreadFlags.ShouldRestart) > 0)
|
|
{
|
|
Refresh();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (didFail)
|
|
{
|
|
OnDidFail();
|
|
}
|
|
else
|
|
{
|
|
UpdatePortMappings();
|
|
}
|
|
}
|
|
OnDidEndWorking();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#endregion
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#region Update Port Mappings Thread
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
private void UpdatePortMappingsThread()
|
|
{
|
|
OnDidBeginWorking();
|
|
|
|
NATPMP.natpmp_t natpmp = new NATPMP.natpmp_t();
|
|
NATPMP.initnatpmp(ref natpmp);
|
|
|
|
// Remove mappings scheduled for removal
|
|
|
|
List<PortMapping> mappingsToRemove = PortMapper.SharedInstance.PortMappingsToRemove;
|
|
while ((mappingsToRemove.Count > 0) && (updatePortMappingsThreadFlags == ThreadFlags.None))
|
|
{
|
|
PortMapping mappingToRemove = mappingsToRemove[0];
|
|
|
|
if (mappingToRemove.MappingStatus == PortMappingStatus.Mapped)
|
|
{
|
|
RemovePortMapping(mappingToRemove, ref natpmp);
|
|
}
|
|
|
|
mappingsToRemove.RemoveAt(0);
|
|
}
|
|
|
|
// If the port mapper is running:
|
|
// -Refresh existing mappings
|
|
// -Add new mappings
|
|
// If the port mapper is stopped:
|
|
// -Remove any existing mappings
|
|
|
|
List<PortMapping> mappings = PortMapper.SharedInstance.PortMappings;
|
|
for (int i = 0; i < mappings.Count && updatePortMappingsThreadFlags == ThreadFlags.None; i++)
|
|
{
|
|
PortMapping existingMapping = mappings[i];
|
|
bool isRunning = PortMapper.SharedInstance.IsRunning;
|
|
|
|
if (existingMapping.MappingStatus == PortMappingStatus.Mapped)
|
|
{
|
|
if (isRunning)
|
|
{
|
|
RefreshPortMapping(existingMapping, ref natpmp);
|
|
}
|
|
else
|
|
{
|
|
RemovePortMapping(existingMapping, ref natpmp);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < mappings.Count && updatePortMappingsThreadFlags == ThreadFlags.None; i++)
|
|
{
|
|
PortMapping mappingToAdd = mappings[i];
|
|
bool isRunning = PortMapper.SharedInstance.IsRunning;
|
|
|
|
if (mappingToAdd.MappingStatus == PortMappingStatus.Unmapped && isRunning)
|
|
{
|
|
AddPortMapping(mappingToAdd, ref natpmp);
|
|
}
|
|
}
|
|
|
|
NATPMP.closenatpmp(ref natpmp);
|
|
|
|
if (PortMapper.SharedInstance.IsRunning)
|
|
{
|
|
if ((updatePortMappingsThreadFlags & ThreadFlags.ShouldRestart) > 0)
|
|
{
|
|
UpdatePortMappings();
|
|
}
|
|
else if ((updatePortMappingsThreadFlags & ThreadFlags.ShouldQuit) > 0)
|
|
{
|
|
Refresh();
|
|
}
|
|
else
|
|
{
|
|
AdjustUpdateTimer();
|
|
}
|
|
}
|
|
|
|
OnDidEndWorking();
|
|
}
|
|
|
|
private void AdjustUpdateTimer()
|
|
{
|
|
if (updateTimer != null)
|
|
{
|
|
updateTimer.Dispose();
|
|
updateTimer = null;
|
|
}
|
|
updateTimer = new Timer(new TimerCallback(UpdatePortMappings), null, (updateInterval * 1000), Timeout.Infinite);
|
|
}
|
|
|
|
private void UpdatePortMappings(object state)
|
|
{
|
|
// Called via timer (on background thread)
|
|
UpdatePortMappings();
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
#endregion
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
private void udpClient_DidReceive(IAsyncResult ar)
|
|
{
|
|
// When the public address changes, the NAT gateway will send a notification on the
|
|
// multicast group 224.0.0.1 port 5351 with the format of a public address response.
|
|
//
|
|
// Public address response:
|
|
//
|
|
// 0 1 2 3
|
|
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
// | Vers = 0 | OP = 128 + 0 | Result Code |
|
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
// | Seconds Since Start of Epoch |
|
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
// | Public IPv4 Address (a.b.c.d) |
|
|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
|
|
DebugLog.WriteLine("NAT-PMP: udpClient_DidReceive");
|
|
|
|
try
|
|
{
|
|
IPEndPoint ep = new IPEndPoint(IPAddress.Parse("224.0.0.1"), 5351);
|
|
byte[] data = udpClient.EndReceive(ar, ref ep);
|
|
|
|
if (data.Length == 12)
|
|
{
|
|
byte[] rawIP = new byte[4];
|
|
Buffer.BlockCopy(data, 8, rawIP, 0, 4);
|
|
|
|
IPAddress newIP = new IPAddress(rawIP);
|
|
|
|
OnDidReceiveBroadcastExternalIPChange(newIP, ep.Address);
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugLog.WriteLine("NAT-PMP: udpClient_DidReceive: Exception: {0}", e);
|
|
}
|
|
|
|
udpClient.BeginReceive(new AsyncCallback(udpClient_DidReceive), null);
|
|
}
|
|
}
|
|
}
|