macOS 内核之网络信息抓包(一)

Oct 31, 2019 at 11:09:18

macOS 内核之网络信息抓包(一)

可以说这个世界有了网络之后,重新了计算机。网络是目前所有 PC 和手机设备不可或缺的东西。同时飞速发展的互联网行业也让这一层的技术更迭迅速,衍生出无数计算机网络技术。

由于涉及的概念和技术点太多,所以一时半会我也不知从何学起,看到 Activity Monitor.app 的 Network 一项系统能够统计的数据挺多的,不如就试试做拿跟他一样的信息看看。

讲道理我们的 App 和系统自带的 App 都是跑在用户空间的,大家用的 API 也差不多,他能做到我们也能做到对吧。

事实证明我还是太天真了😂。

〇、计算机网络背景知识

有学过计算机网络的朋友应该都听说过 OSI Model(Open Systems Interconnection model),把计算机网络分为七层:

# Layer
7 Application (应用层, HTTP)
6 Presentation (表现层, HTTP)
5 Session (会话层, HTTP)
4 Transport (传输层, TCP)
3 Network (网络层, IP)
2 Data link (链路层, Frames)
1 Physical (物理层,Bits)

这是 ISO 提出的逻辑分层标准,好处是分层隔离之后,各层的技术自行更新时不会影响到其他层的逻辑,比如最底层的 Physical Layer (物理层)发展到现在的万兆光纤,它只需要关心 Bits 怎么传输就行,上层的逻辑几乎不需要更新。

但是人们实现这个分层标准的时候也并不完全按照分层来,比如最上面的几层,应用层(Application Layer)提供面向用户的协议比如 HTTP,其中数据压缩本来是表现层(Presentation Layer)的事情但是 HTTP 支持 Compression。然后 TLS/SSL 在传输层但是它支持加解密。

实际上 TCP/IP Model (Internet protocol suite) 的四层模型比 OSI 七层简化了一些,也相对比较贴近大家的使用习惯。

# Layer
4 Application Layer (应用层, HTTP/ IMAP…)
3 Transport Layer (传输层, TCP/UDP…)
2 Internet Layer (网络层, IP/ICMP…)
1 Link Layer (链路层, MAC/PPP…)

以 OSI 七层模型来看,XNU 内核负责的主要是第 2 到第 5 层, TCP/IP 模型则是 1 到 3 层(我们熟悉的 URLSession 是上层提供的,不在内核实现)。

第 2 层里 XNU 提供了网络相关的 interface。如果在终端运行 ifconfig 的话大家会看到一堆信息,以 en0, lo0 开头的。这些是 device interface names,对应了物理或者虚拟网卡,这些设备不在 /dev 里表现,用户空间如果要访问它们就必须通过 Unix domain socket 进行通信(有别于 IP socket,下文将有描述)。

所以如果我们要统计一台机器的网络流量,我们可以通过获取主要网卡的流量信息来解决。

一、统计网卡收发包信息 sysctl()

开源的系统监控软件 GKrellM 项目在 macOS 上的实现就是通过 sysctl() 获取网卡数据来统计网络流量,实现入口在 src/sysdeps/bsd-common.c 里的 void gkrellm_sys_net_read_data(void) 函数。

我们在本 macOS 内核系列的第一篇有提到过利用 sysctl() 函数可以从内核获取很多有用的系统信息,同时系统也提供了 sysctl 命令可以在终端运行。sysctl 基本上是所有类 Unix 系统的标准命令之一。在 XNU 内核中,sysctl以及网络相关的接口由 BSD 内核实现。

另一个非常常见的命令是 ifconfig,运行它可以获取我们所有网卡(network interface)信息。ifconfig 的代码是开源的可以在这里找到。

系统内核会维护一份以树形 MIB (management information base)形式存储的数据,里面包含了硬件信息、网络统计信息等一大堆数据,sysctl 接口会读取 MIB 数据然后返回。我们也可以通过别的接口来获取这些数据(下文将有介绍),但是 sysctl 接口很方便也很快。

