642 lines
26 KiB
C#
642 lines
26 KiB
C#
using LobbyServerDto;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Lobbies
|
|
{
|
|
public class LobbyClient : IDisposable
|
|
{
|
|
TcpLobbbyClient tcpClient = new TcpLobbbyClient();
|
|
private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>();
|
|
BufferRental bufferRental = new BufferRental(4096);
|
|
|
|
AutoResetEvent waitForExternalIp = new AutoResetEvent(false);
|
|
|
|
UdpEchoServer udpEchoServer = new UdpEchoServer();
|
|
|
|
private ConcurrentDictionary<Guid, LobbyInfo> lobbyInformation = new ConcurrentDictionary<Guid, LobbyInfo>();
|
|
private string? host;
|
|
private int port;
|
|
private int connectionId;
|
|
|
|
public string? externalIp;
|
|
public int externalPort;
|
|
|
|
private CancellationTokenRegistration connectionCancellationRegistration;
|
|
|
|
public void Connect(string host, int port, CancellationToken cancellationToken)
|
|
{
|
|
this.host = host;
|
|
this.port = port;
|
|
|
|
try
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
connectionCancellationRegistration.Dispose();
|
|
connectionCancellationRegistration = cancellationToken.Register(() =>
|
|
{
|
|
tcpClient.Stop();
|
|
});
|
|
|
|
tcpClient.DataReceived -= TcpClient_DataReceived;
|
|
tcpClient.DataReceived += TcpClient_DataReceived;
|
|
|
|
tcpClient.Disconnected -= TcpClient_Disconnected;
|
|
tcpClient.Disconnected += TcpClient_Disconnected;
|
|
|
|
_ = Task.Run(() => tcpClient.Connect(host, port));
|
|
}
|
|
catch
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
public bool TryReadEvent(out LobbyClientEvent? result)
|
|
{
|
|
return events.TryDequeue(out result);
|
|
}
|
|
|
|
public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, int port)
|
|
{
|
|
udpEchoServer.Start(0);
|
|
_ = Task.Run(async () => {
|
|
byte[]? hash = null, salt = null;
|
|
|
|
if (!string.IsNullOrEmpty(password))
|
|
{
|
|
(hash, salt) = PasswordHash.Hash(password);
|
|
}
|
|
|
|
var lobbyCreate = new LobbyCreate()
|
|
{
|
|
GameId = gameId,
|
|
Name = name,
|
|
GameMode = gameMode,
|
|
MaxPlayerCount = maxPlayerCount,
|
|
PlayerCount = 0,
|
|
PasswordHash = hash,
|
|
PasswordSalt = salt,
|
|
HostIps = GatherLocalIpAddresses().ToArray(),
|
|
HostPort = port,
|
|
HostTryPort = udpEchoServer.Port
|
|
};
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
try
|
|
{
|
|
|
|
var len = lobbyCreate.Serialize(messageData);
|
|
await tcpClient.Send(messageData, 0, len);
|
|
}
|
|
finally
|
|
{
|
|
bufferRental.Return(messageData);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void RequestLobbyHostInfo(Guid lobbyId, string? password)
|
|
{
|
|
_ = Task.Run(async () => {
|
|
byte[]? passwordHash = null;
|
|
|
|
if (!string.IsNullOrEmpty(password) &&
|
|
lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
|
|
lobby.PasswordSalt != null)
|
|
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt!);
|
|
|
|
var lobbyCreate = new LobbyRequestHostInfo()
|
|
{
|
|
LobbyId = lobbyId,
|
|
PasswordHash = passwordHash,
|
|
};
|
|
|
|
var messageData = bufferRental.Rent();
|
|
try
|
|
{
|
|
var len = lobbyCreate.Serialize(messageData);
|
|
await tcpClient.Send(messageData, 0, len);
|
|
}
|
|
finally
|
|
{
|
|
bufferRental.Return(messageData);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// This callback is called if the nat punch requires the game server/client udp sockets sends a packet to a server so that
|
|
/// the external mapped port by the firewall can be seen by the external point.
|
|
/// </summary>
|
|
/// <param name="remoteEndpoint">The endpoint to send data to</param>
|
|
/// <param name="messageBuffer">The message byte buffer to send</param>
|
|
/// <param name="messageLength">Length of the data to send</param>
|
|
public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength);
|
|
|
|
/// <summary>
|
|
/// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by
|
|
/// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port.
|
|
/// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server.
|
|
/// The client receives the external port and ip seen by the lobby server for the host and can connect to the host.
|
|
/// </summary>
|
|
/// <param name="lobbyId">The lobby to request a nat punch for</param>
|
|
/// <param name="password">Optional password of lobby</param>
|
|
/// <param name="sendUdpToGetExternalPortMappingCallback">A callback to send udp data if you have a udp game client ready and bound</param>
|
|
public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpToGetExternalPortMappingCallback)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
byte[]? passwordHash = null;
|
|
if (!string.IsNullOrEmpty(password) &&
|
|
lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
|
|
lobby.PasswordSalt != null)
|
|
{
|
|
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt);
|
|
}
|
|
|
|
if (!QueryExternalIpAndPort(sendUdpToGetExternalPortMappingCallback, out var ip, out var mappedPort))
|
|
{
|
|
events.Enqueue(new LobbyClientEvent
|
|
{
|
|
EventType = LobbyClientEventTypes.Failed,
|
|
EventData = new LobbyClientDisconnectReason
|
|
{
|
|
WasError = true,
|
|
ErrorMessage = "Could not determine external IP/port."
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
|
|
{
|
|
LobbyId = lobbyId,
|
|
PasswordHash = passwordHash,
|
|
ClientIp = ip,
|
|
ClientPort = mappedPort
|
|
};
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbyRequestNatPunch.Serialize(messageData);
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await tcpClient.Send(messageData, 0, len);
|
|
}
|
|
finally
|
|
{
|
|
bufferRental.Return(messageData);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by
|
|
/// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port.
|
|
/// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server.
|
|
/// The client receives the external port and ip seen by the lobby server for the host and can connect to the host.
|
|
/// </summary>
|
|
/// <param name="lobbyId">The lobby to request a nat punch for</param>
|
|
/// <param name="password">Optional password of lobby</param>
|
|
/// <param name="port">The port the game client will later use. We create a udp socket on it and send a packet to the lobby server to get the firewall to map that port for a short period of time
|
|
/// after that the udp client will be disposed.</param>
|
|
public int RequestLobbyNatPunch(Guid lobbyId, string? password, int port = 0)
|
|
{
|
|
if(port < 0 || port > 65535)
|
|
throw new ArgumentOutOfRangeException(nameof(port));
|
|
|
|
Task.Run(() =>
|
|
{
|
|
var udpEchoServer = new UdpEchoServer();
|
|
|
|
try
|
|
{
|
|
udpEchoServer.Start(port);
|
|
|
|
byte[]? passwordHash = null;
|
|
if (!string.IsNullOrEmpty(password) &&
|
|
lobbyInformation.TryGetValue(lobbyId, out var lobby) &&
|
|
lobby.PasswordSalt != null)
|
|
{
|
|
passwordHash = PasswordHash.Hash(password, lobby.PasswordSalt);
|
|
}
|
|
|
|
if (!QueryExternalIpAndPort(udpEchoServer.Send, out var ip, out var mappedPort))
|
|
{
|
|
events.Enqueue(new LobbyClientEvent
|
|
{
|
|
EventType = LobbyClientEventTypes.Failed,
|
|
EventData = new LobbyClientDisconnectReason
|
|
{
|
|
WasError = true,
|
|
ErrorMessage = "Could not determine external IP/port."
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
|
|
{
|
|
LobbyId = lobbyId,
|
|
PasswordHash = passwordHash,
|
|
ClientIp = ip,
|
|
ClientPort = mappedPort
|
|
};
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbyRequestNatPunch.Serialize(messageData);
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await tcpClient.Send(messageData, 0, len);
|
|
}
|
|
finally
|
|
{
|
|
bufferRental.Return(messageData);
|
|
}
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
udpEchoServer.Dispose();
|
|
}
|
|
});
|
|
|
|
return udpEchoServer.Port;
|
|
}
|
|
|
|
public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, int port)
|
|
{
|
|
byte[]? hash = null, salt = null;
|
|
|
|
if (!string.IsNullOrEmpty(password))
|
|
{
|
|
(hash, salt) = PasswordHash.Hash(password);
|
|
}
|
|
|
|
var lobbyUpdate = new LobbyUpdate()
|
|
{
|
|
Name = name,
|
|
GameMode = gameMode,
|
|
MaxPlayerCount = maxPlayerCount,
|
|
PlayerCount = playerCount,
|
|
PasswordHash = hash,
|
|
PasswordSalt = salt,
|
|
HostIps = GatherLocalIpAddresses().ToArray(),
|
|
HostPort = port,
|
|
HostTryPort = udpEchoServer.Port
|
|
};
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbyUpdate.Serialize(messageData);
|
|
_ = Task.Run(async () => {
|
|
try { await tcpClient.Send(messageData, 0, len); }
|
|
finally { bufferRental.Return(messageData); }
|
|
});
|
|
|
|
}
|
|
|
|
public void CloseLobby()
|
|
{
|
|
udpEchoServer.Stop();
|
|
|
|
var lobbyDelete = new LobbyDelete()
|
|
{
|
|
|
|
};
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbyDelete.Serialize(messageData);
|
|
_ = Task.Run(async () => {
|
|
try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); }
|
|
});
|
|
}
|
|
|
|
public void ObserveLobbies(Guid gameId)
|
|
{
|
|
var lobbiesObserve = new LobbiesObserve() { GameId = gameId };
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbiesObserve.Serialize(messageData);
|
|
_ = Task.Run(async () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
|
|
}
|
|
|
|
public void StopObservingLobbies()
|
|
{
|
|
var lobbiesStopObserve = new LobbiesStopObserve() { };
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbiesStopObserve.Serialize(messageData);
|
|
_ = Task.Run(async () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
|
|
}
|
|
|
|
public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort)
|
|
{
|
|
var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId, ExternalIp = externalIp, ExternalPort = externalPort };
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
var len = lobbyNatPunchDone.Serialize(messageData);
|
|
_ = Task.Run(async () => { try { await tcpClient.Send(messageData, 0, len); } finally { bufferRental.Return(messageData); } });
|
|
}
|
|
|
|
public Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort)
|
|
{
|
|
return Task.Run(() =>
|
|
{
|
|
string? error = string.Empty;
|
|
IPAddress? ret = null;
|
|
try
|
|
{
|
|
using (var waitForIpEvent = new AutoResetEvent(false))
|
|
using (var udpEchoClient = new UdpEchoServer())
|
|
{
|
|
udpEchoClient.Reached += (ep) =>
|
|
{
|
|
ret = ep.Address;
|
|
try
|
|
{
|
|
waitForIpEvent.Set();
|
|
udpEchoClient.Stop();
|
|
}
|
|
catch { }
|
|
};
|
|
|
|
udpEchoClient.Start(0);
|
|
|
|
foreach (var ip in ipAddressesToTry)
|
|
{
|
|
udpEchoClient.CheckConnectionPossible(new IPEndPoint(ip, tryPort));
|
|
}
|
|
|
|
if(!waitForIpEvent.WaitOne(500))
|
|
error = $"Timeout";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Exception: {ex}";
|
|
}
|
|
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.DirectConnectionTestComplete, EventData = new DirectConnectionTestResult { DirectConnectionPossible = ret != null, IPAddress = ret, ErrorMessage = error } });
|
|
});
|
|
}
|
|
|
|
public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted)
|
|
{
|
|
// Check if the hostname is already an IPAddress
|
|
IPAddress? outIpAddress;
|
|
if (IPAddress.TryParse(hostName, out outIpAddress) == true)
|
|
return new IPAddress[] { outIpAddress };
|
|
//<----------
|
|
|
|
IPAddress[] addresslist = Dns.GetHostAddresses(hostName);
|
|
|
|
if (addresslist == null || addresslist.Length == 0)
|
|
return new IPAddress[0];
|
|
//<----------
|
|
|
|
if (ip4Wanted && ip6Wanted)
|
|
return addresslist;
|
|
//<----------
|
|
|
|
if (ip4Wanted)
|
|
return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetwork).ToArray();
|
|
//<----------
|
|
|
|
if (ip6Wanted)
|
|
return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetworkV6).ToArray();
|
|
//<----------
|
|
|
|
return new IPAddress[0];
|
|
}
|
|
|
|
public bool QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback, out string? ip, out int port)
|
|
{
|
|
ip = null;
|
|
port = 0;
|
|
|
|
byte[] messageData = bufferRental.Rent();
|
|
|
|
try
|
|
{
|
|
if (sendUdpCallback == null)
|
|
throw new ArgumentNullException(nameof(sendUdpCallback));
|
|
|
|
if (string.IsNullOrWhiteSpace(host))
|
|
return false;
|
|
|
|
if (connectionId <= 0)
|
|
return false;
|
|
|
|
var ips = GetIPsByName(host, true, false);
|
|
if (ips.Length == 0)
|
|
return false;
|
|
|
|
externalIp = null;
|
|
externalPort = 0;
|
|
waitForExternalIp.Reset();
|
|
|
|
var queryExternalPortAndIp = new QueryExternalPortAndIp
|
|
{
|
|
LobbyClientId = connectionId
|
|
};
|
|
|
|
int len = queryExternalPortAndIp.Serialize(messageData);
|
|
var remoteEndpoint = new IPEndPoint(ips[0], this.port);
|
|
|
|
for (int tries = 0; tries < 100; tries++)
|
|
{
|
|
sendUdpCallback(remoteEndpoint, messageData, len);
|
|
|
|
if (waitForExternalIp.WaitOne(100))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(externalIp) && externalPort > 0)
|
|
{
|
|
ip = externalIp;
|
|
port = externalPort;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
bufferRental.Return(messageData);
|
|
}
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
waitForExternalIp.Set();
|
|
tcpClient.Stop();
|
|
}
|
|
|
|
private void TcpClient_Disconnected(bool clean, string error)
|
|
{
|
|
if(!clean)
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Failed, EventData = new LobbyClientDisconnectReason { WasError = true, ErrorMessage = error } });
|
|
else
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } });
|
|
}
|
|
|
|
private void TcpClient_DataReceived(int dataLength, Memory<byte> data)
|
|
{
|
|
try
|
|
{
|
|
if (dataLength > 0)
|
|
{
|
|
switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span))
|
|
{
|
|
case LobbyNotFound.TypeId:
|
|
{
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyJoinFailed, EventData = new LobbyJoinFailReason { Reason = LobbyJoinFailReason.FailReasons.LobbyNotFound, ErrorMessage = string.Empty } });
|
|
}
|
|
break;
|
|
case LobbyWrongPassword.TypeId:
|
|
{
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyJoinFailed, EventData = new LobbyJoinFailReason { Reason = LobbyJoinFailReason.FailReasons.WrongPassword, ErrorMessage = string.Empty } });
|
|
}
|
|
break;
|
|
case LobbyClientConnectionInfo.TypeId:
|
|
{
|
|
var lobbyClientConnectionInfo = LobbyClientConnectionInfo.Deserialize(data.Span);
|
|
if (lobbyClientConnectionInfo != null)
|
|
{
|
|
connectionId = lobbyClientConnectionInfo.Id;
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null });
|
|
}
|
|
}
|
|
break;
|
|
case LobbyInfo.TypeId:
|
|
{
|
|
var lobbyInfo = LobbyInfo.Deserialize(data.Span);
|
|
if (lobbyInfo != null)
|
|
{
|
|
lobbyInformation[lobbyInfo.Id] = lobbyInfo;
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyUpdate, EventData = lobbyInfo });
|
|
}
|
|
}
|
|
break;
|
|
case LobbyDelete.TypeId:
|
|
{
|
|
var lobbyDelete = LobbyDelete.Deserialize(data.Span);
|
|
if (lobbyDelete != null)
|
|
{
|
|
lobbyInformation.TryRemove(lobbyDelete.Id, out var _);
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyDelete, EventData = lobbyDelete });
|
|
}
|
|
}
|
|
break;
|
|
case LobbyHostInfo.TypeId:
|
|
{
|
|
var lobbyHostInfo = LobbyHostInfo.Deserialize(data.Span);
|
|
|
|
if (lobbyHostInfo != null)
|
|
{
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyHostInfo, EventData = lobbyHostInfo });
|
|
}
|
|
}
|
|
break;
|
|
case LobbyRequestNatPunch.TypeId:
|
|
{
|
|
var lobbyRequestNatPunch = LobbyRequestNatPunch.Deserialize(data.Span);
|
|
if (lobbyRequestNatPunch != null)
|
|
{
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyRequestNatPunch, EventData = lobbyRequestNatPunch });
|
|
}
|
|
}
|
|
break;
|
|
case LobbyNatPunchDone.TypeId:
|
|
{
|
|
var lobbyNatPunchDone = LobbyNatPunchDone.Deserialize(data.Span);
|
|
if (lobbyNatPunchDone != null)
|
|
{
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyNatPunchDone, EventData = lobbyNatPunchDone });
|
|
}
|
|
}
|
|
break;
|
|
case SeenExternalIpAndPort.TypeId:
|
|
{
|
|
var seenExternalIpAndPort = SeenExternalIpAndPort.Deserialize(data.Span);
|
|
if (seenExternalIpAndPort != null)
|
|
{
|
|
externalIp = seenExternalIpAndPort.Ip;
|
|
externalPort = seenExternalIpAndPort.Port;
|
|
waitForExternalIp.Set();
|
|
|
|
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.ExternalIpAndPort, EventData = seenExternalIpAndPort });
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public IEnumerable<IPAddress> GatherLocalIpAddresses()
|
|
{
|
|
foreach (var netInterface in NetworkInterface.GetAllNetworkInterfaces())
|
|
{
|
|
if (netInterface.OperationalStatus != OperationalStatus.Up)
|
|
continue;
|
|
|
|
if (netInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
|
continue;
|
|
|
|
// Optional: virtuelle Interfaces rausfiltern (je nach Bedarf)
|
|
if (netInterface.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) ||
|
|
netInterface.Description.Contains("VMware", StringComparison.OrdinalIgnoreCase) ||
|
|
netInterface.Description.Contains("Docker", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
var ipProps = netInterface.GetIPProperties();
|
|
|
|
foreach (var addr in ipProps.UnicastAddresses)
|
|
{
|
|
var ip = addr.Address;
|
|
|
|
// Nur IPv4
|
|
if (ip.AddressFamily != AddressFamily.InterNetwork)
|
|
continue;
|
|
|
|
// Loopback nochmal sicherheitshalber
|
|
if (IPAddress.IsLoopback(ip))
|
|
continue;
|
|
|
|
// Link-local (APIPA) raus (169.254.x.x)
|
|
if (ip.GetAddressBytes()[0] == 169 && ip.GetAddressBytes()[1] == 254)
|
|
continue;
|
|
|
|
yield return ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
connectionCancellationRegistration.Dispose();
|
|
waitForExternalIp.Dispose();
|
|
tcpClient.Dispose();
|
|
udpEchoServer.Dispose();
|
|
}
|
|
|
|
}
|
|
}
|