Warning: PackageEtherCapture is a 0.x release. The API and resulting data structure are very unstable. If you use this package, we recommend using a specific minor release and not automatically upgrading without testing.
**Warning2: Most of the "API" for PackageEtherCapture is the data structure returned by Frame.init(data: Data). This data structure consists of nested structs and enumerations which are constantly being added to as more protocol decodes are added. This means that even a minor version update of PackageEtherCapture can cause your code which uses the Frame() data structure to require updates. We recommend setting a specific package version (preferably a "release" and only updating when you have development time to handle (or ignore) new protocol decodes in your switch statements.
PackageEtherCapture is a Swift Package with two sets of functionality:
- Capturing frames from the network (wrapping the C-based libpcap library)
- Decoding network frames into a Swift data structure.
Capturing packets requires read access to /dev/bpf* (the berkely packet filter device files). Most MacOS users who "can administer their computers" are in the admin group, so we recommend the following commmand:
sudo chmod g+r /dev/bpf*
If you already installed Wireshark on your system, the installation process may have already created an "access_bpf" group, granted it rw access to /dev/bpf*, and added you to that group. If that is already done, no further change is necessary.
Access to /dev/bpf* is not available on iOS. Capturing packets from the wire is only supported for MacOS.
Access to /dev/bpf* is not possible for sandboxed apps. Capturing packets in an app in the MacOS App store is not possible.
Using PackageEtherCapture to create Frame data structures from data obtained by other means (such as from a .pcap or .pcapng import) does not require priviledged access and is possible within an iOS or sandboxed app.
PackageEtherCapture uses Apple's swift-log API for logging. See https://github.com/apple/swift-log Bootstrapping the logging system is not required to use PackageEtherCapture. The Logger label is "net.networkmom.PackageEtherCapture".
Here is the API for initiating a capture:
public init(interface: String, count: Int32 = 0, command: String, snaplen: Int = 96, promiscuous: Bool = true, _ callback: @escaping (Frame) -> Void) throws {
Where "command" is a String with an embedded libpcap filter (the same as used by tcpdump and Wireshark). See https://www.tcpdump.org/manpages/pcap-filter.7.html
Assuming the initializer does not throw an error, the closure passed into the call will be delivered a Frame data structure each time a Frame is captured.
PackageEtherCaptureDemo https://github.com/darrellroot/PackageEtherCaptureDemo demonstrates the simplest possible capture:
import Foundation
import PackageEtherCapture
let etherCapture: EtherCapture?
do {
etherCapture = try EtherCapture(interface: "en0", command: "icmp or icmp6") { frame in
//This closure is called every time a frame is captured. The magic is in the frame data structure
debugPrint(frame.description)
}
} catch {
print("EtherCapture initialization failed with error \(error)")
}
RunLoop.current.run()
Another way to use PackageEtherCapture is to pass in captured network traffic (one frame at a time) to create Frame data structures. Here's an example from the CLI version of etherdump https://github.com/darrellroot/etherdump, passing in data obtained from a .pcapng file:
for (count,packet) in packetBlocks.enumerated() {
let frame = Frame(data: packet.packetData, originalLength = packet.packetData.count, frameNumber = count)
displayFrame(frame: frame, packetCount: Int32(count), arguments: arguments)
}
Here's the Frame initializer: public init(data: Data, timeval: timeval = timeval(), originalLength: Int, frameNumber: Int? = nil) {
The Frame initializer does not currently fail, but if it is unable to decode a valid frame it will set the frameFormat to .invalid and layer3 contents to .unknown. The raw data will still be available in the data structure.
The originalLength is just there to support .pcap file export, and is not used for anything.
The frameNumber makes it easier to display a numbered list of frames, especially after filtering. If you do not specify a frameNumber, The Frame() API will count for you (but the numbering does not reset if you start a second import/capture).
PackageEtherCapture uses "layers of encapsulation". The initial layer is called layer-2 and is currently an Ethernet Frame. Anything encapsulated in that Frame is called "layer 3". That is usually IPv4 or IPv6, but it could be LLDP, STP, or CDP. Those are (in an OSI model sense) layer-2 protocols, but they are also "application data" encoded inside a frame. Because they are encapsulated inside the Layer-2 frame, PackageEtherCapture puts them at layer-3 of the returned data structure.
The following Layer-2 decodes are currently supported:
- Ethernet-II frame
- 802.3 frame (with optional support for 802.2 SNAP header)
- Invalid
The following Layer-3 decodes are currently supported:
- IPv4 Header
- IPv6 Header
- BPDU (spanning-tree bridge protocol data unit)
- CDP (Cisco Discovery Protocol)
- Unknown
The following Layer-4 decodes are currently supported:
- UDP Datagram Header
- TCP Segment Header
- Unknown
We hope and expect many additional decodes to be added, plus additional layers for application data encoded inside the UDP datagrams or TCP segments.
Here is an overview of the data structure hierarchy:
Frame.layer3
IPv4
IPv6
BPDU
CDP
Unknown
IPv4.layer4 and IPv6.layer4:
Tcp
Udp
Unknown
Frame also has a .layer4 computed property which conveniently returns the layer4 contents
This protocol includes conveniently available computed properties for displaying capture data. Every structure in the Frame hierarchy should comply.
public protocol EtherDisplay {
var description: String { get }
var verboseDescription: String { get }
var hexdump: String { get }
}
public struct Frame: CustomStringConvertible, EtherDisplay, Identifiable, Codable {
public init(data: Data, timeval: timeval = timeval()) {
public let id = UUID()
public let date: Date // pcap timestamp of packet capture
public let srcmac: String
public let dstmac: String
public var frameFormat: FrameFormat
public var ieeeLength: UInt? = nil //802.2 802.3 encapsulation
public var ieeeDsap: UInt8? = nil
public var ieeeSsap: UInt8? = nil
public var ieeeControl: UInt8? = nil
public var snapOrg: UInt? = nil //802.2 SNAP header
public var snapType: UInt? = nil //802.2 SNAP header
public var ethertype: UInt? = nil // ethernetII encapsulation
/**
- Parameter layer3: Nested data structure with higher layer information
*/
public var layer3: Layer3 = .unknown(Unknown.completely)
public var layer4: Layer4? {
public let data: Data // total frame contents
public enum Layer3: CustomStringConvertible, EtherDisplay, Codable {
case ipv4(IPv4)
case ipv6(IPv6)
case bpdu(Bpdu)
case cdp(Cdp)
case unknown(Unknown)
public struct IPv4: CustomStringConvertible, EtherDisplay, Codable {
public let sourceIP: IPv4Address
public let destinationIP: IPv4Address
public let data: Data
public let version: UInt8
public let ihl: UInt8 // 4 times IHL field
public let dscp: UInt8
public let ecn: UInt8
public let totalLength: UInt
public let identification: UInt
public let evilBit: Bool
public let dontFragmentFlag: Bool
public let moreFragmentsFlag: Bool
public let fragmentOffset: UInt
public let ttl: UInt8
public let ipProtocol: UInt8
public let headerChecksum: UInt
public let options: Data?
//public let payload: Data?
/**
- Parameter layer4: Nested data structure with higher layer information
*/
public var layer4: Layer4 = .unknown(Unknown.completely)
public struct IPv6: EtherDisplay, Codable {
public let data: Data
public let version: UInt8
public let trafficClass: UInt8
public let flowLabel: UInt
public let payloadLength: UInt
public let nextHeader: UInt8
public let hopLimit: UInt8
public let sourceIP: IPv6Address
public let destinationIP: IPv6Address
public let layer4: Layer4
public enum Layer4: CustomStringConvertible, EtherDisplay, Codable {
case tcp(Tcp)
case udp(Udp)
case unknown(Unknown)
public struct Tcp: EtherDisplay, Codable {
public let data: Data
public let sourcePort: UInt
public let destinationPort: UInt
public let sequenceNumber: UInt
public let acknowledgementNumber: UInt
public let dataOffset: UInt8
public let urg: Bool
public let ack: Bool
public let psh: Bool
public let rst: Bool
public let syn: Bool
public let fin: Bool
public let window: UInt
public let checksum: UInt
public let urgentPointer: UInt
public let options: Data?
public let payload: Data
public struct Udp: EtherDisplay, Codable {
public let data: Data
public let payload: Data
public let sourcePort: UInt
public let destinationPort: UInt
public let length: UInt
public let checksum: UInt
- Create new public Swift File/Struct in DataStructure directory (Lldp.swift)
- Make the struct conform to CustomStringConvertible and EtherDisplay. Create an init(data: Data) initializer
- Add the struct as a case in the Layer3 enumeration, including all description/hexdump switches
- Add the new case to the Frame.layer4 switch
- In Frame.init(), call the Lldp() initializer in the appropriate part of the switch (usually based on ethertype)
- Get a wireshark capture of a frame, right-click on the frame and select "copy as hex stream"
- Use your hex stream to generate a unit test case. Without a test case you WILL have a bug!
- Make sure everything needed by other Packages and applicaitons (such as Etherdump) is set to public
- Commit, tag with a minor version number change, and push with tags to github
- Update to latest package versions to get your new PackageEtherCapture version
- Create new layer3 detail view (LldpDetailView in our example)
- Edit Layer3DetailView switch to call your new detail view. The compiler will show you where (switch must be exhaustive)
At this time we do not have a layer-5 structure for application-level data, but we anticipate that in the future.
PackageEtherCaptureDemo is a minimial sample CLI application which uses PackageEtherCapture with under 20 lines of code!
PackageSwiftPcapng can read pcapng and pcap files. The packet data read can be sent into the PackageEtherCapture Frame() initializer.
etherdump is a fully featured CLI packet capture tool which uses PackageEtherCapture
Etherdump is a GUI packet capture tool which uses PackageEtherCapture