sysctl的 MIB 存储划分为多种类型,内存 vm, 网络 net, 硬件 hw 之类的。可以通过 sysctl -A 命令打出来。

sysctl 不仅可以读数据,也可以写数据。该函数原型 XNU 没有注释,我们(可以参考这里)在 Linux 上的定义:

int sysctl (int *name,
            int nlen, 
            void *oldval,
            size_t *oldlenp,
            void *newval, 
            size_t newlen);
  • name: 一个整数的数组,里面是查询参数
  • nlen: 第一个参数里有多少个整数
  • oldval: 存储的数据通过这个指针返回,有可能为 NULL
  • oldlenp: 存储的数据的长度
  • newval: 用该参数写入新数据到 MIB,传 NULL 则不修改
  • newlen: 新数据的长度

在 GKrellM 里获取网卡信息的实现分为两步,第一步先取数据长度 oldlenp:

static int  mib_net[] = { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0 };
static char *buf;
static int  alloc;
size_t          needed;

if (sysctl(mib_net, 6, NULL, &needed, NULL, 0) < 0)
return;

第二步,取到长度之后分配一个足够长的内存然后正式读数据:

if (alloc < needed)
{
    if (buf != NULL)
        free(buf);
    buf = malloc(needed);
    if (buf == NULL)
        return;
    alloc = needed;
}

if (sysctl(mib_net, 6, buf, &needed, NULL, 0) < 0)
return;

net 前缀在宏定义里是 CTL_NET

PF_ROUTE 是路由表相关的操作。前缀 PF_ 是 Protocol Family 的意思,对应的还有 AF_ Address Family。在 XNU 里,PF_AF_ 的定义是完全一样的(Linux 也是)。

1.1 PF 和 AF

前面说跟 interface 打交道得通过 Unix domain socket(跟 IP socket 稍有不同),要创建 一个 Unix domain socket,第一个参数就是 Protocol Famil。我们知道 XNU 包含了 Mach 内核和 FreeBSD 内核,它本身最常用的 IPC 方式是 Mach 内核提供的 Mach Port 方式,BSD 提供的这种 socket 方式其实比较少见。

BSD 中创建 socket 使用 socket() 函数:

int socket  (int family, int type, int protocol);

第一个参数是 family,指的其实是 Protocol Family,也就是 PF_ 开头的参数,但实际上我们可以用 AF_ 来代替,这是一个历史遗留产物。在的书中提到:

(This PF_INET thing is a close relative of the AF_INET that you can use when initializing the sin_family field in your struct sockaddr_in. In fact, they’re so closely related that they actually have the same value, and many programmers will call socket() and pass AF_INET as the first argument instead of PF_INET. Now, get some milk and cookies, because it’s time for a story. Once upon a time, a long time ago, it was thought that maybe an address family (what the “AF” in “AF_INET” stands for) might support several protocols that were referred to by their protocol family (what the “PF” in “PF_INET” stands for). That didn’t happen. And they all lived happily ever after, The End. So the most correct thing to do is to use AF_INET in your struct sockaddr_in and PF_INET in your call to socket().)

大意是说以前大家曾经试图在 socket 上抽象出一个 Protocol Family 的概念,允许一个 Address Family 支持多种协议。但是这件事情一直没人实现过😂,所以遗留了这么个东西。Unix 和 Linux 的定义都是直接把 PF_ 开头的宏定义为同名的 AF_ 宏。

第二个参数是 socket 类型:

/*
 * Types
 */
#define    SOCK_STREAM 1       /* stream socket */
#define    SOCK_DGRAM  2       /* datagram socket */
#define    SOCK_RAW    3       /* raw-protocol interface */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define    SOCK_RDM    4       /* reliably-delivered message */
#endif /* (!_POSIX_C_SOURCE || _DARWIN_C_SOURCE) */
#define    SOCK_SEQPACKET  5       /* sequenced packet stream */

