diff --git a/.gitignore b/.gitignore index c1c8edd..e755bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -230,3 +230,6 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk + +# Rider +.idea/ diff --git a/README.md b/README.md index 1ca361f..c0742a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Snifter ======= +[![NuGet](https://img.shields.io/nuget/v/Snifter.svg)](https://www.nuget.org/packages/Snifter) +
@@ -14,8 +16,8 @@ Snifter -

Snifter is a raw socket IP packet capturing tool for Windows and Linux, with a tiny CPU and memory footprint.

-

Output is written in PCAPNG format, and you can filter captured packets based on protocol, source/destination address and source/destination port.

+

Snifter is a raw socket IP packet capturing library/app for Windows and Linux, with a tiny CPU and memory footprint.

+

Output can be written to PCAPNG files, and you can filter captured packets based on protocol, source/destination address and source/destination port.

@@ -25,14 +27,24 @@ Why? On Windows, you can't capture on the local loopback address `127.0.0.1` with a packet capture driver like [WinPcap](https://wiki.wireshark.org/WinPcap) - but you can by using a *raw socket* sniffer, like Snifter. -Additionally, Snifter is a cross-platform, portable tool that doesn't require any drivers to be installed. +Additionally, Snifter is a cross-platform, portable library/tool that doesn't require any drivers to be installed. + +Snifter started life only for Windows, and Linux support was later added thanks to .NET Core. + +Getting Started +--------------- +Install the [Snifter](https://www.nuget.org/packages/Snifter) package from NuGet: + +```powershell +Install-Package Snifter +``` -Snifter started life as a Windows-only tool, and Linux support was later added just because .NET Core makes it possible. +You can see an example of how to use the library in the `Snifter.App` code in `src/App`, including capturing, parsing, filtering and saving packets. -Limitations ------------ +App Limitations +--------------- -You must run Snifter with elevated privileges on Windows, or with `sudo` on Linux - this is an OS-level requirement to create raw sockets. +You must run `Snifter.App` with elevated privileges on Windows, or with `sudo` on Linux - this is an OS-level requirement to create raw sockets. For now at least, Snifter only supports IPv4. It should be straightforward to add support for IPv6, but I don't use IPv6 yet, so haven't added it. diff --git a/Snifter.sln b/Snifter.sln index 82b1e4c..c687c02 100644 --- a/Snifter.sln +++ b/Snifter.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2010 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snifter", "src\Snifter.csproj", "{BD011029-7C87-495C-A135-BDA549DB03CF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snifter", "src\Snifter\Snifter.csproj", "{BD011029-7C87-495C-A135-BDA549DB03CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{E76A7FDE-F7FA-49BC-AC9C-1B1ADEDBC35A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {BD011029-7C87-495C-A135-BDA549DB03CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {BD011029-7C87-495C-A135-BDA549DB03CF}.Release|Any CPU.ActiveCfg = Release|Any CPU {BD011029-7C87-495C-A135-BDA549DB03CF}.Release|Any CPU.Build.0 = Release|Any CPU + {E76A7FDE-F7FA-49BC-AC9C-1B1ADEDBC35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E76A7FDE-F7FA-49BC-AC9C-1B1ADEDBC35A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E76A7FDE-F7FA-49BC-AC9C-1B1ADEDBC35A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E76A7FDE-F7FA-49BC-AC9C-1B1ADEDBC35A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Snifter.sln.DotSettings b/Snifter.sln.DotSettings index ef1d1e3..57f3a1c 100644 --- a/Snifter.sln.DotSettings +++ b/Snifter.sln.DotSettings @@ -62,4 +62,7 @@ LIVE_MONITOR DO_NOTHING LIVE_MONITOR - True \ No newline at end of file + True + True + True + True \ No newline at end of file diff --git a/build.cmd b/build.cmd index f2e60f9..14d3639 100644 --- a/build.cmd +++ b/build.cmd @@ -1,2 +1,6 @@ dotnet restore .\Snifter.sln -dotnet build .\src\Snifter.csproj --configuration Release --framework net471 + +dotnet build .\src\App\App.csproj --configuration Release +dotnet build .\src\Snifter\Snifter.csproj --configuration Release + +dotnet pack .\src\Snifter -c Release diff --git a/build.sh b/build.sh index b1a185c..4e51171 100644 --- a/build.sh +++ b/build.sh @@ -2,4 +2,5 @@ set -ev dotnet restore ./Snifter.sln -dotnet build ./src/Snifter.csproj --configuration Release --framework netcoreapp2.1 +dotnet build ./src/App/App.csproj --configuration Release --framework netcoreapp3.1 +dotnet build ./src/Snifter/Snifter.csproj --configuration Release --framework netcoreapp3.1 diff --git a/src/App/App.csproj b/src/App/App.csproj new file mode 100644 index 0000000..2705485 --- /dev/null +++ b/src/App/App.csproj @@ -0,0 +1,37 @@ + + + + Exe + net471;netcoreapp3.1 + + Snifter.App + Snifter.App + Raw Socket Sniffer + 1.3.0 + latest + false + Colin Anderson + Copyright © Colin Anderson 2020 + http://github.com/cocowalla/snifter + git + false + + + + + + + + + + + + + + + + + + + + diff --git a/src/AppOptions.cs b/src/App/AppOptions.cs similarity index 83% rename from src/AppOptions.cs rename to src/App/AppOptions.cs index 2b83fd6..03d43c9 100644 --- a/src/AppOptions.cs +++ b/src/App/AppOptions.cs @@ -2,8 +2,10 @@ using System.IO; using System.Net; using Snifter.Filter; +using Snifter.Protocol.Internet; +using Snifter.Protocol.Transport; -namespace Snifter +namespace Snifter.App { public class AppOptions { @@ -50,33 +52,33 @@ public void Parse(string[] args) this.optionSet.Parse(args); } - public Filters BuildFilters() + public Filters BuildFilters() { - var filters = new Filters(this.FilterOperator); + var filters = new Filters(this.FilterOperator); if (this.FilterProtocol.HasValue) { - filters.PropertyFilters.Add(new PropertyFilter(x => x.Protocol, this.FilterProtocol.Value)); + filters.PropertyFilters.Add(new PropertyFilter(x => x.Protocol, this.FilterProtocol.Value)); } if (this.FilterSourceAddress != null) { - filters.PropertyFilters.Add(new PropertyFilter(x => x.SourceAddress, this.FilterSourceAddress)); + filters.PropertyFilters.Add(new PropertyFilter(x => x.SourceAddress, this.FilterSourceAddress)); } if (this.FilterDestAddress != null) { - filters.PropertyFilters.Add(new PropertyFilter(x => x.DestAddress, this.FilterDestAddress)); + filters.PropertyFilters.Add(new PropertyFilter(x => x.DestinationAddress, this.FilterDestAddress)); } if (this.FilterSourcePort.HasValue) { - filters.PropertyFilters.Add(new PropertyFilter(x => x.SourcePort, this.FilterSourcePort.Value)); + filters.PropertyFilters.Add(new PropertyFilter(x => x.TransportPacket is IHasPorts hasPorts ? (ushort?)hasPorts.SourcePort : null, this.FilterSourcePort.Value)); } if (this.FilterDestPort.HasValue) { - filters.PropertyFilters.Add(new PropertyFilter(x => x.DestPort, this.FilterDestPort.Value)); + filters.PropertyFilters.Add(new PropertyFilter(x => x.TransportPacket is IHasPorts hasPorts ? (ushort?)hasPorts.DestinationPort : null, this.FilterDestPort.Value)); } return filters; diff --git a/src/Options.cs b/src/App/Options.cs similarity index 99% rename from src/Options.cs rename to src/App/Options.cs index 36c5103..42ac314 100644 --- a/src/Options.cs +++ b/src/App/Options.cs @@ -127,7 +127,7 @@ using System.Text; using System.Text.RegularExpressions; -namespace Snifter +namespace Snifter.App { public class OptionValueCollection : IList, IList { diff --git a/src/Program.cs b/src/App/Program.cs similarity index 84% rename from src/Program.cs rename to src/App/Program.cs index 693cdd6..ab42979 100644 --- a/src/Program.cs +++ b/src/App/Program.cs @@ -1,18 +1,17 @@ using System; -using System.Security.Principal; using System.Threading; -using Snifter.Outputs.PcapNg; +using Snifter.Output.PcapNg; -namespace Snifter +namespace Snifter.App { - internal class Program + internal static class Program { private static bool isStopping; private static void Main(string[] args) { // You can only create raw sockets with elevated privileges - if (!SystemInformation.IsAdmin()) + if (!UserInformation.IsAdmin()) { var message = SystemInformation.IsWindows ? "Please run with elevated prilileges" @@ -42,13 +41,16 @@ private static void Main(string[] args) } var filters = appOptions.BuildFilters(); - var nic = nics[appOptions.InterfaceId.Value]; + var nic = nics[appOptions.InterfaceId!.Value]; // Start capturing packets var output = new PcapNgFileOutput(nic, appOptions.Filename); var sniffer = new SocketSniffer(nic, filters, output); sniffer.Start(); + // Shutdown gracefully on CTRL+C + Console.CancelKeyPress += ConsoleOnCancelKeyPress; + Console.WriteLine(); Console.WriteLine("Capturing on interface {0} ({1})", nic.Name, nic.IPAddress); Console.WriteLine("Saving to file {0}", appOptions.Filename); @@ -56,19 +58,20 @@ private static void Main(string[] args) Console.WriteLine(); Console.WriteLine(); Console.WriteLine(); - - // Shutdown gracefully on CTRL+C - Console.CancelKeyPress += ConsoleOnCancelKeyPress; - + Console.WriteLine(); + Console.WriteLine(); + while (!isStopping) { - Console.SetCursorPosition(0, Console.CursorTop - 2); - Console.WriteLine("Packets Observed: {0}", sniffer.PacketsObserved); - Console.WriteLine("Packets Captured: {0}", sniffer.PacketsCaptured); + Console.SetCursorPosition(0, Console.CursorTop - 4); + Console.WriteLine("Packets Observed: {0} ", sniffer.Statistics.PacketsObserved); + Console.WriteLine("Packets Captured: {0} ", sniffer.Statistics.PacketsCaptured); + Console.WriteLine("Packets Dropped: {0} ", sniffer.Statistics.PacketsDropped); + Console.WriteLine("Buffers in Use: {0} ", sniffer.Statistics.BuffersInUse); Thread.Sleep(200); } - + sniffer.Stop(); } diff --git a/src/App/UserInformation.cs b/src/App/UserInformation.cs new file mode 100644 index 0000000..3c4cca2 --- /dev/null +++ b/src/App/UserInformation.cs @@ -0,0 +1,19 @@ +using System.Security.Principal; +using Mono.Unix.Native; + +namespace Snifter.App +{ + public static class UserInformation + { + public static bool IsAdmin() + { + if (SystemInformation.IsWindows) + { + return new WindowsPrincipal(WindowsIdentity.GetCurrent()) + .IsInRole(WindowsBuiltInRole.Administrator); + } + + return Syscall.geteuid() == 0; + } + } +} diff --git a/src/BufferManager.cs b/src/BufferManager.cs deleted file mode 100644 index 5d9c4fc..0000000 --- a/src/BufferManager.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Net.Sockets; - -namespace Snifter -{ - /// - /// Fixed pool of buffers for use by async socket connections, to avoid connections creating their - /// own buffers (which leads to a lot of pinned buffers, causing heap fragmentation) - /// - public class BufferManager - { - private readonly byte[] buffer; - private readonly int segmentSize; - private int nextOffset; - - public BufferManager(int segmentSize, int numSegments) - { - this.segmentSize = segmentSize; - this.buffer = new byte[segmentSize * numSegments]; - } - - public void AssignSegment(SocketAsyncEventArgs e) - { - if (this.nextOffset + this.segmentSize > this.buffer.Length) - throw new IndexOutOfRangeException("Buffer exhausted"); - - e.SetBuffer(this.buffer, this.nextOffset, this.segmentSize); - this.nextOffset += this.segmentSize; - } - } -} diff --git a/src/IPPacket.cs b/src/IPPacket.cs deleted file mode 100644 index 7a9ac8e..0000000 --- a/src/IPPacket.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; - -namespace Snifter -{ - // ReSharper disable once InconsistentNaming - public class IPPacket - { - public int Version { get; } - public int HeaderLength { get; } - public int Protocol { get; } - public IPAddress SourceAddress { get; } - public IPAddress DestAddress { get; } - public ushort SourcePort { get; } - public ushort DestPort { get; } - - public IPPacket(byte[] data) - { - var versionAndLength = data[0]; - this.Version = versionAndLength >> 4; - - // Only parse IPv4 packets for now - if (this.Version != 4) - return; - - this.HeaderLength = (versionAndLength & 0x0F) << 2; - - this.Protocol = Convert.ToInt32(data[9]); - this.SourceAddress = new IPAddress(BitConverter.ToUInt32(data, 12)); - this.DestAddress = new IPAddress(BitConverter.ToUInt32(data, 16)); - - if (Enum.IsDefined(typeof(ProtocolsWithPort), this.Protocol)) - { - // Ensure big-endian - this.SourcePort = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, this.HeaderLength)); - this.DestPort = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, this.HeaderLength + 2)); - } - } - } - - /// - /// Protocols that have a port abstraction - /// - internal enum ProtocolsWithPort - { - TCP = 6, - UDP = 17, - SCTP = 132 - } -} diff --git a/src/Outputs/PcapNg/BaseBlock.cs b/src/Outputs/PcapNg/BaseBlock.cs deleted file mode 100644 index af2a1d3..0000000 --- a/src/Outputs/PcapNg/BaseBlock.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.IO; -using System.Text; - -namespace Snifter.Outputs.PcapNg -{ - public abstract class BaseBlock : IBlock - { - protected static byte[] GetOptionBytes(int code, string value) - { - byte[] optionData; - - var optionCode = BitConverter.GetBytes(code); - var optionValue = Encoding.UTF8.GetBytes(value); - var optionValueLength = BitConverter.GetBytes(optionValue.Length); - PadToMultipleOf(ref optionValue, 4); - - using (var ms = new MemoryStream()) - using (var writer = new BinaryWriter(ms)) - { - writer.Write(optionCode, 0, 2); - writer.Write(optionValueLength, 0, 2); - writer.Write(optionValue); - - optionData = ms.ToArray(); - } - - return optionData; - } - - protected static byte[] GetOptionBytes(int code, byte[] value) - { - byte[] optionData; - - var optionCode = BitConverter.GetBytes(code); - var optionValueLength = BitConverter.GetBytes(value.Length); - var optionValue = PadToMultipleOf(value, 4); - - using (var ms = new MemoryStream()) - using (var writer = new BinaryWriter(ms)) - { - writer.Write(optionCode, 0, 2); - writer.Write(optionValueLength, 0, 2); - writer.Write(optionValue); - - optionData = ms.ToArray(); - } - - return optionData; - } - - protected static byte[] PadToMultipleOf(byte[] src, int pad) - { - int len = (src.Length + pad - 1) / pad * pad; - var padded = new byte[len]; - src.CopyTo(padded, 0); - - return padded; - } - - protected static void PadToMultipleOf(ref byte[] src, int pad) - { - int len = (src.Length + pad - 1) / pad * pad; - Array.Resize(ref src, len); - } - - public abstract byte[] GetBytes(); - } -} diff --git a/src/Outputs/PcapNg/EnhancedPacketBlock.cs b/src/Outputs/PcapNg/EnhancedPacketBlock.cs deleted file mode 100644 index 4436018..0000000 --- a/src/Outputs/PcapNg/EnhancedPacketBlock.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.IO; - -namespace Snifter.Outputs.PcapNg -{ - public class EnhancedPacketBlock : BaseBlock - { - private readonly TimestampedData timestampedData; - private static readonly byte[] BlockType = { 0x06, 0x00, 0x00, 0x00 }; - private static readonly byte[] InterfaceId = { 0x00, 0x00, 0x00, 0x00 }; - private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - public EnhancedPacketBlock(TimestampedData timestampedData) - { - this.timestampedData = timestampedData; - } - - public override byte[] GetBytes() - { - byte[] blockData; - - // Timestamp - var timestamp = (long)(this.timestampedData.Timestamp - Epoch).TotalMilliseconds; - var timestampHigh = (int)(timestamp >> 32); - var timestampLow = (int)timestamp; - - // Captured Length - var capturedLength = this.timestampedData.Data.Length; - - // Packet Length - var packetLength = this.timestampedData.Data.Length; - - // Packet Data - var packetData = PadToMultipleOf(this.timestampedData.Data, 4); - - // Block Length - var blockLength = 32 + packetData.Length; - - using (var ms = new MemoryStream()) - using (var writer = new BinaryWriter(ms)) - { - writer.Write(BlockType); - writer.Write(blockLength); - writer.Write(InterfaceId); - writer.Write(timestampHigh); - writer.Write(timestampLow); - writer.Write(capturedLength); - writer.Write(packetLength); - writer.Write(packetData); - writer.Write(blockLength); - - blockData = ms.ToArray(); - } - - return blockData; - } - } -} diff --git a/src/Outputs/PcapNg/IBlock.cs b/src/Outputs/PcapNg/IBlock.cs deleted file mode 100644 index fe333f9..0000000 --- a/src/Outputs/PcapNg/IBlock.cs +++ /dev/null @@ -1,8 +0,0 @@ - -namespace Snifter.Outputs.PcapNg -{ - public interface IBlock - { - byte[] GetBytes(); - } -} diff --git a/src/Outputs/PcapNg/InterfaceDescriptionBlock.cs b/src/Outputs/PcapNg/InterfaceDescriptionBlock.cs deleted file mode 100644 index d887f25..0000000 --- a/src/Outputs/PcapNg/InterfaceDescriptionBlock.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.IO; - -namespace Snifter.Outputs.PcapNg -{ - public class InterfaceDescriptionBlock : BaseBlock - { - private readonly NetworkInterfaceInfo nic; - private static readonly byte[] BlockType = { 0x01, 0x00, 0x00, 0x00 }; - - // SnapLen (65535 bytes) - private static readonly byte[] SnapLen = { 0xff, 0xff, 0x00, 0x00 }; - - // Link Layer Type (Raw IP: http://www.tcpdump.org/linktypes.html) - private static readonly byte[] LinkLayer = { 0x65, 0x00, 0x00, 0x00 }; - - // Options: Timestamp Resolution Name (10^-3s == milliseconds) - private static readonly byte[] TsResolution = { 0x03 }; - private static readonly byte[] TsResolutionOption = GetOptionBytes(9, TsResolution); - - // End of options - private static readonly byte[] EndOptionCode = { 0x00, 0x00 }; - private static readonly byte[] EndOptionLength = { 0x00, 0x00 }; - - public InterfaceDescriptionBlock(NetworkInterfaceInfo nic) - { - this.nic = nic; - } - - public override byte[] GetBytes() - { - byte[] blockData; - - // Options: Interface Name - var interfaceNameOption = GetOptionBytes(2, $"\\Device\\NPF_{this.nic.Id}"); - - // Block Length - var blockLength = 24 + interfaceNameOption.Length + TsResolutionOption.Length; - - using (var ms = new MemoryStream()) - { - using (var writer = new BinaryWriter(ms)) - { - writer.Write(BlockType); - writer.Write(blockLength); - writer.Write(LinkLayer); - writer.Write(SnapLen); - writer.Write(interfaceNameOption); - writer.Write(TsResolutionOption); - writer.Write(EndOptionCode); - writer.Write(EndOptionLength); - writer.Write(blockLength); - } - - blockData = ms.ToArray(); - } - - return blockData; - } - } -} diff --git a/src/Outputs/PcapNg/SectionHeaderBlock.cs b/src/Outputs/PcapNg/SectionHeaderBlock.cs deleted file mode 100644 index 9732ccd..0000000 --- a/src/Outputs/PcapNg/SectionHeaderBlock.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; - -namespace Snifter.Outputs.PcapNg -{ - public class SectionHeaderBlock : BaseBlock - { - private static readonly byte[] BlockType = { 0x0a, 0x0d, 0x0d, 0x0a }; - private static readonly byte[] ByteOrderMagic = { 0x4d, 0x3c, 0x2b, 0x1a }; - - // Version (makor version 1, minor version 0) - private static readonly byte[] Version = { 0x01, 0x00, 0x00, 0x00 }; - - // Section Length (unspecified) - private static readonly byte[] SectionLength = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; - - private static readonly byte[] OperatingSystem = GetOptionBytes(3, Environment.OSVersion.ToString()); - private static readonly byte[] Application = GetOptionBytes(4, "https://github.com/cocowalla/snifter"); - - // End of options - private static readonly byte[] EndOptionCode = { 0x00, 0x00 }; - private static readonly byte[] EndOptionLength = { 0x00, 0x00 }; - - public override byte[] GetBytes() - { - byte[] blockData; - - // Block Length - var blockLength = 32 + OperatingSystem.Length + Application.Length; - - using (var ms = new MemoryStream()) - { - using (var writer = new BinaryWriter(ms)) - { - writer.Write(BlockType); - writer.Write(blockLength); - writer.Write(ByteOrderMagic); - writer.Write(Version); - writer.Write(SectionLength); - writer.Write(OperatingSystem); - writer.Write(Application); - writer.Write(EndOptionCode); - writer.Write(EndOptionLength); - writer.Write(blockLength); - } - - blockData = ms.ToArray(); - } - - return blockData; - } - } -} diff --git a/src/Snifter.csproj b/src/Snifter.csproj deleted file mode 100644 index ba667c7..0000000 --- a/src/Snifter.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - Raw Socket Sniffer - 1.2.0 - false - Colin Anderson - Copyright © Colin Anderson 2015 - net471;netcoreapp2.1 - Exe - latest - Snifter - Snifter - http://github.com/cocowalla/snifter - git - false - - - - - - - - - - - - - - - - diff --git a/src/Filter/FilterOperator.cs b/src/Snifter/Filter/FilterOperator.cs similarity index 100% rename from src/Filter/FilterOperator.cs rename to src/Snifter/Filter/FilterOperator.cs diff --git a/src/Filter/Filters.cs b/src/Snifter/Filter/Filters.cs similarity index 81% rename from src/Filter/Filters.cs rename to src/Snifter/Filter/Filters.cs index 1f6f633..aea3d8e 100644 --- a/src/Filter/Filters.cs +++ b/src/Snifter/Filter/Filters.cs @@ -5,8 +5,8 @@ namespace Snifter.Filter { public class Filters { - public FilterOperator FilterOperator { get; set; } - public IList> PropertyFilters { get; set; } + public FilterOperator FilterOperator { get; } + public IList> PropertyFilters { get; } public Filters(FilterOperator filterFilterOperator) { diff --git a/src/Filter/PropertyFilter.cs b/src/Snifter/Filter/PropertyFilter.cs similarity index 100% rename from src/Filter/PropertyFilter.cs rename to src/Snifter/Filter/PropertyFilter.cs diff --git a/src/NetworkInterfaceInfo.cs b/src/Snifter/NetworkInterfaceInfo.cs similarity index 90% rename from src/NetworkInterfaceInfo.cs rename to src/Snifter/NetworkInterfaceInfo.cs index 1970d81..e778a8a 100644 --- a/src/NetworkInterfaceInfo.cs +++ b/src/Snifter/NetworkInterfaceInfo.cs @@ -20,8 +20,8 @@ public static IList GetInterfaces() foreach (var nic in nics) { - var ipAddresses = nic.GetIPProperties().UnicastAddresses.Where(x => - x.Address != null && x.Address.AddressFamily == AddressFamily.InterNetwork); + var ipAddresses = nic.GetIPProperties().UnicastAddresses + .Where(x => x.Address != null && x.Address.AddressFamily == AddressFamily.InterNetwork); foreach (var ipAddress in ipAddresses) { diff --git a/src/Outputs/IOutput.cs b/src/Snifter/Output/IOutput.cs similarity index 79% rename from src/Outputs/IOutput.cs rename to src/Snifter/Output/IOutput.cs index 17437fe..af94b93 100644 --- a/src/Outputs/IOutput.cs +++ b/src/Snifter/Output/IOutput.cs @@ -1,5 +1,5 @@  -namespace Snifter.Outputs +namespace Snifter.Output { public interface IOutput { diff --git a/src/Snifter/Output/PcapNg/EnhancedPacketBlock.cs b/src/Snifter/Output/PcapNg/EnhancedPacketBlock.cs new file mode 100644 index 0000000..7afb5da --- /dev/null +++ b/src/Snifter/Output/PcapNg/EnhancedPacketBlock.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Text; + +namespace Snifter.Output.PcapNg +{ + /// + /// An Enhanced Packet Block. This is the standard container for storing captured packets. + /// https://tools.ietf.org/html/draft-tuexen-opswg-pcapng-00#section-4.3 + /// + /// 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 + /// +---------------------------------------------------------------+ + /// 0 | Block Type = 0x00000006 | + /// +---------------------------------------------------------------+ + /// 4 | Block Total Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 8 | Interface ID | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 12 | Timestamp (High) | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 16 | Timestamp (Low) | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 20 | Captured Len | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 24 | Packet Len | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 28 / / + /// / Packet Data / + /// / variable length, aligned to 32 bits / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / Options (variable) / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Block Total Length | + /// +---------------------------------------------------------------+ + /// + public class EnhancedPacketBlock : IBlock + { + private readonly TimestampedData timestampedData; + + private static readonly byte[] BlockType = { 0x06, 0x00, 0x00, 0x00 }; + + // Fixed at zero, since we only capture from a single interface + private static readonly byte[] InterfaceId = { 0x00, 0x00, 0x00, 0x00 }; + + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + // Number of bytes that are fixed in every block + private const int FixedBlockSize = 32; + + public EnhancedPacketBlock(TimestampedData timestampedData) + { + this.timestampedData = timestampedData; + } + + public void WriteTo(BinaryWriter writer) + { + // Timestamp + var timestamp = (ulong)(this.timestampedData.Timestamp - Epoch).TotalMilliseconds; + var timestampHigh = (uint)(timestamp >> 32); + var timestampLow = (uint)timestamp; + + // Captured Length (number of bytes captured from the packet, unpadded - here the same as Packet Length) + var capturedLength = this.timestampedData.Data.Length; + + // Packet Length + var packetLength = this.timestampedData.Data.Length; + + // Packet Data (the block body, which must be written aligned to 32-bits) + var packetData = this.timestampedData.Data; + + // Block Total Length + var blockLength = FixedBlockSize + packetData.GetAlignedLength(); + + writer.Write(BlockType); + writer.Write(blockLength); + writer.Write(InterfaceId); + writer.Write(timestampHigh); + writer.Write(timestampLow); + writer.Write(capturedLength); + writer.Write(packetLength); + writer.WriteAligned(packetData); + writer.Write(blockLength); + } + + public byte[] GetBytes() + { + byte[] blockData; + + // Block Total Length + var blockLength = FixedBlockSize + this.timestampedData.Data.GetAlignedLength(); + + using (var ms = MemoryStreamPool.Get(blockLength)) + { + using (var writer = new BinaryWriter(ms, Encoding.UTF8, true)) + { + writer.Write(this); + } + + blockData = ms.ToArray(); + } + + return blockData; + } + } +} diff --git a/src/Snifter/Output/PcapNg/IBinaryWritable.cs b/src/Snifter/Output/PcapNg/IBinaryWritable.cs new file mode 100644 index 0000000..5590cf2 --- /dev/null +++ b/src/Snifter/Output/PcapNg/IBinaryWritable.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Snifter.Output.PcapNg +{ + public interface IBinaryWritable + { + void WriteTo(BinaryWriter writer); + } +} diff --git a/src/Snifter/Output/PcapNg/IBlock.cs b/src/Snifter/Output/PcapNg/IBlock.cs new file mode 100644 index 0000000..acbf3a5 --- /dev/null +++ b/src/Snifter/Output/PcapNg/IBlock.cs @@ -0,0 +1,8 @@ + +namespace Snifter.Output.PcapNg +{ + public interface IBlock : IBinaryWritable + { + byte[] GetBytes(); + } +} diff --git a/src/Snifter/Output/PcapNg/InterfaceDescriptionBlock.cs b/src/Snifter/Output/PcapNg/InterfaceDescriptionBlock.cs new file mode 100644 index 0000000..77a5a79 --- /dev/null +++ b/src/Snifter/Output/PcapNg/InterfaceDescriptionBlock.cs @@ -0,0 +1,101 @@ +using System.IO; +using System.Text; + +namespace Snifter.Output.PcapNg +{ + /// + /// Interface Description Block. This block specifies the characteristics of the network + /// interface on which the capture has been made. + /// https://tools.ietf.org/html/draft-tuexen-opswg-pcapng-00#section-4.2 + /// + /// 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 + /// +---------------------------------------------------------------+ + /// 0 | Block Type = 0x00000001 | + /// +---------------------------------------------------------------+ + /// 4 | Block Total Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 8 | LinkType | Reserved | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 12 | SnapLen | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 16 / / + /// / Options (variable) / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Block Total Length | + /// +---------------------------------------------------------------+ + /// + /// + public class InterfaceDescriptionBlock : IBlock + { + private static readonly byte[] BlockType = { 0x01, 0x00, 0x00, 0x00 }; + + // SnapLen (65535 bytes) + private static readonly byte[] SnapLen = { 0xff, 0xff, 0x00, 0x00 }; + + // Link Layer Type (Raw IP: http://www.tcpdump.org/linktypes.html) + private static readonly byte[] LinkType = { 0x65, 0x00, 0x00, 0x00 }; + + // Options: Timestamp Resolution Name (10^-3s == milliseconds) + private static readonly byte[] TsResolution = { 0x03 }; + private static readonly OptionalField TsResolutionOption = new OptionalField(OptionTypeCode.InterfaceTimestampResolution, TsResolution); + private readonly OptionalField interfaceNameOption; + private readonly OptionalField interfaceDescriptionOption; + + // Block Total Length + public int TotalBlockLength { get; } + + public InterfaceDescriptionBlock(NetworkInterfaceInfo nic) + { + // Options: Interface Name (if_name) + this.interfaceNameOption = new OptionalField(OptionTypeCode.InterfaceName, $"\\Device\\NPF_{nic.Id}"); + + // Options: Interface Description (if_description) + this.interfaceDescriptionOption = new OptionalField(OptionTypeCode.InterfaceDescription, nic.Name); + + this.TotalBlockLength = + BlockType.Length + + sizeof(int) + + LinkType.Length + + SnapLen.Length + + this.interfaceNameOption.Length + + this.interfaceDescriptionOption.Length + + TsResolutionOption.Length + + OptionalField.EndOfOptions.Length + + sizeof(int); + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write(BlockType); + writer.Write(this.TotalBlockLength); + writer.Write(LinkType); + writer.Write(SnapLen); + + writer.Write(this.interfaceNameOption); + writer.Write(this.interfaceDescriptionOption); + writer.Write(TsResolutionOption); + writer.Write(OptionalField.EndOfOptions); + + writer.Write(this.TotalBlockLength); + } + + public byte[] GetBytes() + { + byte[] blockData; + + using (var ms = MemoryStreamPool.Get(this.TotalBlockLength)) + { + using (var writer = new BinaryWriter(ms, Encoding.UTF8, true)) + { + writer.Write(this); + } + + blockData = ms.ToArray(); + } + + return blockData; + } + } +} diff --git a/src/Snifter/Output/PcapNg/OptionTypeCodes.cs b/src/Snifter/Output/PcapNg/OptionTypeCodes.cs new file mode 100644 index 0000000..afae337 --- /dev/null +++ b/src/Snifter/Output/PcapNg/OptionTypeCodes.cs @@ -0,0 +1,40 @@ + +namespace Snifter.Output.PcapNg +{ + public class OptionTypeCode + { + public short Value { get; } + + private OptionTypeCode(short value) + { + this.Value = value; + } + + private static OptionTypeCode WithValue(short value) + => new OptionTypeCode(value); + + // opt_endofopt + public static readonly OptionTypeCode EndOfOptions = WithValue(0); + + // opt_comment + public static readonly OptionTypeCode Comment = WithValue(1); + + // shb_hardware + public static readonly OptionTypeCode SectionHeaderHardware = WithValue(2); + + // shb_os + public static readonly OptionTypeCode SectionHeaderOperatingSystem = WithValue(3); + + // shb_userappl + public static readonly OptionTypeCode SectionHeaderUserApp = WithValue(2); + + // if_name + public static readonly OptionTypeCode InterfaceName = WithValue(2); + + // if_description + public static readonly OptionTypeCode InterfaceDescription = WithValue(3); + + // if_tsresol + public static readonly OptionTypeCode InterfaceTimestampResolution = WithValue(9); + } +} diff --git a/src/Snifter/Output/PcapNg/OptionalField.cs b/src/Snifter/Output/PcapNg/OptionalField.cs new file mode 100644 index 0000000..b020dc1 --- /dev/null +++ b/src/Snifter/Output/PcapNg/OptionalField.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Text; + +namespace Snifter.Output.PcapNg +{ + /// + /// Options Field + /// https://tools.ietf.org/html/draft-tuexen-opswg-pcapng-00#section-3.5 + /// + /// 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 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Option Code | Option Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / Option Value / + /// / variable length, aligned to 32 bits / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// / / + /// / . . . other options . . . / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Option Code == opt_endofopt | Option Length == 0 | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + public class OptionalField : IBinaryWritable + { + public static readonly OptionalField EndOfOptions = new OptionalField(OptionTypeCode.EndOfOptions, new byte[0]); + + public short Code { get; } + public byte[] Value { get; } + public int Length { get; } + + /// + /// Create a new Optional Field from a byte[] value + /// + /// Option type + /// The value, which will be padded to 32-bits if required + public OptionalField(OptionTypeCode typeCode, byte[] value) + { + this.Code = typeCode.Value; + this.Value = value; + + // 2 bytes for Option Code, 2 bytes for Option Value Length (without padding), variable length for Value Length (including any padding) + this.Length = sizeof(short) + sizeof(short) + this.Value.GetAlignedLength(); + } + + /// + /// Create a new Optional Field from a string value + /// + /// Option type + /// The value, which will be converted to a UTF8-encoded byte array and padded to 32-bits if required + public OptionalField(OptionTypeCode typeCode, string value) + : this(typeCode, Encoding.UTF8.GetBytes(value)) + { + // Do nothing + } + + public byte[] GetBytes() + { + using (var ms = MemoryStreamPool.Get()) + { + using (var writer = new BinaryWriter(ms, Encoding.UTF8, true)) + { + writer.Write(this); + } + + return ms.ToArray(); + } + } + + public void WriteTo(BinaryWriter writer) + { + // 2-byte Option Code + writer.Write(this.Code); + + // 2-byte Option Length + writer.Write((short)this.Value.Length); + + // Variable length Option Value + writer.WriteAligned(this.Value); + } + } +} \ No newline at end of file diff --git a/src/Outputs/PcapNg/PcapNgFileOutput.cs b/src/Snifter/Output/PcapNg/PcapNgFileOutput.cs similarity index 92% rename from src/Outputs/PcapNg/PcapNgFileOutput.cs rename to src/Snifter/Output/PcapNg/PcapNgFileOutput.cs index de18d1f..dadb0db 100644 --- a/src/Outputs/PcapNg/PcapNgFileOutput.cs +++ b/src/Snifter/Output/PcapNg/PcapNgFileOutput.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace Snifter.Outputs.PcapNg +namespace Snifter.Output.PcapNg { /// /// Outputs files in PCAPNG file format @@ -18,7 +18,8 @@ public PcapNgFileOutput(NetworkInterfaceInfo nic, string filename) this.nic = nic; this.fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None); this.writer = new BinaryWriter(this.fileStream); - this.WriteHeader(); + + WriteHeader(); } public void Output(TimestampedData timestampedData) @@ -45,6 +46,8 @@ private void WriteHeader() var interfaceDescriptionBlock = new InterfaceDescriptionBlock(this.nic); this.writer.Write(interfaceDescriptionBlock.GetBytes()); + + this.writer.Flush(); } } } diff --git a/src/Snifter/Output/PcapNg/SectionHeaderBlock.cs b/src/Snifter/Output/PcapNg/SectionHeaderBlock.cs new file mode 100644 index 0000000..d2d672e --- /dev/null +++ b/src/Snifter/Output/PcapNg/SectionHeaderBlock.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Text; + +namespace Snifter.Output.PcapNg +{ + /// + /// Section Header Block (SHB). The SHB is mandatory, and identifies the beginning of a section + /// of the capture dump file. + /// https://tools.ietf.org/html/draft-tuexen-opswg-pcapng-00#section-4.1 + /// + /// 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 + /// +---------------------------------------------------------------+ + /// 0 | Block Type = 0x0A0D0D0A | + /// +---------------------------------------------------------------+ + /// 4 | Block Total Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 8 | Byte-Order Magic | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 12 | Major Version | Minor Version | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 16 | | + /// | Section Length | + /// | | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// 24 / / + /// / Options (variable) / + /// / / + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Block Total Length | + /// +---------------------------------------------------------------+ + /// + public class SectionHeaderBlock : IBlock + { + private static readonly byte[] BlockType = { 0x0a, 0x0d, 0x0d, 0x0a }; + private static readonly byte[] ByteOrderMagic = { 0x4d, 0x3c, 0x2b, 0x1a }; + + // PCAPNG format version (major version 1, minor version 0) + private static readonly byte[] Version = { 0x01, 0x00, 0x00, 0x00 }; + + // Section Length (0xffffffffffffffff means "unspecified") + private static readonly byte[] SectionLength = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; + + private static readonly OptionalField OperatingSystem = new OptionalField(OptionTypeCode.SectionHeaderOperatingSystem, Environment.OSVersion.ToString()); + private static readonly OptionalField Application = new OptionalField(OptionTypeCode.SectionHeaderUserApp, "https://github.com/cocowalla/snifter"); + + // Block Total Length + private static readonly int TotalBlockLength = + BlockType.Length + + sizeof(int) + + ByteOrderMagic.Length + + Version.Length + + SectionLength.Length + + OperatingSystem.Length + + Application.Length + + OptionalField.EndOfOptions.Length + + sizeof(int); + + public void WriteTo(BinaryWriter writer) + { + writer.Write(BlockType); + writer.Write(TotalBlockLength); + writer.Write(ByteOrderMagic); + writer.Write(Version); + writer.Write(SectionLength); + + writer.Write(OperatingSystem); + writer.Write(Application); + writer.Write(OptionalField.EndOfOptions); + + writer.Write(TotalBlockLength); + } + + public byte[] GetBytes() + { + byte[] blockData; + + using (var ms = MemoryStreamPool.Get(TotalBlockLength)) + { + using (var writer = new BinaryWriter(ms, Encoding.UTF8, true)) + { + WriteTo(writer); + } + + blockData = ms.ToArray(); + } + + return blockData; + } + } +} diff --git a/src/Snifter/Protocol/Internet/FragmentationFlags.cs b/src/Snifter/Protocol/Internet/FragmentationFlags.cs new file mode 100644 index 0000000..54b6d4a --- /dev/null +++ b/src/Snifter/Protocol/Internet/FragmentationFlags.cs @@ -0,0 +1,11 @@ +using System; + +namespace Snifter.Protocol.Internet +{ + [Flags] + public enum FragmentationFlags : byte + { + DontFragment = 0x01, + MoreFragments = 0x02 + } +} diff --git a/src/Snifter/Protocol/Internet/IIpPacket.cs b/src/Snifter/Protocol/Internet/IIpPacket.cs new file mode 100644 index 0000000..138091e --- /dev/null +++ b/src/Snifter/Protocol/Internet/IIpPacket.cs @@ -0,0 +1,31 @@ +using System; +using System.Net; +using Snifter.Protocol.Transport; + +namespace Snifter.Protocol.Internet +{ + /// + /// An Internet Protocol (IP) packet + /// + public interface IIpPacket + { + public DateTime CaptureTime { get; } + + /// IP Protocol Version (e.g. IPv4 or IPv6) + public IpVersion Version { get; } + + public IpProtocol Protocol { get; } + + public IPAddress SourceAddress { get; } + public IPAddress DestinationAddress { get; } + + /// Transport-layer packet contained within the payload + public ITransportPacket TransportPacket { get; } + + /// The packet payload + public ReadOnlyMemory Payload { get; } + + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + } +} diff --git a/src/Snifter/Protocol/Internet/IpPacketParser.cs b/src/Snifter/Protocol/Internet/IpPacketParser.cs new file mode 100644 index 0000000..dd030d3 --- /dev/null +++ b/src/Snifter/Protocol/Internet/IpPacketParser.cs @@ -0,0 +1,58 @@ +using System; +using Snifter.Protocol.Transport; + +namespace Snifter.Protocol.Internet +{ + public class IpPacketParser + { + private readonly TransportPacketParser transportPacketParser; + + /// + /// Create an IP Packet Parser that will parse IP packets, without parsing the transport-level payload + /// + public IpPacketParser() + { + // Do nothing + } + + /// + /// Create an IP Packet Parser that will parse IP packets and their transport-level payload + /// + public IpPacketParser(TransportPacketParser transportPacketParser) + { + this.transportPacketParser = transportPacketParser; + } + + /// + /// Builds an IP packet from a raw packet capture + /// + /// Raw packet capture + /// Time the packet was captured + /// A parsed IP packet + public IIpPacket Parse(ReadOnlyMemory data, DateTime captureTime) + { + // First byte contains both the IP Version and Header Length + var versionAndLength = data.Span[0]; + var version = BinaryHelper.ReadBits(versionAndLength, 0, 4); + + if (version == 4) + { + var packet = new IpV4Packet(data, captureTime); + + if (this.transportPacketParser != null) + { + packet.ParseTransportPacket(this.transportPacketParser); + } + + return packet; + } + if (version == 6) + { + // IPv6 packets not yet supported! + return null; + } + + throw new ArgumentOutOfRangeException($"Unexpected IP packet version: {version}"); + } + } +} diff --git a/src/Snifter/Protocol/Internet/IpProtocol.cs b/src/Snifter/Protocol/Internet/IpProtocol.cs new file mode 100644 index 0000000..331f792 --- /dev/null +++ b/src/Snifter/Protocol/Internet/IpProtocol.cs @@ -0,0 +1,446 @@ + +// ReSharper disable CommentTypo +// ReSharper disable InconsistentNaming +namespace Snifter.Protocol.Internet +{ + /// + /// IP protocols, as specified in RFC 790 + /// + public enum IpProtocol : byte + { + /// IPv6 Hop-by-Hop Option + HOPOPT = 0, + + /// Internet Control Message Protocol + ICMP = 1, + + /// Internet Group Management Protocol + IGMP = 2, + + /// Gateway-to-Gateway Protocol + GGP = 3, + + /// IP in IP (encapsulation) + IP_in_IP = 4, + + /// Internet Stream Protocol + ST = 5, + + /// Transmission Control Protocol + TCP = 6, + + /// Core-based trees + CBT = 7, + + /// Exterior Gateway Protocol + EGP = 8, + + /// Interior Gateway Protocol (any private interior gateway (used by Cisco for their IGRP)) + IGP = 9, + + /// BBN RCC Monitoring + BBN_RCC_MON = 10, + + /// Network Voice Protocol + NVP_II = 11, + + /// Xerox PUP + PUP = 12, + + /// ARGUS + ARGUS = 13, + + /// EMCON + EMCON = 14, + + /// Cross Net Debugger + XNET = 15, + + /// Chaos + CHAOS = 16, + + /// User Datagram Protocol + UDP = 17, + + /// Multiplexing + MUX = 18, + + /// DCN Measurement Subsystems + DCN_MEAS = 19, + + /// Host Monitoring Protocol + HMP = 20, + + /// Packet Radio Measurement + PRM = 21, + + /// XEROX NS IDP + XNS_IDP = 22, + + /// Trunk-1 + TRUNK_1 = 23, + + /// Trunk-2 + TRUNK_2 = 24, + + /// Leaf-1 + LEAF_1 = 25, + + /// Leaf-2 + LEAF_2 = 26, + + /// Reliable Data Protocol + RDP = 27, + + /// Internet Reliable Transaction Protocol + IRTP = 28, + + /// ISO Transport Protocol Class 4 + ISO_TP4 = 29, + + /// Bulk Data Transfer Protocol + NETBLT = 30, + + /// MFE Network Services Protocol + MFE_NSP = 31, + + /// MERIT Internodal Protocol + MERIT_INP = 32, + + /// Datagram Congestion Control Protocol + DCCP = 33, + + /// Third Party Connect Protocol + ThreePC = 34, + + /// Inter-Domain Policy Routing Protocol + IDPR = 35, + + /// Xpress Transport Protocol + XTP = 36, + + /// Datagram Delivery Protocol + DDP = 37, + + /// IDPR Control Message Transport Protocol + IDPR_CMTP = 38, + + /// TP++ Transport Protocol + TP_PlusPlus = 39, + + /// IL Transport Protocol + IL = 40, + + /// IPv6 Encapsulation + IPv6 = 41, + + /// Source Demand Routing Protocol + SDRP = 42, + + /// Routing Header for IPv6 + IPv6_Route = 43, + + /// Fragment Header for IPv6 + IPv6_Frag = 44, + + /// Inter-Domain Routing Protocol + IDRP = 45, + + /// Resource Reservation Protocol + RSVP = 46, + + /// Generic Routing Encapsulation + GREs = 47, + + /// Dynamic Source Routing Protocol + DSR = 48, + + /// Burroughs Network Architecture + BNA = 49, + + /// Encapsulating Security Payload + ESP = 50, + + /// Authentication Header + AH = 51, + + /// Integrated Net Layer Security Protocol + I_NLSP = 52, + + /// SwIPe + SwIPe = 53, + + /// NBMA Address Resolution Protocol + NARP = 54, + + /// IP Mobility (Min Encap) + MOBILE = 55, + + /// Transport Layer Security Protocol (using Kryptonet key management) + TLSP = 56, + + /// Simple Key-Management for Internet Protocol + SKIP = 57, + + /// ICMP for IPv6 + IPv6_ICMP = 58, + + /// No Next Header for IPv6 + IPv6_NoNxt = 59, + + /// Destination Options for IPv6 + IPv6_Opts = 60, + + /// Any host internal protocol + HostInternal = 61, + + /// CFTP + CFTP = 62, + + /// Any local network + LocalNetwork = 63, + + /// SATNET and Backroom EXPAK + SAT_EXPAK = 64, + + /// Kryptolan + KRYPTOLAN = 65, + + /// MIT Remote Virtual Disk Protocol + RVD = 66, + + /// Internet Pluribus Packet Core + IPPC = 67, + + /// Any distributed file system + DistributedFileSystem = 68, + + /// SATNET Monitoring + SAT_MON = 69, + + /// VISA Protocol + VISA = 70, + + /// Internet Packet Core Utility + IPCU = 71, + + /// Computer Protocol Network Executive + CPNX = 72, + + /// Computer Protocol Heart Beat + CPHB = 73, + + /// Wang Span Network + WSN = 74, + + /// Packet Video Protocol + PVP = 75, + + /// Backroom SATNET Monitoring + BR_SAT_MON = 76, + + /// SUN ND PROTOCOL-Temporary + SUN_ND = 77, + + /// WIDEBAND Monitoring + WB_MON = 78, + + /// WIDEBAND EXPAK + WB_EXPAK = 79, + + /// International Organization for Standardization Internet Protocol + ISO_IP = 80, + + /// Versatile Message Transaction Protocol + VMTP = 81, + + /// Secure Versatile Message Transaction Protocol + SECURE_VMTP = 82, + + /// VINES + VINES = 83, + + /// TTP + TTP = 84, + + /// Internet Protocol Traffic Manager + IPTM = 84, + + /// NSFNET-IGP + NSFNET_IGP = 85, + + /// Dissimilar Gateway Protocol + DGP = 86, + + /// TCF + TCF = 87, + + /// EIGRP + EIGRP = 88, + + /// Open Shortest Path First + OSPF = 89, + + /// Sprite RPC Protocol + Sprite_RPC = 90, + + /// Locus Address Resolution Protocol + LARP = 91, + + /// Multicast Transport Protocol + MTP = 92, + + /// AX.25 + AX25 = 93, + + /// KA9Q NOS compatible IP over IP tunneling + OS = 94, + + /// Mobile Internetworking Control Protocol + MICP = 95, + + /// Semaphore Communications Sec. Pro + SCC_SP = 96, + + /// Ethernet-within-IP Encapsulation + ETHERIP = 97, + + /// Encapsulation Header + ENCAP = 98, + + /// Any private encryption scheme + PrivateEncryptionScheme = 99, + + /// GMTP + GMTP = 100, + + /// Ipsilon Flow Management Protocol + IFMP = 101, + + /// PNNI over IP + PNNI = 102, + + /// Protocol Independent Multicast + PIM = 103, + + /// IBM's ARIS (Aggregate Route IP Switching) Protocol + ARIS = 104, + + /// SCPS (Space Communications Protocol Standards) + SCPS = 105, + + /// QNX + QNX = 106, + + /// Active Networks + AN = 107, + + /// IP Payload Compression Protocol + IPComp = 108, + + /// Sitara Networks Protocol + SNP = 109, + + /// Compaq Peer Protocol + Compaq_Peer = 110, + + /// IPX in IP + IPX_in_IP = 111, + + /// Virtual Router Redundancy Protocol, Common Address Redundancy Protocol (not IANA assigned) + VRRP = 112, + + /// PGM Reliable Transport Protocol + PGM = 113, + + /// Any 0-hop protocol + AnyZeroHop = 114, + + /// Layer Two Tunneling Protocol Version 3 + L2TP = 115, + + /// D-II Data Exchange (DDX) + DDX = 116, + + /// Interactive Agent Transfer Protocol + IATP = 117, + + /// Schedule Transfer Protocol + STP = 118, + + /// SpectraLink Radio Protocol + SRP = 119, + + /// Universal Transport Interface Protocol + UTI = 120, + + /// Simple Message Protocol + SMP = 121, + + /// Simple Multicast Protocol + SM = 122, + + /// Performance Transparency Protocol + PTP = 123, + + /// Intermediate System to Intermediate System (IS-IS) Protocol over IPv4 + IS_IS_over_IPv4 = 124, + + /// Flexible Intra-AS Routing Environment + FIRE = 125, + + /// Combat Radio Transport Protocol + CRTP = 126, + + /// Combat Radio User Datagram + CRUDP = 127, + + /// Service-Specific Connection-Oriented Protocol in a Multi-link and Connectionless Environment + SSCOPMCE = 128, + + /// + IPLT = 129, + + /// Secure Packet Shield + SPS = 130, + + /// Private IP Encapsulation within IP + PIPE = 131, + + /// Stream Control Transmission Protocol + SCTP = 132, + + /// Fibre Channel + FC = 133, + + /// Reservation Protocol (RSVP) End-to-End Ignore + RSVP_E2E_IGNORE = 134, + + /// Header + Mobility = 135, + + /// Lightweight User Datagram Protocol + UDPLite = 136, + + /// Multi-protocol Label Switching Encapsulated in IP + MPLS_in_IP = 137, + + /// MANET Protocols + manet = 138, + + /// Host Identity Protocol + HIP = 139, + + /// Site Multi-homing by IPv6 Intermediation + Shim6 = 140, + + /// Wrapped Encapsulating Security Payload + WESP = 141, + + /// Robust Header Compression + ROHC = 142, + + /// IPv6 Segment Routing + Ethernet = 143 + } +} diff --git a/src/Snifter/Protocol/Internet/IpV4Packet.cs b/src/Snifter/Protocol/Internet/IpV4Packet.cs new file mode 100644 index 0000000..f07a393 --- /dev/null +++ b/src/Snifter/Protocol/Internet/IpV4Packet.cs @@ -0,0 +1,128 @@ +using System; +using System.Net; +using Snifter.Protocol.Transport; + +namespace Snifter.Protocol.Internet +{ + /// + /// An IPv4 packet, as described in RFC 791 + /// https://tools.ietf.org/html/rfc791 + /// + /// 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 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// |Version| IHL |Type of Service| Total Length | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Identification |Flags| Fragment Offset | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Time to Live | Protocol | Header Checksum | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Source Address | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Destination Address | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Options | Padding | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// + public sealed class IpV4Packet : IIpPacket + { + public DateTime CaptureTime { get; } + + public IpVersion Version { get; } = IpVersion.Ipv4; + + /// Length of the IP header, in bytes + public ushort HeaderLength { get; } + public byte TypeOfService { get; } + + /// + /// The total length of the packet, including header and payload, as stated in the Total Length field of the packet header + /// + public ushort TotalLength { get; } + + public ushort Identification { get; } + public FragmentationFlags FragmentationFlags { get; } + public ushort FragmentOffset { get; } + + public byte TimeToLive { get; } + public IpProtocol Protocol { get; } + public ushort HeaderChecksum { get; } + + public IPAddress SourceAddress { get; } + public IPAddress DestinationAddress { get; } + + /// Transport-layer packet contained within the payload + public ITransportPacket TransportPacket { get; private set; } + + /// The packet payload + public ReadOnlyMemory Payload { get; } + + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + + public IpV4Packet(ReadOnlyMemory data, DateTime? captureTime = null) + { + this.CaptureTime = captureTime ?? DateTime.UtcNow; + + this.RawData = data; + var span = this.RawData.Span; + + // First byte contains both the IP Version and Header Length + var versionAndLength = span[Offsets.VersionAndHeaderLength]; + var version = BinaryHelper.ReadBits(versionAndLength, 0, 4); + + // This is an IPv4 packet + if (version != 4) + return; + + // IHL is encoded as the number of 32-bit words (4 bytes) + this.HeaderLength = (ushort)(BinaryHelper.ReadBits(versionAndLength, 4, 4) * 4); + this.Payload = this.RawData.Slice(this.HeaderLength); + + this.TypeOfService = span[Offsets.TypeOfService]; + this.TotalLength = span.ReadUInt16BigEndian(Offsets.TotalLength); + + this.Identification = span.ReadUInt16BigEndian(Offsets.Identification); + + // ushort containing flags in the first 3 bits, and offset in the following 13 bits + var flagsAndOffset = span.ReadUInt16BigEndian(Offsets.FlagsAndOffset); + this.FragmentationFlags = (FragmentationFlags)BinaryHelper.ReadBits(flagsAndOffset, 0, 3); + this.FragmentOffset = BinaryHelper.ReadBits(flagsAndOffset, 3, 13); + + this.TimeToLive = span[Offsets.Ttl]; + this.Protocol = (IpProtocol)span[Offsets.Protocol]; + this.HeaderChecksum = span.ReadUInt16BigEndian(Offsets.HeaderChecksum); + + this.SourceAddress = IPHelper.ReadIPv4Address(span.Slice(Offsets.SourceAddress, sizeof(uint))); + this.DestinationAddress = IPHelper.ReadIPv4Address(span.Slice(Offsets.DestinationAddress, sizeof(uint))); + + this.TransportPacket = new RawPacket(this); + } + + /// + /// Parse the payload into a Transport Packet + /// + /// + public void ParseTransportPacket(TransportPacketParser parser) + { + this.TransportPacket = parser.Parse(this); + } + + private static class Offsets + { + public const int VersionAndHeaderLength = 0; + public const int TypeOfService = 1; + public const int TotalLength = 2; + public const int Identification = 4; + public const int FlagsAndOffset = 6; + public const int Ttl = 8; + public const int Protocol = 9; + public const int HeaderChecksum = 10; + public const int SourceAddress = 12; + public const int DestinationAddress = 16; + + // Not often used in the real-world, so we don't parse it + public const int Options = 20; + } + } +} diff --git a/src/Snifter/Protocol/Internet/IpVersion.cs b/src/Snifter/Protocol/Internet/IpVersion.cs new file mode 100644 index 0000000..42d8376 --- /dev/null +++ b/src/Snifter/Protocol/Internet/IpVersion.cs @@ -0,0 +1,12 @@ + +namespace Snifter.Protocol.Internet +{ + /// + /// Internet Protocol versions + /// + public enum IpVersion : byte + { + Ipv4 = 4, + Ipv6 = 6 + } +} diff --git a/src/Snifter/Protocol/Transport/ITransportPacket.cs b/src/Snifter/Protocol/Transport/ITransportPacket.cs new file mode 100644 index 0000000..89f6293 --- /dev/null +++ b/src/Snifter/Protocol/Transport/ITransportPacket.cs @@ -0,0 +1,19 @@ +using System; + +namespace Snifter.Protocol.Transport +{ + public interface ITransportPacket + { + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + } + + /// + /// A transport-layer packet for a protocol with a port abstraction (e.g. TCP, UDP, SCTP) + /// + public interface IHasPorts + { + public ushort SourcePort { get; } + public ushort DestinationPort { get; } + } +} diff --git a/src/Snifter/Protocol/Transport/IcmpPacket.cs b/src/Snifter/Protocol/Transport/IcmpPacket.cs new file mode 100644 index 0000000..dadc50f --- /dev/null +++ b/src/Snifter/Protocol/Transport/IcmpPacket.cs @@ -0,0 +1,54 @@ +using System; +using Snifter.Protocol.Internet; + +namespace Snifter.Protocol.Transport +{ + /// + /// An ICMP packet, as described in RFC 792 + /// https://tools.ietf.org/html/rfc792 + /// + /// 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 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Type | Code | Checksum | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Value, depending on Type | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Payload | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// + public sealed class IcmpPacket : ITransportPacket + { + public ushort Checksum { get; } + + /// The packet payload + public ReadOnlyMemory Payload { get; } + + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + + public IcmpPacket(IIpPacket ipPacket) + { + if (ipPacket == null) throw new ArgumentNullException(nameof(ipPacket)); + if (ipPacket.Protocol != IpProtocol.ICMP) throw new ArgumentOutOfRangeException(nameof(ipPacket.Protocol)); + + this.RawData = ipPacket.Payload; + var span = this.RawData.Span; + + // TODO: Parse Type, Code and Value + + this.Checksum = span.ReadUInt16BigEndian(Offsets.Checksum); + this.Payload = this.RawData.Slice(Offsets.Payload); + } + + private static class Offsets + { + public const int Type = 0; + public const int Code = 2; + public const int Checksum = 16; + public const int Value = 32; + public const int Payload = 64; + } + } +} diff --git a/src/Snifter/Protocol/Transport/RawPacket.cs b/src/Snifter/Protocol/Transport/RawPacket.cs new file mode 100644 index 0000000..f3e12e4 --- /dev/null +++ b/src/Snifter/Protocol/Transport/RawPacket.cs @@ -0,0 +1,21 @@ +using System; +using Snifter.Protocol.Internet; + +namespace Snifter.Protocol.Transport +{ + /// + /// An unparsed packet, simply providing access to the raw payload + /// + public sealed class RawPacket : ITransportPacket + { + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + + public RawPacket(IIpPacket ipPacket) + { + if (ipPacket == null) throw new ArgumentNullException(nameof(ipPacket)); + + this.RawData = ipPacket.Payload; + } + } +} diff --git a/src/Snifter/Protocol/Transport/TcpControlFlags.cs b/src/Snifter/Protocol/Transport/TcpControlFlags.cs new file mode 100644 index 0000000..4fc87d9 --- /dev/null +++ b/src/Snifter/Protocol/Transport/TcpControlFlags.cs @@ -0,0 +1,43 @@ + +// ReSharper disable CommentTypo +// ReSharper disable InconsistentNaming +namespace Snifter.Protocol.Transport +{ + /// + /// TCP Control flags, as specified in RFC 793 + /// + public enum TcpControlFlags : ushort + { + /// No control flags are set + None = 0x0000, + + /// No more data from sender + FIN = 0x0001, + + /// Synchronize sequence numbers + SYN = 0x0002, + + /// Reset the connection + RST = 0x0004, + + /// Push Function + PSH = 0x0008, + + /// Acknowledgment field significant + ACK = 0x0010, + + /// Urgent Pointer field significant + URG = 0x0020, + + /// Explicit congestion notification echo. Added to the IPv4 spec in RFC 3168 + ECE = 0x0040, + + /// + /// Congestion Window Reduced. Added to the IPv4 spec in RFC 3168 + /// + CWR = 0x0080, + + /// Nonce sum. Added to the IPv4 spec in RFC 3540 + NS = 0x0100 + } +} diff --git a/src/Snifter/Protocol/Transport/TcpPacket.cs b/src/Snifter/Protocol/Transport/TcpPacket.cs new file mode 100644 index 0000000..f813ad5 --- /dev/null +++ b/src/Snifter/Protocol/Transport/TcpPacket.cs @@ -0,0 +1,117 @@ +using System; +using Snifter.Protocol.Internet; + +namespace Snifter.Protocol.Transport +{ + /// + /// A TCP packet, as described in RFC 793 + /// https://tools.ietf.org/html/rfc793 + /// + /// 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 + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Source Port | Destination Port | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Sequence Number | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Acknowledgment Number | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Data | Res |N|C|E|U|A|P|R|S|F| | + /// | Offset| erv |S|W|C|R|C|S|S|Y|I| Window | + /// | | ed | |R|E|G|K|H|T|N|N| | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Checksum | Urgent Pointer | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Options | Padding | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// | Data | + /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + /// + /// + public sealed class TcpPacket : ITransportPacket, IHasPorts + { + public ushort SourcePort { get; } + public ushort DestinationPort { get; } + + /// + /// The sequence number of the first data octet in this segment (except when SYN is present). + /// If SYN is present the sequence number is the initial sequence number (ISN) and the first data octet is ISN+1. + /// + public uint SequenceNumber { get; } + + /// + /// If the ACK control bit is set, this field contains the value of the next sequence number that the sender + /// of the segment is expecting to receive. Once a connection is established this is always sent. + /// + public uint AcknowledgmentNumber { get; } + + /// Length of the TCP header, in bytes. This indicates where the data begins + public ushort DataOffset { get; } + + /// Control flags + public TcpControlFlags ControlFlags { get; } + + /// + /// The number of data octets, beginning with the one indicated in the acknowledgment field, which the sender + /// of this segment is willing to accept + /// + public ushort Window { get; } + public ushort Checksum { get; } + + /// + /// The current value of the urgent pointer as a positive offset from the sequence number in this segment. + /// The urgent pointer points to the sequence number of the octet following the urgent data. + /// This field is only be interpreted in segments with the URG control bit set. + /// + public ushort UrgentPointer { get; } + + /// The packet payload + public ReadOnlyMemory Payload { get; } + + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + + public TcpPacket(IIpPacket ipPacket) + { + if (ipPacket == null) throw new ArgumentNullException(nameof(ipPacket)); + if (ipPacket.Protocol != IpProtocol.TCP) throw new ArgumentOutOfRangeException(nameof(ipPacket.Protocol)); + + this.RawData = ipPacket.Payload; + var span = this.RawData.Span; + + this.SourcePort = span.ReadUInt16BigEndian(Offsets.SourcePort); + this.DestinationPort = span.ReadUInt16BigEndian(Offsets.DestinationPort); + this.SequenceNumber = span.ReadUInt32BigEndian(Offsets.SequenceNumber); + this.AcknowledgmentNumber = span.ReadUInt32BigEndian(Offsets.AcknowledgmentNumber); + + // ushort containing Data Offset (aka Header Length) in the first 4 bits, and Control Flags in the following 12 bits + var dataOffsetAndFlags = span.ReadUInt16BigEndian(Offsets.DataOffsetAndFlags); + + // Data Offset (aka Header Length) is encoded as the number of 32-bit words (4 bytes) + this.DataOffset = (ushort)(BinaryHelper.ReadBits(dataOffsetAndFlags, 0, 4) * 4); + this.Payload = this.RawData.Slice(this.DataOffset); + + this.ControlFlags = (TcpControlFlags)BinaryHelper.ReadBits(dataOffsetAndFlags, 4, 12); + + this.Window = span.ReadUInt16BigEndian(Offsets.Window); + this.Checksum = span.ReadUInt16BigEndian(Offsets.Checksum); + this.UrgentPointer = span.ReadUInt16BigEndian(Offsets.UrgentPointer); + + // TODO: Parse Options + } + + // Byte offsets + private static class Offsets + { + public const int SourcePort = 0; + public const int DestinationPort = 2; + public const int SequenceNumber = 4; + public const int AcknowledgmentNumber = 8; + public const int DataOffsetAndFlags = 12; + public const int Window = 14; + public const int Checksum = 16; + public const int UrgentPointer = 18; + public const int Options = 20; + } + } +} diff --git a/src/Snifter/Protocol/Transport/TransportPacketParser.cs b/src/Snifter/Protocol/Transport/TransportPacketParser.cs new file mode 100644 index 0000000..cf590ac --- /dev/null +++ b/src/Snifter/Protocol/Transport/TransportPacketParser.cs @@ -0,0 +1,23 @@ +using Snifter.Protocol.Internet; + +namespace Snifter.Protocol.Transport +{ + public class TransportPacketParser + { + /// + /// Builds a transport-layer packet from an IP packet payload + /// + public ITransportPacket Parse(IIpPacket ipPacket) + { + return ipPacket.Protocol switch + { + IpProtocol.TCP => new TcpPacket(ipPacket), + IpProtocol.UDP => new UdpPacket(ipPacket), + IpProtocol.ICMP => new IcmpPacket(ipPacket), + + // Other transport-layer packet types are not yet supported + _ => null + }; + } + } +} diff --git a/src/Snifter/Protocol/Transport/UdpPacket.cs b/src/Snifter/Protocol/Transport/UdpPacket.cs new file mode 100644 index 0000000..72610dd --- /dev/null +++ b/src/Snifter/Protocol/Transport/UdpPacket.cs @@ -0,0 +1,61 @@ +using System; +using Snifter.Protocol.Internet; + +namespace Snifter.Protocol.Transport +{ + /// + /// A UDP packet, as described in RFC 768 + /// https://tools.ietf.org/html/rfc768 + /// + /// 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 + /// +---------------+---------------+---------------+---------------+ + /// | Source Port | Destination Port | + /// +---------------+---------------+---------------+---------------- + /// | Length | Checksum | + /// +---------------+---------------+---------------+---------------- + /// | Data | + /// +---------------+---------------+---------------+---------------+ + /// + /// + public sealed class UdpPacket : ITransportPacket, IHasPorts + { + public ushort SourcePort { get; } + public ushort DestinationPort { get; } + + /// Total length of the packet, in bytes (includes both header and data) + public ushort Length { get; } + + public ushort Checksum { get; } + + /// The packet payload + public ReadOnlyMemory Payload { get; } + + /// The full, raw data that comprises the packet + public ReadOnlyMemory RawData { get; } + + public UdpPacket(IIpPacket ipPacket) + { + if (ipPacket == null) throw new ArgumentNullException(nameof(ipPacket)); + if (ipPacket.Protocol != IpProtocol.UDP) throw new ArgumentOutOfRangeException(nameof(ipPacket.Protocol)); + + this.RawData = ipPacket.Payload; + var span = this.RawData.Span; + + this.SourcePort = span.ReadUInt16BigEndian(Offsets.SourcePort); + this.DestinationPort = span.ReadUInt16BigEndian(Offsets.DestinationPort); + this.Length = span.ReadUInt16BigEndian(Offsets.Length); + this.Checksum = span.ReadUInt16BigEndian(Offsets.Checksum); + this.Payload = this.RawData.Slice(Offsets.Payload); + } + + private static class Offsets + { + public const int SourcePort = 0; + public const int DestinationPort = 2; + public const int Length = 4; + public const int Checksum = 6; + public const int Payload = 8; + } + } +} diff --git a/src/Snifter/Snifter.csproj b/src/Snifter/Snifter.csproj new file mode 100644 index 0000000..9b1c2d1 --- /dev/null +++ b/src/Snifter/Snifter.csproj @@ -0,0 +1,48 @@ + + + + Raw Socket Sniffer + 1.3.0 + false + Colin Anderson + Copyright © Colin Anderson 2015 + net471;netstandard2.0;netstandard2.1 + Library + latest + Snifter + Snifter + Snifter + network;packet sniffer + http://github.com/cocowalla/snifter + git + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snifter/SocketSniffer.cs b/src/Snifter/SocketSniffer.cs new file mode 100644 index 0000000..093a53a --- /dev/null +++ b/src/Snifter/SocketSniffer.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Snifter.Filter; +using Snifter.Output; +using Snifter.Protocol.Internet; +using Snifter.Protocol.Transport; + +namespace Snifter +{ + /// + /// Raw socket sniffer + /// + public class SocketSniffer : IDisposable + { + private const int BUFFER_SIZE = 1024 * 64; + private const int MAX_PROCESS_QUEUE = 10_000; + + private volatile bool isStopping; + private readonly IpPacketParser packetParser = new IpPacketParser(new TransportPacketParser()); + private readonly SocketAsyncEventArgs socketEventArgs = new SocketAsyncEventArgs(); + private readonly Socket socket; + + // When packets are captured they are queued here, then parsed, filters and output when dequeued + private readonly BlockingCollection processQueue; + + private readonly Filters filters; + private readonly IOutput output; + private readonly ILogger logger; + + public Statistics Statistics { get; } = new Statistics(); + + /// + /// Raised when a matching packet has been captured and parsed + /// + public event Action PacketCaptured; + + /// + /// Create a new raw socket sniffer + /// + /// Network interface from which to capture packets + /// Filters to apply before outputting packets or raising events + /// An optional output to which matching packets should be written, such as a PCAPNG file + /// Maximum size of the packet processing queue. Defaults to 10,000 + /// An optional logger, used only for logging errors + public SocketSniffer(NetworkInterfaceInfo nic, Filters filters, IOutput output = null, + int maxProcessQueue = MAX_PROCESS_QUEUE, ILogger logger = null) + { + this.logger = logger ?? NullLogger.Instance; + this.filters = filters; + this.output = output; + this.processQueue = new BlockingCollection(maxProcessQueue); + + // Capturing at the IP level is not supported on Linux + // https://github.com/dotnet/corefx/issues/25115 + // https://github.com/dotnet/corefx/issues/30197 + var protocolType = SystemInformation.IsWindows + ? ProtocolType.IP + : ProtocolType.Tcp; + + // IPv4 + var endPoint = new IPEndPoint(nic.IPAddress, 0); + this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, protocolType); + this.socket.Bind(endPoint); + this.socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); + + var buffer = new byte[BUFFER_SIZE]; + this.socketEventArgs.Completed += (e, args) => OnReceived(socketEventArgs); + this.socketEventArgs.SetBuffer(buffer, 0, BUFFER_SIZE); + + // Enter promiscuous mode on Windows only + if (SystemInformation.IsWindows) + { + EnterPromiscuousMode(); + } + } + + private void EnterPromiscuousMode() + { + try + { + this.socket.IOControl(IOControlCode.ReceiveAll, BitConverter.GetBytes(1), new byte[4]); + } + catch (Exception ex) + { + // Can't sniff raw sockets on Windows without being in promiscuous mode + throw new InvalidOperationException($"Unable to enter promiscuous mode: {ex.Message}", ex); + } + } + + public void Start() + { + // Process queued packets that we've read + _ = Task.Run(() => + { + // GetConsumingEnumerable() will wait when queue is empty, until CompleteAdding() is called + foreach (var timestampedData in this.processQueue.GetConsumingEnumerable()) + { + Process(timestampedData); + } + }); + + // Read packets and queue them up for processing in this.processQueue + _ = Task.Run(() => StartReceiving(this.socketEventArgs)); + } + + public void Stop() + { + this.isStopping = true; + } + + // Queue up a captured packet for processing + private void Enqueue(TimestampedData timestampedData) + { + if (this.isStopping) + { + this.processQueue.CompleteAdding(); + return; + } + + var added = this.processQueue.TryAdd(timestampedData); + this.Statistics.IncrementObserved(); + + // Did we add the packet to the processing queue, or was it full? + if (!added) + { + this.Statistics.IncrementDropped(); + } + } + + // Parse, filter and output a captured packet + private void Process(TimestampedData timestampedData) + { + // Only parse the packet if we need to filter or raise an event + if (PacketCaptured != null || this.filters.PropertyFilters.Any()) + { + try + { + var packet = this.packetParser.Parse(timestampedData.Data, timestampedData.Timestamp); + + if (!this.filters.IsMatch(packet)) + { + return; + } + + try + { + PacketCaptured?.Invoke(packet); + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Unhandled error in event handler: {ErrorMessage}", ex.Message); + } + } + catch (Exception ex) + { + this.logger.LogWarning(ex, "Unable to parse packet of {Length} bytes received at {Timestamp}: {ErrorMessage}", + timestampedData.Data.Length, timestampedData.Timestamp, ex.Message); + + return; + } + } + + this.output?.Output(timestampedData); + this.Statistics.IncrementCaptured(); + } + + // Start a socket receive operation + private void StartReceiving(SocketAsyncEventArgs socketEventArgs) + { + try + { + // If true, the IO operation is still pending, and will complete asynchronously - in that case, the + // socketEventArgs.Completed event will fire to handle the operation after it completes. + // If false, the IO operation completed synchronously - in that case, the socketEventArgs.Completed event will not fire, so + // we call it here + var operationPending = this.socket.ReceiveAsync(socketEventArgs); + + if (!operationPending) + { + OnReceived(socketEventArgs); + } + } + catch (Exception ex) + { + // Exceptions while shutting down are expected + if (!this.isStopping) + { + throw new InvalidOperationException($"Error while binding to socket: {ex.Message}", ex); + } + + this.socket.Close(); + } + } + + // Fired when a socket receive operation has completed + private void OnReceived(SocketAsyncEventArgs e) + { + // Start a new receive operation while we deal with the one that just completed + //Task.Run(() => StartReceiving()); + + try + { + if (e.SocketError != SocketError.Success) + { + if (!this.isStopping) + { + throw new InvalidOperationException($"Socket error during receive operation: {e.SocketError}"); + } + + return; + } + + if (e.BytesTransferred <= 0) + { + return; + } + + // TODO: Use pooled buffers - need to figure out buffer ownership tho, as packets leave our control via the PacketCaptured + // event. Probably we copy the pooled buffer to a "new byte[]" if this event is used, *or* we make the user + // responsible for disposing the IIpPackets + + // Copy the bytes received into a new buffer + var buffer = new byte[e.BytesTransferred]; + Buffer.BlockCopy(e.Buffer, e.Offset, buffer, 0, e.BytesTransferred); + + Enqueue(new TimestampedData(DateTime.UtcNow, buffer)); + } + catch (SocketException ex) + { + throw new InvalidOperationException($"Socket error during receive operation: {ex.Message}", ex); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unexpected error during receive operation: {ex.Message}", ex); + } + finally + { + // Start the next receive operation + if (!this.isStopping && this.socket != null && this.socket.IsBound) + { + StartReceiving(e); + } + } + } + + public void Dispose() + { + this.socket.Dispose(); + this.processQueue.Dispose(); + } + } +} diff --git a/src/Snifter/Statistics.cs b/src/Snifter/Statistics.cs new file mode 100644 index 0000000..5d758af --- /dev/null +++ b/src/Snifter/Statistics.cs @@ -0,0 +1,32 @@ +using System.Threading; + +namespace Snifter +{ + /// + /// Holds statistics about a packet capture session + /// + public class Statistics + { + private long packetsObserved; + private long packetsCaptured; + private long packetsDropped; + private int buffersInUse; + + public long PacketsObserved => this.packetsObserved; + public long PacketsCaptured => this.packetsCaptured; + public long PacketsDropped => this.packetsDropped; + public int BuffersInUse => this.buffersInUse; + + public void IncrementObserved() + => Interlocked.Increment(ref this.packetsObserved); + + public void IncrementCaptured() + => Interlocked.Increment(ref this.packetsCaptured); + + public void IncrementDropped() + => Interlocked.Increment(ref this.packetsDropped); + + public void SetBuffersInUse(int curentBuffersInUse) + => Interlocked.Exchange(ref this.buffersInUse, curentBuffersInUse); + } +} diff --git a/src/TimestampedData.cs b/src/Snifter/TimestampedData.cs similarity index 56% rename from src/TimestampedData.cs rename to src/Snifter/TimestampedData.cs index 7978b6e..e6beeaf 100644 --- a/src/TimestampedData.cs +++ b/src/Snifter/TimestampedData.cs @@ -2,15 +2,18 @@ namespace Snifter { + /// + /// A raw packet capture with a timestamp of the capture time + /// public class TimestampedData { public DateTime Timestamp { get; } public byte[] Data { get; } public TimestampedData(DateTime timestamp, byte[] data) - { - this.Timestamp = timestamp; - this.Data = data; - } + { + this.Timestamp = timestamp; + this.Data = data; + } } } diff --git a/src/Snifter/Utils/ArrayExtensions.cs b/src/Snifter/Utils/ArrayExtensions.cs new file mode 100644 index 0000000..0e57a92 --- /dev/null +++ b/src/Snifter/Utils/ArrayExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + public static class ArrayExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetAlignedLength(this IReadOnlyCollection source, int alignment = 4) + { + var alignedLength = (source.Count + alignment - 1) / alignment * alignment; + return alignedLength; + } + } +} diff --git a/src/Snifter/Utils/BinaryHelper.cs b/src/Snifter/Utils/BinaryHelper.cs new file mode 100644 index 0000000..463cd01 --- /dev/null +++ b/src/Snifter/Utils/BinaryHelper.cs @@ -0,0 +1,168 @@ +using System; +using System.Runtime.CompilerServices; + +// BinaryPrimitives only available from .NET Standard 2.1 +#if NETSTANDARD2_1 +using System.Buffers.Binary; +#else +using System.Runtime.InteropServices; +#endif + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + /// + /// Helpers for reading big-endian integers + /// + public static class BinaryHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32BigEndian(this ReadOnlySpan span, int offset) + => ReadUInt32BigEndian(span.Slice(offset, sizeof(uint))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16BigEndian(this ReadOnlySpan span, int offset) + => ReadUInt16BigEndian(span.Slice(offset, sizeof(ushort))); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadBits(byte value, int bitPosition, int bitLength) + { + const int bitsInByte = sizeof(byte) * 8; + var bitShift = bitsInByte - bitPosition - bitLength; + + if (bitShift < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException("Unable to read more than 8 bits from a byte"); + } + + return (byte)(((0xff >> bitPosition) & value) >> bitShift); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadBits(ushort value, int bitPosition, int bitLength) + { + const int bitsInUshort = sizeof(ushort) * 8; + var bitShift = bitsInUshort - bitPosition - bitLength; + + if (bitShift < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException("Unable to read more than 16 bits from a ushort"); + } + + return (ushort)(((0xffff >> bitPosition) & value) >> bitShift); + } + + // BinaryPrimitives only available from .NET Standard 2.1 + #if NETSTANDARD2_1 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32BigEndian(ReadOnlySpan source) + => BinaryPrimitives.ReadUInt32BigEndian(source); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16BigEndian(ReadOnlySpan source) + => BinaryPrimitives.ReadUInt16BigEndian(source); + + #else + + /// + /// Reads an Int32 out of a read-only span of bytes as big endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32BigEndian(ReadOnlySpan source) + { + var result = MemoryMarshal.Read(source); + + if (BitConverter.IsLittleEndian) + { + result = ReverseEndianness(result); + } + + return result; + } + + /// + /// Reads an Int16 out of a read-only span of bytes as big endian. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16BigEndian(ReadOnlySpan source) + { + var result = MemoryMarshal.Read(source); + + if (BitConverter.IsLittleEndian) + { + result = ReverseEndianness(result); + } + + return result; + } + + /// Reverses a primitive value - performs an endianness swap + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReverseEndianness(ushort value) + { + // Don't need to AND with 0xFF00 or 0x00FF since the final + // cast back to ushort will clear out all bits above [ 15 .. 00 ]. + // This is normally implemented via "movzx eax, ax" on the return. + // Alternatively, the compiler could elide the movzx instruction + // entirely if it knows the caller is only going to access "ax" + // instead of "eax" / "rax" when the function returns. + + return (ushort)((value >> 8) + (value << 8)); + } + + /// Reverses a primitive value - performs an endianness swap + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReverseEndianness(uint value) + { + // This takes advantage of the fact that the JIT can detect + // ROL32 / ROR32 patterns and output the correct intrinsic. + // + // Input: value = [ ww xx yy zz ] + // + // First line generates : [ ww xx yy zz ] + // & [ 00 FF 00 FF ] + // = [ 00 xx 00 zz ] + // ROR32(8) = [ zz 00 xx 00 ] + // + // Second line generates: [ ww xx yy zz ] + // & [ FF 00 FF 00 ] + // = [ ww 00 yy 00 ] + // ROL32(8) = [ 00 yy 00 ww ] + // + // (sum) = [ zz yy xx ww ] + // + // Testing shows that throughput increases if the AND + // is performed before the ROL / ROR. + + return RotateRight(value & 0x00FF00FFu, 8) // xx zz + + RotateLeft(value & 0xFF00FF00u, 8); // ww yy + } + + /// + /// Rotates the specified value right by the specified number of bits. + /// Similar in behavior to the x86 instruction ROR. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateRight(uint value, int offset) + => (value >> offset) | (value << (32 - offset)); + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + => (value << offset) | (value >> (32 - offset)); + + #endif + } +} \ No newline at end of file diff --git a/src/Snifter/Utils/BinaryWriterExtensions.cs b/src/Snifter/Utils/BinaryWriterExtensions.cs new file mode 100644 index 0000000..87f9f6b --- /dev/null +++ b/src/Snifter/Utils/BinaryWriterExtensions.cs @@ -0,0 +1,15 @@ +using System.IO; +using Snifter.Output.PcapNg; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + public static class BinaryWriterExtensions + { + public static void Write(this BinaryWriter writer, IBinaryWritable value) + => value.WriteTo(writer); + + public static void WriteAligned(this BinaryWriter writer, byte[] value) + => writer.BaseStream.WriteAligned(value); + } +} diff --git a/src/Snifter/Utils/IPHelper.cs b/src/Snifter/Utils/IPHelper.cs new file mode 100644 index 0000000..a219168 --- /dev/null +++ b/src/Snifter/Utils/IPHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + public static class IPHelper + { + // IPAddress ctor that takes ReadOnlySpan only available from .NET Standard 2.1 + #if NETSTANDARD2_1 + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IPAddress ReadIPv4Address(ReadOnlySpan address) + => new IPAddress(address); + + #else + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IPAddress ReadIPv4Address(ReadOnlySpan address) + { + const int IPv4NumBytes = 4; + + if (address.Length != IPv4NumBytes) + { + throw new ArgumentException($"Invalid IPv4 address - expected 4 bytes, but got {address.Length}"); + } + + var intAddress = (uint)((address[3] << 24 | address[2] << 16 | address[1] << 8 | address[0]) & 0x0FFFFFFFF); + return new IPAddress(intAddress); + } + + #endif + } +} \ No newline at end of file diff --git a/src/Snifter/Utils/MemoryStreamPool.cs b/src/Snifter/Utils/MemoryStreamPool.cs new file mode 100644 index 0000000..1e5fd10 --- /dev/null +++ b/src/Snifter/Utils/MemoryStreamPool.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Runtime.CompilerServices; +using Microsoft.IO; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + /// + /// Thin abstraction over RecyclableMemoryStreamManager + /// + public static class MemoryStreamPool + { + private static readonly RecyclableMemoryStreamManager Default = new RecyclableMemoryStreamManager(); + + public static MemoryStream Get([CallerMemberName]string name = null) => + Default.GetStream(name); + + public static MemoryStream Get(int len, [CallerMemberName]string name = null) => + Default.GetStream(name, len); + + public static MemoryStream Get(byte[] buffer, [CallerMemberName]string name = null) => + Default.GetStream(name, buffer, 0, buffer.Length); + } +} \ No newline at end of file diff --git a/src/Snifter/Utils/StreamExtensions.cs b/src/Snifter/Utils/StreamExtensions.cs new file mode 100644 index 0000000..1d17d98 --- /dev/null +++ b/src/Snifter/Utils/StreamExtensions.cs @@ -0,0 +1,43 @@ +using System; +using System.Buffers; +using System.IO; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + public static class StreamExtensions + { + /// + /// Writes data aligned to bytes + /// + /// Stream to write aligned data to + /// Value to write to the stream, aligned to bytes + /// Number of bytes to align to - defaults to 4 (32-bits) + public static void WriteAligned(this Stream stream, byte[] value, int alignment = 4) + { + if (value.Length == 0) + return; + + stream.Write(value, 0, value.Length); + + // Determine how much padding is required to align, if any + var alignedLen = (value.Length + alignment - 1) / alignment * alignment; + + // Is padding required? + if (value.Length == alignedLen) + return; + +#if NETSTANDARD2_1 + Span padding = stackalloc byte[alignedLen - value.Length]; + stream.Write(padding); +#else + var paddingLen = alignedLen - value.Length; + var padding = ArrayPool.Shared.Rent(paddingLen); + + stream.Write(padding, 0, padding.Length); + + ArrayPool.Shared.Return(padding); +#endif + } + } +} diff --git a/src/Snifter/Utils/SystemInformation.cs b/src/Snifter/Utils/SystemInformation.cs new file mode 100644 index 0000000..55a7a11 --- /dev/null +++ b/src/Snifter/Utils/SystemInformation.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + public static class SystemInformation + { + public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } +} diff --git a/src/Snifter/Utils/ThrowHelper.cs b/src/Snifter/Utils/ThrowHelper.cs new file mode 100644 index 0000000..1760557 --- /dev/null +++ b/src/Snifter/Utils/ThrowHelper.cs @@ -0,0 +1,14 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Snifter +{ + /// + /// Methods with a throw statement can't be inlined - a "throw helper" works around that limitation + /// + internal static class ThrowHelper + { + internal static void ThrowArgumentOutOfRangeException(string message) + => throw new ArgumentOutOfRangeException(message); + } +} diff --git a/src/SocketSniffer.cs b/src/SocketSniffer.cs deleted file mode 100644 index 36f5171..0000000 --- a/src/SocketSniffer.cs +++ /dev/null @@ -1,218 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Snifter.Filter; -using Snifter.Outputs; - -namespace Snifter -{ - public class SocketSniffer - { - private const int BUFFER_SIZE = 1024 * 64; - private const int MAX_RECEIVE = 100; - - private bool isStopping; - private long packetsObserved; - private long packetsCaptured; - private Socket socket; - private readonly ConcurrentStack receivePool; - private readonly SemaphoreSlim maxReceiveEnforcer = new SemaphoreSlim(MAX_RECEIVE, MAX_RECEIVE); - private readonly BufferManager bufferManager; - private readonly BlockingCollection outputQueue; - private readonly Filters filters; - private readonly IOutput output; - - public long PacketsObserved => this.packetsObserved; - public long PacketsCaptured => this.packetsCaptured; - - public SocketSniffer(NetworkInterfaceInfo nic, Filters filters, IOutput output) - { - this.outputQueue = new BlockingCollection(); - this.filters = filters; - this.output = output; - - this.bufferManager = new BufferManager(BUFFER_SIZE, MAX_RECEIVE); - this.receivePool = new ConcurrentStack(); - var endPoint = new IPEndPoint(nic.IPAddress, 0); - - // Capturing at the IP level is not supported on Linux - // https://github.com/dotnet/corefx/issues/25115 - // https://github.com/dotnet/corefx/issues/30197 - var protocolType = SystemInformation.IsWindows - ? ProtocolType.IP - : ProtocolType.Tcp; - - // IPv4 - this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, protocolType); - this.socket.Bind(endPoint); - this.socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); - - // Enter promiscuous mode on Windows only - if (SystemInformation.IsWindows) - { - EnterPromiscuousMode(); - } - } - - private void EnterPromiscuousMode() - { - try - { - this.socket.IOControl(IOControlCode.ReceiveAll, BitConverter.GetBytes(1), new byte[4]); - } - catch (Exception ex) - { - Console.WriteLine("Unable to enter promiscuous mode: {0}", ex); - throw; - } - } - - public void Start() - { - // Pre-allocate pool of SocketAsyncEventArgs for receive operations - for (var i = 0; i < MAX_RECEIVE; i++) - { - var socketEventArgs = new SocketAsyncEventArgs(); - socketEventArgs.Completed += (e, args) => Receive(socketEventArgs); - - // Allocate space from the single, shared buffer - this.bufferManager.AssignSegment(socketEventArgs); - - this.receivePool.Push(socketEventArgs); - } - - Task.Factory.StartNew(() => - { - // GetConsumingEnumerable() will wait when queue is empty, until CompleteAdding() is called - foreach (var timestampedData in this.outputQueue.GetConsumingEnumerable()) - { - Output(timestampedData); - } - }); - - Task.Factory.StartNew(StartReceiving); - } - - public void Stop() - { - this.isStopping = true; - } - - private void EnqueueOutput(TimestampedData timestampedData) - { - if (this.isStopping) - { - this.outputQueue.CompleteAdding(); - return; - } - - this.outputQueue.Add(timestampedData); - } - - private void Output(TimestampedData timestampedData) - { - // Only parse the packet header if we need to filter - if (this.filters.PropertyFilters.Any()) - { - var packet = new IPPacket(timestampedData.Data); - - if (!this.filters.IsMatch(packet)) - { - return; - } - } - - this.output.Output(timestampedData); - Interlocked.Increment(ref this.packetsCaptured); - } - - private void StartReceiving() - { - try - { - // Get SocketAsyncEventArgs from pool - this.maxReceiveEnforcer.Wait(); - - if (!this.receivePool.TryPop(out var socketEventArgs)) - { - // Because we are controlling access to pooled SocketAsyncEventArgs, this - // *should* never happen... - throw new Exception("Connection pool exhausted"); - } - - // Returns true if the operation will complete asynchronously, or false if it completed - // synchronously - var willRaiseEvent = this.socket.ReceiveAsync(socketEventArgs); - - if (!willRaiseEvent) - { - Receive(socketEventArgs); - } - } - catch (Exception ex) - { - // Exceptions while shutting down are expected - if (!this.isStopping) - { - Console.WriteLine(ex); - } - - this.socket.Close(); - this.socket = null; - } - } - - private void Receive(SocketAsyncEventArgs e) - { - // Start a new receive operation straight away, without waiting - StartReceiving(); - - try - { - if (e.SocketError != SocketError.Success) - { - if (!this.isStopping) - { - Console.WriteLine("Socket error: {0}", e.SocketError); - } - - return; - } - - if (e.BytesTransferred <= 0) - { - return; - } - - Interlocked.Increment(ref this.packetsObserved); - - // Copy the bytes received into a new buffer - var buffer = new byte[e.BytesTransferred]; - Buffer.BlockCopy(e.Buffer, e.Offset, buffer, 0, e.BytesTransferred); - - EnqueueOutput(new TimestampedData(DateTime.UtcNow, buffer)); - } - catch (SocketException ex) - { - Console.WriteLine("Socket error: {0}", ex); - } - catch (Exception ex) - { - Console.WriteLine("Error: {0}", ex); - } - finally - { - // Put the SocketAsyncEventArgs back into the pool - if (!this.isStopping && this.socket != null && this.socket.IsBound) - { - this.receivePool.Push(e); - this.maxReceiveEnforcer.Release(); - } - } - } - } -} diff --git a/src/SystemInformation.cs b/src/SystemInformation.cs deleted file mode 100644 index 02ac3c6..0000000 --- a/src/SystemInformation.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Runtime.InteropServices; -using System.Security.Principal; -using Mono.Unix.Native; - -namespace Snifter -{ - public static class SystemInformation - { - public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - public static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - - public static bool IsAdmin() - { - if (IsWindows) - { - return new WindowsPrincipal(WindowsIdentity.GetCurrent()) - .IsInRole(WindowsBuiltInRole.Administrator); - } - else - { - return Syscall.geteuid() == 0; - } - } - } -}