没有任何协议是 POSIX 标准。POSIX 根本不需要系统支持任何特定的网络协议或任何网络协议。
AF_PACKET
是一个纯 Linux 发明 AFAIK,你不会在其他系统上找到它。
BPF(伯克利包过滤器)也不是 POSIX,它是许多系统复制的 BSD 发明,因为它非常方便。但是,您不能使用它注入流量,您只能使用它捕获传入和传出流量。
如果有人关心,这里是最新的 POSIX 标准:
The Open Group Base Specifications Issue 7, 2018 edition
IEEE Std 1003.1™-2017(IEEE Std 1003.1-2008 修订版)
如果你真的想发送原始 IP 数据包(无论是 IPv4 还是 IPv6),使用原始 IP 套接字是最便携的:
int soc = socket(PF_INET, SOCK_RAW, IPPROTO_IP);
然后你需要告诉系统你想提供你自己的IP头:
int yes = 1;
setsockopt(soc, IPPROTO_IP, IP_HDRINCL, &yes, sizeof(yes));
现在您可以将原始 IP 数据包(例如 IP 标头 + UDP 标头 + 有效负载数据)发送到套接字以进行发送,但是,根据您的系统,系统将执行一些健全性检查,并且可能会覆盖标头中的某些字段。例如,它可能不允许您创建格式错误的 IP 数据包或阻止您执行 IP 地址欺骗。因此,它可以例如为您计算 IPv4 标头校验和,或者如果您的 IP 标头使用0.0.0.0
或::
作为源地址,则自动填写正确的源地址。在您的目标系统上查看ip(4)
或查看手册页。raw(7)
Apple 不再提供 macOS 的程序员手册页,但您可以在网上找到它们。
引用该手册页:
与以前的 BSD 版本不同,程序必须设置 IP 标头的所有字段,包括以下内容:
ip->ip_v = IPVERSION;
ip->ip_hl = hlen >> 2;
ip->ip_id = 0; /* 0 means kernel set appropriate value */
ip->ip_off = offset;
ip->ip_len = len;
请注意,ip_off
andip_len
字段是按主机字节顺序排列的。
如果标头源地址设置为INADDR_ANY
,内核将选择一个合适的地址。
请注意,ip_sum
根本没有提到这一点,因此显然您不必提供那个,系统将始终为您计算它。
如果将其与 Linux raw(7)进行比较:
┌───────────────────────────────────────────────────┐
│IP Header fields modified on sending by IP_HDRINCL │
├──────────────────────┬────────────────────────────┤
│IP Checksum │ Always filled in │
├──────────────────────┼────────────────────────────┤
│Source Address │ Filled in when zero │
├──────────────────────┼────────────────────────────┤
│Packet ID │ Filled in when zero │
├──────────────────────┼────────────────────────────┤
│Total Length │ Always filled in │
└──────────────────────┴────────────────────────────┘
当从原始 IP 套接字接收时,您将获得所有到达主机的传入 IP 数据包或只是其中的一部分(例如,Windows 确实支持原始套接字,但永远不会让您发送或接收 TCP 数据包)。您将收到完整的数据包,包括所有标头,因此收到的每个数据包的第一个字节是 IP 标头的第一个字节。
这里有些人会问我为什么使用IPPROTO_IP
而不是IPPROTO_RAW
. 使用IPPROTO_RAW
时不必设置IP_HDRINCL
:
IPPROTO_RAW
隐含的协议已启用IP_HDRINCL
,并且能够发送在传递的标头中指定的任何 IP 协议。
但您只能IPPROTO_RAW
用于传出流量:
IPPROTO_RAW
套接字仅用于发送。
在 macOS 上您可以使用IPPROTO_IP
并且您将收到所有 IP 数据包,但在 Linux 上这可能不起作用,因此创建了一个新的套接字PF_PACKET
套接字类型。应该在两个系统上工作的是指定一个子协议:
int soc = socket(PF_INET, SOCK_RAW, IPPROTO_UDP);
当然,现在您只能通过该套接字发送/接收 UDP 数据包。如果IP_HDRINCL
再次设置,则需要在发送时提供完整的 IP 标头,并且您将在接收时收到完整的 IP 标头。如果你不设置它,你可以只在发送时提供UDP头,系统会自己添加一个IP头,也就是说,如果套接字是连接的并且可以选择绑定,那么系统就知道在那个头中使用哪些地址. 对于接收该选项不起作用,您总是会为您在此类套接字上收到的每个 UDP 数据包获取 IP 标头。
如果人们想知道我为什么使用PF_INET
而不是AF_INET
:PF 表示Protocol Family和 AF 表示Address Family。通常这些是相同的(例如AF_INET == PF_INET
)所以你使用什么并不重要,但严格来说应该创建套接字PF_
并且sockaddr
应该设置结构中的系列,AF_
因为有一天可能会有一个支持两种不同地址的协议然后会有AF_XXX1
和AF_XXX2
没有一个可能与 相同PF_XXX
。