第三个是协议类型,比如 UDP, TCP:

// bsd/netinet/in.h

#define IPPROTO_UDP 17 /* user datagram protocol /
#define IPPROTO_TCP 6 /
tcp */

bsd/netinet/in.h 里还定义了上百个,我已放弃学习🤦‍♂️。

在 IPv4 网络中,第一个参数我们传 PF_INET,IP 地址会保存在 sockaddr_in 结构体中:

struct sockaddr_in {
    short   sin_family;
    u_short sin_port;
    struct  in_addr sin_addr;
    char    sin_zero[8];
};

IPv6 则是 PF_INET6,XNU 的相关定义在 bsd/netinet/in.h

1.2 PF_ROUTE

PF_ROUTE 获取的是系统路由表相关的信息,XNU 没什么文档,但是这是一个 BSD 标准,所以我们可以参考 NetBSD 关于网络的文档。BSD 中关于路由表的实现分为三个部分,以 Radix Tree (基数树)存储的数据库 net/radix.c,提供查询和修改接口的 net/route.c,以及提供给上层的 socket 接口 net/rtsock.c。系统的 route(8) 命令有用到 PF_ROUTE,可以到 Apple Open Source 找到源码

在用户空间,我们和路由表的交互都是通过 protocol family 为 PF_ROUTE 的 socket 来跟 network interface 通信的。

BSD 的 Network Routing 层负责转发数据包 packet 到目标网关,涉及到 ARP 解析(也就是 IP 地址与 Mac 地址的映射)。比如说一个 TCP/IP 协议的包到了路由这一层,就会根据 IP 地址寻找到目标网卡,把包发过去,比如发到 WiFi 网卡。所以我们可以通过路由这一层获得某一个网卡上所有的收发包数据,从而实现流量监控。

我们通过 sysctl() 接口获取信息的时候,这个 socket 是由内核创建的,我们只需要传参数就行。可以参考 FreeBSD 关于 sysctl(3) 的文档

1.3 NET_RT_IFLIST

static int  mib_net[] = { CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0 };

留意到这里其实传了六个参数,CTL_NETPF_ROUTE 已经解释过了。第三参数 0 是 hardcoded 的,以前留给 Protocol Family 的。第四个是 Address Family,这里填 0 可以表示获取所有 Family。第五个和第六个是有关联的,具体参考 FreeBSD 文档,我们只要知道传 NET_RT_IFLIST 时后面一个传 0。

最近阅读内核代码,碰到这种有历史的 C 接口感觉都非常依赖文档,如果没有文档几乎寸步难行。T_T

The NET_RT_IFLISTL is like NET_RT_IFLIST, just returning message
header structs with additional fields allowing the interface to
be extended without breaking binary compatibility.

The NET_RT_IFLISTL uses 'l' versions of the message header struc-
tures: struct if_msghdrl and struct ifa_msghdrl.

根据文档,NET_RT_IFLIST 会返回 message header structs,用的是这个结构体 if_msghdr

1.4 if_msghdr

struct if_msghdr {
     u_short ifm_msglen;        /* to skip over non-understood messages */
     u_char  ifm_version;       /* future binary compatibility */
     u_char  ifm_type;      /* message type */
     int     ifm_addrs;     /* like rtm_addrs */
     int     ifm_flags;     /* value of if_flags */
     u_short ifm_index;     /* index for associated ifp */
     struct  if_data ifm_data;  /* statistics and other data about if */
 };

sysctl 返回的是一个数组,包含多个 if_msghdr 结构体,ifm_msglen 用于指针偏移量。我们可以通过一个循环来取每个 message header。

struct if_msghdr *ifmsg = (struct if_msghdr *)currentData;
if (ifmsg->ifm_type != RTM_IFINFO) {
    currentData += ifmsg->ifm_msglen;
    continue;
}

