// // PcapNostaleManager.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; using System.Diagnostics; using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PacketDotNet; using SharpPcap; using SharpPcap.LibPcap; namespace NosSmooth.Pcap; /// /// Captures packets, distributes them to Pcap clients. /// public class PcapNostaleManager { private readonly ILogger _logger; private readonly PcapNostaleOptions _options; private readonly ConcurrentDictionary _connections; private readonly ConcurrentDictionary> _clients; private Task? _deletionTask; private CancellationTokenSource? _deletionTaskCancellationSource; private int _clientsCount; private bool _started; /// /// Initializes a new instance of the class. /// /// The logger. /// The options. public PcapNostaleManager(ILogger logger, IOptions options) { _logger = logger; _options = options.Value; _connections = new ConcurrentDictionary(); _clients = new ConcurrentDictionary>(); } /// /// Add a pcap client. /// internal void AddClient() { var count = Interlocked.Increment(ref _clientsCount); if (count == 1) { StartCapturing(); } } /// /// Remove a pcap client. /// /// /// When no clients are left, packet capture will be stopped. /// internal void RemoveClient() { var count = Interlocked.Decrement(ref _clientsCount); if (count == 0) { Stop(); } } /// /// Associate the given connection with the given client. /// /// The connection to associate. /// The client to associate the connection with. internal void RegisterConnection(TcpConnection connection, PcapNostaleClient client) { _clients.AddOrUpdate ( connection, _ => { var clients = new List(); clients.Add(client); return clients; }, (_, currentClients) => { var clients = new List(currentClients); clients.Add(client); return clients; } ); if (_connections.TryGetValue(connection, out var data)) { foreach (var sniffedPacket in data.SniffedData) { client.OnPacketArrival(null, connection, sniffedPacket); } } } /// /// Disassociate the given connection. /// /// The connection to disassociate. /// The client to unregister. internal void UnregisterConnection(TcpConnection connection, PcapNostaleClient client) { _clients.AddOrUpdate ( connection, (c) => new List(), (c1, c2) => { var clients = new List(c2); clients.Remove(client); return clients; } ); } private void Stop() { if (!_started) { return; } _started = false; foreach (var device in LibPcapLiveDeviceList.Instance) { device.StopCapture(); } var task = _deletionTask; _deletionTask = null; _deletionTaskCancellationSource?.Cancel(); _deletionTaskCancellationSource?.Dispose(); _deletionTaskCancellationSource = null; task?.GetAwaiter().GetResult(); task?.Dispose(); _connections.Clear(); _clients.Clear(); } /// /// Start capturing packets from all devices. /// public void StartCapturing() { if (_started) { return; } _started = true; _deletionTaskCancellationSource = new CancellationTokenSource(); _deletionTask = Task.Run(() => DeletionTask(_deletionTaskCancellationSource.Token)); foreach (var device in LibPcapLiveDeviceList.Instance) { if (!device.Opened) { device.Open(); } device.Filter = "ip and tcp"; device.OnPacketArrival += DeviceOnOnPacketArrival; device.StartCapture(); } } private void DeviceOnOnPacketArrival(object sender, PacketCapture e) { try { DeviceOnPacketArrivalInner(e); } catch (Exception ex) { _logger.LogError(ex, "OnPacketArrival has produced an exception"); } } private void DeviceOnPacketArrivalInner(PacketCapture e) { var rawPacket = e.GetPacket(); var packet = PacketDotNet.Packet.ParsePacket(rawPacket.LinkLayerType, rawPacket.Data); var tcpPacket = packet.Extract(); if (tcpPacket is null) { return; } if (!tcpPacket.HasPayloadData || tcpPacket.PayloadData.Length == 0) { return; } var ipPacket = (PacketDotNet.IPPacket)tcpPacket.ParentPacket; System.Net.IPAddress srcIp = ipPacket.SourceAddress; System.Net.IPAddress dstIp = ipPacket.DestinationAddress; int srcPort = tcpPacket.SourcePort; int dstPort = tcpPacket.DestinationPort; var tcpConnection = new TcpConnection(srcIp.Address, srcPort, dstIp.Address, dstPort); if (!_connections.ContainsKey(tcpConnection)) { _connections.TryAdd ( tcpConnection, new ConnectionData ( srcIp, srcPort, dstIp, dstPort, new List(), DateTimeOffset.Now ) ); } var data = _connections[tcpConnection]; data.LastReceivedAt = DateTimeOffset.Now; if (data.SniffedData.Count < 5 && tcpPacket.PayloadData.Length < 500 && data.FirstObservedAt.AddMilliseconds(_options.CleanSniffedDataInterval) > DateTimeOffset.Now) { data.SniffedData.Add(tcpPacket.PayloadData); } if (_clients.TryGetValue(tcpConnection, out var clients)) { foreach (var client in clients) { client.OnPacketArrival((LibPcapLiveDevice)e.Device, tcpConnection, tcpPacket.PayloadData); } } } private async Task DeletionTask(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { DeleteData(); await Task.Delay(TimeSpan.FromMilliseconds(_options.CleanSniffedDataInterval * 3), ct); } catch (OperationCanceledException) { // ignored } catch (Exception e) { _logger.LogError(e, "The pcap manager deletion task has thrown an exception"); } } } private void DeleteData() { foreach (var connectionData in _connections) { if (connectionData.Value.FirstObservedAt.AddMilliseconds (_options.ForgetConnectionInterval) < DateTimeOffset.Now) { _connections.TryRemove(connectionData); } if (connectionData.Value.SniffedData.Count > 0 && connectionData.Value.LastReceivedAt.AddMilliseconds (_options.CleanSniffedDataInterval) < DateTimeOffset.Now) { connectionData.Value.SniffedData.Clear(); } } } }