Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
## C++ library to receive network stats on MacOS (XNU Darwin)

## NOTE: A user (Sunil-P) reported that the socket used is gone in Big Sur, so this method will not work.

### Introduction

Darwin kernel provides an unpublished API to receive pseudo realtime notifications of network connections and stats. This is the same data that powers Activity Monitor. See [protocol.md](./docs/protocol.md) for details on the underlying mechanism and protocol. Feature summary:
Expand Down Expand Up @@ -41,5 +39,41 @@ XNU version:3789
bytes (tx/rx):926/17047 packets:5/314 wifi
```

### Kernel versions

The ntstat structs differ between XNU releases. One set of definitions per generation
lives in `src/ntstat_kernel_<xnu>.h`, with a matching handler in the `.cpp` next to it, and
`_loadStructHandler()` selects one based on the running kernel.

| header | XNU | MacOS |
| --- | --- | --- |
| ntstat_kernel_2422 | 2422 | 10.9 Mavericks |
| ntstat_kernel_2782 | 2782 | 10.10 Yosemite |
| ntstat_kernel_3248 | 3248 | 10.11 El Capitan |
| ntstat_kernel_3789 | 3789 | 10.12 Sierra |
| ntstat_kernel_4570 | 4570 | 10.13 High Sierra .. 10.15 Catalina |
| ntstat_kernel_7195 | 7195 | 11 Big Sur .. 26 Tahoe |

The offsets each handler reads are checked with `static_assert` in the handler `.cpp`, so a
mismatch between a struct definition and the layout it is meant to describe is a
compile error.

If a future release moves a field, the symptom is that no stream is reported at all, or
that stream addresses and process names are nonsense. To add support for it:

1. Take `bsd/net/ntstat.h` for the new tag from
[apple-oss-distributions/xnu](https://github.com/apple-oss-distributions/xnu) and print
its layouts with `clang -Xclang -fdump-record-layouts` (a handful of kernel-private
types have to be stubbed to compile it in userland).
2. Compare the offsets of the fields the handler reads - `local`, `remote`, `ifindex`,
`state`, `txwindow`, `txcwindow`, `pid`, `pname` - with the `static_assert` block of the
newest handler. Fields appended to the end of a descriptor do not matter.
3. If none of them moved, widen the version range in `_loadStructHandler()`.
4. If one of them moved, copy the newest `ntstat_kernel_*.{h,cpp}` pair to the new tag,
apply the struct differences, correct the `static_assert` values and add one branch to
`_loadStructHandler()`.
5. Confirm against the kernel: record a session with `enableRecording()`, then feed the
file to `replay <xnu> <file>` and check the streams it prints.

### Credits
This is based on lsock by Jonathan Levin (http://newosxbook.com/index.php?page=code). There were several significant changes to the socket protocol in 10.12 Sierra (XNU v3789) that breaks lsock. He said that an update to lsock is coming soon.
2 changes: 1 addition & 1 deletion replay/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ int main(int argc, const char * argv[])
xnuVersion = atoi(argv[1]);
filename = argv[2];

if (xnuVersion < 2000 || xnuVersion > 5000) { printf("xnuVersion\n"); exit(3); }
if (xnuVersion < 2000 || xnuVersion > 99999) { printf("xnuVersion\n"); exit(3); }

// create

Expand Down
5 changes: 4 additions & 1 deletion src/NetworkStatisticsClientImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ NTStatKernelStructHandler* NewNTStatKernel2782();
NTStatKernelStructHandler* NewNTStatKernel3789();
NTStatKernelStructHandler* NewNTStatKernel3248();
NTStatKernelStructHandler* NewNTStatKernel4570();
NTStatKernelStructHandler* NewNTStatKernel7195();

// minimum ntstat.h definitions needed here

Expand Down Expand Up @@ -260,7 +261,9 @@ class NetworkStatisticsClientImpl : public NetworkStatisticsClient, public MsgDe
{
printf("XNU version:%d\n", xnuVersion);

if (xnuVersion > 3800)
if (xnuVersion > 6200)
_structHandler = NewNTStatKernel7195();
else if (xnuVersion > 3800)
_structHandler = NewNTStatKernel4570();
else if (xnuVersion > 3300)
_structHandler = NewNTStatKernel3789();
Expand Down
261 changes: 261 additions & 0 deletions src/ntstat_kernel_7195.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@

#include "NTStatKernelStructHandler.hpp"

// definitions from darwin-xnu/bsd/net/ntstat.h kernel header

#include <uuid/uuid.h>

#include "ntstat_kernel_7195.h"

#include <stddef.h>
#include <string.h>
#include <vector>
#include <string>
using namespace std;

// The descriptor field offsets read below, as clang lays them out for Darwin.
// Unchanged from xnu-7195 through xnu-12377; later revisions append to the
// descriptor tails, which does not move any field read here.

static_assert(offsetof(nstat_tcp_descriptor, ifindex) == 72, "tcp ifindex");
static_assert(offsetof(nstat_tcp_descriptor, state) == 76, "tcp state");
static_assert(offsetof(nstat_tcp_descriptor, txwindow) == 100, "tcp txwindow");
static_assert(offsetof(nstat_tcp_descriptor, txcwindow)== 104, "tcp txcwindow");
static_assert(offsetof(nstat_tcp_descriptor, pid) == 116, "tcp pid");
static_assert(offsetof(nstat_tcp_descriptor, local) == 124, "tcp local");
static_assert(offsetof(nstat_tcp_descriptor, remote) == 152, "tcp remote");
static_assert(offsetof(nstat_tcp_descriptor, pname) == 196, "tcp pname");
static_assert(sizeof(nstat_tcp_descriptor) == 336, "tcp descriptor");

static_assert(offsetof(nstat_udp_descriptor, local) == 56, "udp local");
static_assert(offsetof(nstat_udp_descriptor, remote) == 84, "udp remote");
static_assert(offsetof(nstat_udp_descriptor, ifindex) == 112, "udp ifindex");
static_assert(offsetof(nstat_udp_descriptor, pid) == 128, "udp pid");
static_assert(offsetof(nstat_udp_descriptor, pname) == 132, "udp pname");
static_assert(sizeof(nstat_udp_descriptor) == 272, "udp descriptor");

static_assert(sizeof(nstat_counts) == 112, "counts");
static_assert(sizeof(nstat_msg_src_description) == 40, "src desc message");

class NTStatKernel7195 : public NTStatKernelStructHandler
{
public:

virtual bool isProviderTcp(uint64_t providerId){
return (NSTAT_PROVIDER_TCP_KERNEL == providerId)||(NSTAT_PROVIDER_TCP_USERLAND);}

virtual bool isProviderUdp(uint64_t providerId) {
return (NSTAT_PROVIDER_UDP_KERNEL == providerId)||(NSTAT_PROVIDER_UDP_USERLAND);}

virtual bool isProviderInterface(uint64_t providerId) { return NSTAT_PROVIDER_IFNET == providerId; }

//--------------------------------------------------------------------
// write GET_SRC_DESC message to dest
//--------------------------------------------------------------------
virtual void writeSrcDesc(MsgDest &dest, uint64_t providerId, uint64_t srcRef )
{
nstat_msg_get_src_description msg = nstat_msg_get_src_description();

NTSTAT_MSG_HDR(msg, dest, NSTAT_MSG_TYPE_GET_SRC_DESC);

msg.srcref = srcRef;

dest.send(&msg.hdr, sizeof(msg));
}

//--------------------------------------------------------------------
// write QUERY_SRC message to dest
//--------------------------------------------------------------------
virtual void writeQuerySrc(MsgDest &dest, uint64_t srcRef)
{
nstat_msg_query_src_req msg = nstat_msg_query_src_req();

NTSTAT_MSG_HDR(msg, dest, NSTAT_MSG_TYPE_QUERY_SRC);

msg.srcref= srcRef;

dest.send(&msg.hdr, sizeof(msg));
}

//--------------------------------------------------------------------
// write ADD_ADD_SRCS message to dest
//--------------------------------------------------------------------
virtual void writeAddAllSrc(MsgDest &dest, uint32_t providerId)
{
nstat_msg_add_all_srcs msg = nstat_msg_add_all_srcs();

NTSTAT_MSG_HDR(msg, dest, NSTAT_MSG_TYPE_ADD_ALL_SRCS);

msg.provider = providerId ;

dest.send(&msg.hdr, sizeof(msg));
}

// xnu-3789 is first time we see split _KERNEL and _USERLAND

virtual void writeAddAllTcpSrc(MsgDest &dest) {
writeAddAllSrc(dest, NSTAT_PROVIDER_TCP_KERNEL);
//writeAddAllSrc(dest, NSTAT_PROVIDER_TCP_USERLAND);
}

virtual void writeAddAllUdpSrc(MsgDest &dest) {
writeAddAllSrc(dest, NSTAT_PROVIDER_UDP_KERNEL);
//writeAddAllSrc(dest, NSTAT_PROVIDER_UDP_USERLAND);
}

virtual void writeAddAllInterfaces(MsgDest &dest) {
writeAddAllSrc(dest, NSTAT_PROVIDER_IFNET);
}


//--------------------------------------------------------------------
// extract srcRef, providerId (if possible) from message
//--------------------------------------------------------------------
virtual void getSrcRef(nstat_msg_hdr* msg, int structlen, uint64_t &srcRef, uint32_t &providerId) {
switch(msg->type)
{
case NSTAT_MSG_TYPE_SRC_COUNTS:
srcRef = ((nstat_msg_src_counts*)msg)->srcref;
break;
case NSTAT_MSG_TYPE_SRC_DESC:
srcRef = ((nstat_msg_src_description*)msg)->srcref;
providerId = ((nstat_msg_src_description*)msg)->provider;
break;
case NSTAT_MSG_TYPE_SRC_ADDED:
srcRef = ((nstat_msg_src_added*)msg)->srcref;
providerId = ((nstat_msg_src_added*)msg)->provider;
break;
case NSTAT_MSG_TYPE_SRC_REMOVED:
srcRef = ((nstat_msg_src_removed*)msg)->srcref;
break;
default:
//printf("E getSrcRef not implemented for type %d\n", msg->type);
break;
}
}


//--------------------------------------------------------------------
// populate dest with message data
//--------------------------------------------------------------------
virtual bool readSrcDesc(nstat_msg_hdr*hdr, int structlen, NTStatStream* dest )
{
nstat_msg_src_description *msg = (nstat_msg_src_description*)hdr;
if (msg->provider == NSTAT_PROVIDER_TCP_KERNEL || msg->provider == NSTAT_PROVIDER_TCP_USERLAND) {
readTcpSrcDesc(hdr, structlen, dest);
} else if (msg->provider == NSTAT_PROVIDER_UDP_KERNEL || msg->provider == NSTAT_PROVIDER_UDP_USERLAND) {
readUdpSrcDesc(hdr, structlen, dest);
} else {
// ??
}
return true;
}


//--------------------------------------------------------------------
// populate dest with message ifnet data
//--------------------------------------------------------------------
/*
virtual bool readSrcDesc(nstat_msg_hdr* hdr, int structlen, NTStatInterface* dest )
{
nstat_msg_src_description *msg = (nstat_msg_src_description*)hdr;
if (msg->provider != NSTAT_PROVIDER_IFNET) return false;

nstat_ifnet_descriptor* ifnet = (nstat_ifnet_descriptor*)msg->data;
dest->name = string(ifnet->name);
dest->description = string(ifnet->description);
dest->ifindex = ifnet->ifindex;
dest->type = ifnet->type;

return false;
}*/

//--------------------------------------------------------------------
// TCP: populate dest with message data
//--------------------------------------------------------------------
virtual void readTcpSrcDesc(nstat_msg_hdr*hdr, int structlen, NTStatStream* dest )
{
nstat_msg_src_description *msg = (nstat_msg_src_description*)hdr;
nstat_tcp_descriptor*tcp = (nstat_tcp_descriptor*)msg->data;

dest->key.ifindex = tcp->ifindex;
dest->key.ipproto = IPPROTO_TCP;
dest->key.isV6 = (tcp->local.v4.sin_family == AF_INET6);

if (tcp->local.v4.sin_family == AF_INET6)
{
dest->key.lport = tcp->local.v6.sin6_port;
dest->key.local.addr6 = tcp->local.v6.sin6_addr;
dest->key.rport = tcp->remote.v6.sin6_port;
dest->key.remote.addr6 = tcp->remote.v6.sin6_addr;
} else {
dest->key.lport = tcp->local.v4.sin_port;
dest->key.rport = tcp->remote.v4.sin_port;
dest->key.local.addr4 = tcp->local.v4.sin_addr;
dest->key.remote.addr4 = tcp->remote.v4.sin_addr;
}
dest->states.txwindow = tcp->txwindow;
dest->states.txcwindow = tcp->txcwindow;
dest->states.state = tcp->state;

dest->process.pid = tcp->pid;

strcpy(dest->process.name, ((tcp->pid > 0 && tcp->pname[0]) ? tcp->pname : ""));
}

//--------------------------------------------------------------------
// UDP: populate dest with message data
//--------------------------------------------------------------------
virtual void readUdpSrcDesc(nstat_msg_hdr*hdr, int structlen, NTStatStream* dest )
{
nstat_msg_src_description *msg = (nstat_msg_src_description*)hdr;
nstat_udp_descriptor*udp = (nstat_udp_descriptor*)msg->data;

dest->key.ifindex = udp->ifindex;
dest->key.ipproto = IPPROTO_UDP;
dest->key.isV6 = (udp->local.v4.sin_family == AF_INET6);

if (udp->local.v4.sin_family == AF_INET6)
{
dest->key.lport = udp->local.v6.sin6_port;
dest->key.local.addr6 = udp->local.v6.sin6_addr;
dest->key.rport = udp->remote.v6.sin6_port;
dest->key.remote.addr6 = udp->remote.v6.sin6_addr;
} else {
dest->key.lport = udp->local.v4.sin_port;
dest->key.rport = udp->remote.v4.sin_port;
dest->key.local.addr4 = udp->local.v4.sin_addr;
dest->key.remote.addr4 = udp->remote.v4.sin_addr;
}

dest->process.pid = udp->pid;
strcpy(dest->process.name, ((udp->pid > 0 && udp->pname[0]) ? udp->pname : ""));
}

//--------------------------------------------------------------------
// populate dest with message counts data
//--------------------------------------------------------------------
virtual void readCounts(nstat_msg_hdr*hdr, int structlen, NTStatCounters& dest )
{
nstat_msg_src_counts *msg = (nstat_msg_src_counts*)hdr;
dest.rxbytes = msg->counts.nstat_rxbytes;
dest.txbytes = msg->counts.nstat_txbytes;
dest.rxpackets = msg->counts.nstat_rxpackets;
dest.txpackets = msg->counts.nstat_txpackets;

dest.cell_rxbytes = msg->counts.nstat_cell_rxbytes;
dest.cell_txbytes = msg->counts.nstat_cell_txbytes;

dest.wifi_rxbytes = msg->counts.nstat_wifi_rxbytes;
dest.wifi_txbytes = msg->counts.nstat_wifi_txbytes;

dest.wired_rxbytes = msg->counts.nstat_wired_rxbytes;
dest.wired_txbytes = msg->counts.nstat_wired_txbytes;
}

};


NTStatKernelStructHandler* NewNTStatKernel7195() {
return new NTStatKernel7195();
}
Loading