这里只关心 RTM_IFINFO 这种类型,相关定义还有十几个,在 bsd/net/route.hRTM_ 开头的宏。

if (ifmsg->ifm_flags & IFF_LOOPBACK) {
    currentData += ifmsg->ifm_msglen;
    continue;
}

我们只关心真正和互联网通信的 interface,所以过滤本地 loopback 网络。这里我们可以简单理解包含了 localhost 的特殊网卡(可以参考这里),如果你在终端运行 ifconfig 看到 lo 开头的就是 loopback interface。

struct sockaddr_dl *sdl = (struct sockaddr_dl *)(ifmsg + 1);
if (sdl->sdl_family != AF_LINK) {
    currentData += ifmsg->ifm_msglen;
    continue;
}

ifmsg 这个 if_msghdr + 1 我们得到 Header 之后的内存地址,也就是 sockaddr_dl 数据,这个数据是 Link-Level sockaddr。我们先取 sdl_family,如果是 AF_LINK 就说明我们的结构体取对了。这里取得 sockaddr_dl 之后, sdl_data 的前 sdl_nlen 长度的数据就是他的名字,后面的是 ll address。

/*
 * Structure of a Link-Level sockaddr:
 */
struct sockaddr_dl {
    u_char  sdl_len;        /* Total length of sockaddr */
    u_char  sdl_family;     /* AF_LINK */
    u_short sdl_index;      /* if != 0, system given index for interface */
    u_char  sdl_type;       /* interface type */
    u_char  sdl_nlen;       /* interface name length, no trailing 0 reqd. */
    u_char  sdl_alen;       /* link level address length */
    u_char  sdl_slen;       /* link layer selector length */
    char    sdl_data[12];   /* minimum work area, can be larger;
                             *  contains both if name and ll address */
#ifndef __APPLE__
    /* For TokenRing */
    u_short sdl_rcf;        /* source routing control */
    u_short sdl_route[16];  /* source routing information */
#endif
};

我们直接读 sdl_datasdl_nlen 这么长的数据,得到 interface name:

NSString *interfaceName = [[NSString alloc] initWithBytes:sdl->sdl_data length:sdl->sdl_nlen encoding:NSASCIIStringEncoding];

接下来检查这个 interface 有没有在跑:

if (ifmsg->ifm_flags & IFF_UP)

然后就可以读 ifmsgif_data 数据了:

/*
 * Structure describing information about an interface
 * which may be of interest to management entities.
 */
struct if_data {
    /* generic interface information */
    u_char          ifi_type;       /* ethernet, tokenring, etc */
    u_char          ifi_typelen;    /* Length of frame type id */
    u_char          ifi_physical;   /* e.g., AUI, Thinnet, 10base-T, etc */
    u_char          ifi_addrlen;    /* media address length */
    u_char          ifi_hdrlen;     /* media header length */
    u_char          ifi_recvquota;  /* polling quota for receive intrs */
    u_char          ifi_xmitquota;  /* polling quota for xmit intrs */
    u_char          ifi_unused1;    /* for future use */
    u_int32_t       ifi_mtu;        /* maximum transmission unit */
    u_int32_t       ifi_metric;     /* routing metric (external only) */
    u_int32_t       ifi_baudrate;   /* linespeed */
    /* volatile statistics */
    u_int32_t       ifi_ipackets;   /* packets received on interface */
    u_int32_t       ifi_ierrors;    /* input errors on interface */
    u_int32_t       ifi_opackets;   /* packets sent on interface */
    u_int32_t       ifi_oerrors;    /* output errors on interface */
    u_int32_t       ifi_collisions; /* collisions on csma interfaces */
    u_int32_t       ifi_ibytes;     /* total number of octets received */
    u_int32_t       ifi_obytes;     /* total number of octets sent */
    u_int32_t       ifi_imcasts;    /* packets received via multicast */
    u_int32_t       ifi_omcasts;    /* packets sent via multicast */
    u_int32_t       ifi_iqdrops;    /* dropped on input, this interface */
    u_int32_t       ifi_noproto;    /* destined for unsupported protocol */
    u_int32_t       ifi_recvtiming; /* usec spent receiving when timing */
    u_int32_t       ifi_xmittiming; /* usec spent xmitting when timing */
    struct IF_DATA_TIMEVAL ifi_lastchange;  /* time of last administrative change */
    u_int32_t       ifi_unused2;    /* used to be the default_proto */
    u_int32_t       ifi_hwassist;   /* HW offload capabilities */
    u_int32_t       ifi_reserved1;  /* for future use */
    u_int32_t       ifi_reserved2;  /* for future use */
};

我们只统计流量所以只关心这两个数值:

u_int32_t       ifi_ibytes;     /* total number of octets received */
u_int32_t       ifi_obytes;     /* total number of octets sent */

获取 CPU 信息的原理差不多,上面的数据是一个累计数值,但是我们要计算的是一个瞬时速率,所以得获取两次数据作比较。

1.5 ifi_bytes 溢出

这里 ifi_ibytesifi_obytes 使用 u_int32_t 存的,但是内核在计算这个数值的时候会一直累加,也就是说这个数据会 overflow (溢出)。计数增长的方法在 XNU 源码的 bsd/net/kip_interface.c 里面:

if (s->bytes_in != 0)
        atomic_add_64(&ifp->if_data.ifi_ibytes, s->bytes_in);

所以如果我们要计算数据累加量的话,要自己处理这个 u_int32_t 的大小变化,如果发现保存的上一次的 ifi_ibytes 大于新的数值,说明新的数值已经溢出变小了。

P.S. 所有的网络监控软件都无法统计到历史数据,只能统计他开始监控那一刻起的数据。系统内核因为是第一个启动的,所以它能统计到的数据一定比我们多。

1.6 Interface Naming

以上的处理是针对非 PPP 连接的 interface 的数据处理,PPP interface 比较麻烦,需要自建 socket 跟 interface 通信。在开始 PPP 连接处理之前,我们先岔开看看 interface naming。

留意到在 macOS 上运行 ifconfig 和在 Linux 上看到的 interface 命名规则有点不同:

# macOS
lo0: …
gif0: …
stf0: …
en0: …
en1: …
bridge0: …
p2p0: …
awdl0: …
llw0: …
utun0: …
utun1: …

Ubuntu

eth0: …
lo0: …

interface 命名规则是由操作系统自己实现的,BSD 和 Linux 各有自己的规则。早期的 Linux 系统会只有 eth[0123…],根据内核启动时发现这些硬件的序号来命名。后来才加了 Consistent Network Device Naming feature。

在 Unix 系统上,这些 interface 会根据不同的类别有不同的前缀,《Mac OS X and iOS Internals》这本书的 Chap 17,Layer II: INTERFACES 对此命名规则有过介绍。大家可以参考看看。

主要分为两大类,一类是 XNU 原生支持的 interfaces,比如 bridgelo。另一类是通过 Kernel Extension 支持的 interfaces,比如 enppp

en 的支持在 IONetworkingFamily kext 里,对应的是 Ethernet (以太网)标准,在我的 MacBook 上 en0 是无线网卡,如果接上有线网卡会多出来一个 en1,前缀是类型,后缀数字区分不同硬件。

pppPPP kext 里,支持 PPP 点对点协议。平时我们最常见到这个协议的应用就是 PPPoE (Point-to-Point Protocol over Ethernet) 了,这个协议主要是在 Ethernet 协议上加了一层身份认证和传输加密,这样电信运营商才可以知道你的帐号,判断你有没有交钱。如果你的机器通过 WiFi 连接到家里的路由器,那么我们只管看 en interface 的数据就好,但是你也有可能直接通过你的 Mac PPPoE 拨号上网,那就得统计 PPP 端口了。

1.7 PPP Connect

PPP interface 的数据处理起来比较麻烦,sysctl() 并没有直接返回数据,我们得另起一个 UNIX domain socket 跟它进行 IPC 通信(参考 MenuMeters的实现)。

UNIX domain socket 跟现在常见的 IP socket 不一样,不过接口差不多。UNIX domain socket 是 UNIX 独有的 IPC 通信方式,出现比 IP socket 还在,它可以用本地文件系统的路径作为 socket 地址(虽然不是真的文件,大部分都在 /var/run 里面),可以直接通过 socket 传文件。当然 Mach Port 也可以传 file descriptor,我们之前的文章也有介绍过。不过Mach Port 和这种特殊 socket 都不是 POSIX 标准。

// PPP local socket path
#define kPPPSocketPath     "/var/run/pppconfd\0"

pppconfdSocket = socket(AF_LOCAL, SOCK_STREAM, 0);
struct sockaddr_un socketaddr = { 0, AF_LOCAL, kPPPSocketPath };
if (connect(pppconfdSocket, (struct sockaddr *)&socketaddr, (socklen_t)sizeof(socketaddr))) {
NSLog(@"MenuMeterNetPPP unable to establish socket for pppconfd. Abort.");
return nil;
}

首先创建一个 UNIX domain socket,然后连接到 pppconfd:

int pppconfdSocket = socket(AF_LOCAL, SOCK_STREAM, 0);
struct sockaddr_un socketaddr = { 0, AF_LOCAL, kPPPSocketPath };
if (connect(pppconfdSocket, (struct sockaddr *)&socketaddr, (socklen_t)sizeof(socketaddr))) {
    NSLog(@"MenuMeterNetPPP unable to establish socket for pppconfd. Abort.");
    return nil;
}

AF_LOCAL 就是 UNIX domain socket 类型,这种类型的 socket 只支持 SOCK_STREAM + TCP 或者 SOCK_DGRAM + UDP,所以第三个参数可以不传。接下来通过 connect 函数连接两个 socket。

// Create the filehandle
pppconfdHandle = [[NSFileHandle alloc] initWithFileDescriptor:pppconfdSocket];
if (!pppconfdHandle) {
    NSLog(@"MenuMeterNetPPP unable to establish file handle for pppconfd. Abort.");
    return nil;
}

ObjC 的 NSFileHandle 可以来做 socket 通信,一个 writeData: 一个 readDataOfLength: 一发已收。

- (NSData *)pppconfdExecMessage:(NSData *)message {
// Write the data
[pppconfdHandle writeData:message];
// Read back the reply headers
NSData *header = [pppconfdHandle readDataOfLength:sizeof(struct ppp_msg_hdr)];
if ([header length]) {
    struct ppp_msg_hdr *header_message = (struct ppp_msg_hdr *)[header bytes];
    if (header_message &amp;&amp; header_message-&gt;m_len) {
        NSData *reply = [pppconfdHandle readDataOfLength:header_message-&gt;m_len];
        if ([reply length] &amp;&amp; !header_message-&gt;m_result) {
            return reply;
        }
    }
}

// Get here we got nothing
return nil;

} // pppconfdExecMessage

接下来先查一下 interface status,我们跟 pppconfd 发一个 PPP 消息:

struct msg {
    struct ppp_msg_hdr  hdr;
    unsigned char   data[MAXDATASIZE];
};

/* PPP message paquets */
struct ppp_msg_hdr {
u_int16_t m_flags; // special flags
u_int16_t m_type; // type of the message
u_int32_t m_result; // error code of notification message
u_int32_t m_cookie; // user param
u_int32_t m_link; // link for this message
u_int32_t m_len; // len of the following data
};

struct ppp_msg {
u_int16_t m_flags; // special flags
u_int16_t m_type; // type of the message
u_int32_t m_result; // error code of notification message
u_int32_t m_cookie; // user param, or error num for event
u_int32_t m_link; // link for this message
u_int32_t m_len; // len of the following data
u_char m_data[1]; // msg data sent or received
};

PPP 的实现不在 XNU 内核范围内,但也是开源的,可以到这里下载源码。可以看到不管是 struct msg 还是 struct ppp_msg 他的内存布局都是一样的,前面是 header 后面是数据。

看到我们跟 PPP 通信需要带一个 m_link 参数,因为 PPP 协议是基于 link 进行数据传输的。PPP 协议主要由三个部分组成:

  • Link Control Protocol (LCP) — Link 控制协议,管理在两点之间通过 link 连接。
  • Authentication protocol — 校验协议,确保两点之间的通信安全。
  • Network control protocol (NCP) — 初始化 PPP 协议栈,用于处理多种网络层的协议,比如 IPv4,IPv6 等等。

其中 LCP 协议规定了 PPP 端口通过 link 传输。并且,PPP 协议支持一点对多点通信,这也是为什么我们家里的宽带有可能通过多拨实现带宽翻倍的原因。多连接协议称为 Multi-Link PPPoE (MLPPP)。

所以要跟 pppconfd 通信前我们还需要先拿到当前的 link:

// Get the link id for the interface
struct ppp_msg_hdr idMsg = { 0, PPP_GETLINKBYIFNAME, 0, 0, -1, (u_int32_t)[ifnameData length] };
NSMutableData *idMsgData = [NSMutableData dataWithBytes:&idMsg length:sizeof(idMsg)];
[idMsgData appendData:ifnameData];
NSData *idReply = [self pppconfdExecMessage:idMsgData];
uint32_t linkID = 0;
if ([idReply length] != sizeof(uint32_t)) return nil;
[idReply getBytes:&linkID];

传入 message type PPP_GETLINKBYIFNAME,带一个 ifname 表示对应的 interface。PPP 源码中对应的实现在这个函数:

static
void socket_getlinkbyifname(struct client *client, struct msg *msg, void **reply)

非常简单,遍历所有端口匹配一下然后 copy 信息返回。

这个函数里的实现用到一个 bytes 转换函数叫做 htonl(),因为 host byte order 和 network byte order 的排序不一样。上层几乎不需要管,但是在后续使用 bpf/pcap 抓包实现的时候就需要自己手动转换这些数据了。

获得 linkID 之后就可以问 PPP 要这条 link 的收发包数据了:

// Now get status of that link
struct ppp_msg_hdr statusMsg = { 0, PPP_STATUS, 0, 0, linkID, 0 };
NSData *statusReply = [self pppconfdExecMessage:[NSData dataWithBytes:&statusMsg length:sizeof(statusMsg)]];
if ([statusReply length] != sizeof(struct ppp_status)) return nil;
struct ppp_status *pppStatus = (struct ppp_status *)[statusReply bytes];
if (pppStatus->status == PPP_RUNNING) {
    // pppStatus->s.run.inBytes
    // pppStatus->s.run.outBytes
    // pppStatus->s.run.timeElapsed
    // pppStatus->s.run.timeRemaining
}

数据处理跟上面非 PPP Connection 的一样, PPP_STATUS 在 PPP 源码中对应的实现在:

static
void socket_status(struct client *client, struct msg *msg, void **reply)

二、小结

本来网络抓包的学习除了通过 sysctl() 接口和 pppconfd 的 socket 通信之外,我还尝试了 NetworkStatistics.frameworkNStat, BPF/pcap 等多种实现。但是没想到第一种实现就已经这么复杂,所以我们把剩下的内容分开多篇来学习。

计算机网络的出现是革命性的,互联网已经重塑了整个世界。相应的,他的蓬勃发展也带来技术的蓬勃发展。虽然历史遗留的问题很多,也有些设计上的缺陷经常被人用于恶意攻击(比如 ARP 的设计就非常不安全),但是以我微弱的能力,对于这些计算机先辈的设计只有滔滔景仰的敬意,以及,缺少文档时阅读起来的痛苦😂。

内核系列文章

参考资料

Tags: