Linux 网络编程-结构体总结

  • 资料来源:

    <>

  • 更新

    1
    2024.03.05 初始

导语

一些 Linux 网络编程中常见的结构体及一些用法总结.

Function and Algorithm

先是大小端 (网络序/主机序) 转换两个函数:

  • ntohl ntohs 32bit 和 16bit | 网络序 -> 主机序
  • htonl htons 32bit 和 16bit | 主机序 -> 网络序
    这俩函数基本是网络编程的基石了,先拜拜 永无 bug;

1’s Complement 加法

1's complement 翻译为 二进制数的 反码; 是一种 数字表示法

  • 将 二进制 数每个数字反转得到的数: 若某一位为 0, 则使其变为 1,反之亦然.[^1]

network 的 1's complement 加法

  • 校验和 位置 置 0
  • 待计算部分 划分为 16bit, 不足部分补 0.
  • 逐个 16bit 相加 得到 sum
  • sum 高于 16bit 部分 + 回低 16bit; 重复这个过程直到最后只剩下 16bit res;
  • 取 res 的逐位取反 (即 res 的 1's complement 表示).

校验

  • 同样 16bit 得到 sum;
  • sum += 校验和
  • sum 高 16bit 加回低 16bit; 得到 16bit res;
  • 再取反 应该得到 0;

校验和算法参考: https://github.com/buckrudy/Blog/issues/12

L2

以太网头部: 默认长度 12 字节, 有标签情况下可能是 18 字节 (802.1q) 22 字节 (802.1ad)

1
2
3
4
5
6
#define ETH_ALEN    6
struct ethhdr {
unsigned char h_dest[ETH_ALEN]; /* destination eth addr */
unsigned char h_source[ETH_ALEN]; /* source ether addr */
__be16 h_proto; /* packet type ID field */
} __attribute__((packed));

h_proto 是以太网的协议类型; v4 v6 arp; 0x8100 VXLAN (ETH_P_8021Q ETH_P_8021AD); MPLS 的 单/多标签

0x8100 VXLAN (ETH_P_8021Q ETH_P_8021AD) 时候需要处理拓展的 标签长度.

L3

ip 头 和 ip 地址的存储转换

Ip 地址

v4 地址 struct in_addr 就是一个 网络序的 u32; v6 struct in6_addr 则是联合体 32*4 128bit 网络序;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};

地址转换: 人类可读的 str 类型 和 struct in_addr struct in6_addr 互相转换

  • inet_pton inet_ntop
1
2
3
4
5
6
7
8
9
10
11
struct in_addr ipv4addr;
inet_pton(AF_INET, "192.168.1.1", &ipv4addr);

struct in6_addr ipv6addr;
inet_pton(AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334", &ipv6addr);

char ipv4str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &ipv4addr, ipv4str, INET_ADDRSTRLEN);

char ipv6str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &ipv6addr, ipv6str, INET6_ADDRSTRLEN);

Ipv4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct iphdr
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ihl:4;
unsigned int version:4;
#elif __BYTE_ORDER == __BIG_ENDIAN
unsigned int version:4;
unsigned int ihl:4;
#else
# error "Please fix <bits/endian.h>"
#endif
uint8_t tos;
uint16_t tot_len;
uint16_t id;
uint16_t frag_off;
uint8_t ttl;
uint8_t protocol;
uint16_t check;
uint32_t saddr;
uint32_t daddr;
/*The options start here. */
};

saddr daddr 代表了 v4 地址的数值表示.

校验和

ihl(Internet Header Length): 第一个字节的后 4bit 代表了头部长度 (x 4 字节 x32bit)

  • 基本情况下是 20 字节, 也就是 5;
  • 但是可选字段下最大可以拓展到 60 字节;

校验和计算范围: 包括可选字段的整个 header, 不足 16bit 补 0;

Ipv6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct ip6_hdr
{
union
{
struct ip6_hdrctl
{
uint32_t ip6_un1_flow; /* 4 bits version, 8 bits TC,
20 bits flow-ID */
uint16_t ip6_un1_plen; /* payload length */
uint8_t ip6_un1_nxt; /* next header */
uint8_t ip6_un1_hlim; /* hop limit */
} ip6_un1;
uint8_t ip6_un2_vfc; /* 4 bits version, top 4 bits tclass */
} ip6_ctlun;
struct in6_addr ip6_src; /* source address */
struct in6_addr ip6_dst; /* destination address */
};

/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};

struct in6_addr 代表了 v6 地址表示,实际长度是 u32 x 4 = 128bit 对应 v6 的长度.

  • 封装了 3 种形式的访问: 8bit 16bit 和 32bit

校验和

ipv6 没有校验和 第一军团没有秘密

l2 以及提供足够的校验了, tcp/udp 也有自己的校验, l3 其实没那么需要.

v6 设计时候就加快包转发, header 长度固定, 没有分片, 不计算校验和 更减轻了路由的计算压力.

L4

l4 这里主要是传输层协议 tcp udp icmp 和 icmpv6 了.

Tcp

struct tcphdr tcp 头部

  • netinet/tcp.hnetinet/tcp.h , 不涉及内核, 网络编程就选 netinet/tcp.h

struct tcphdr 这里使用了两个结构体访问同一个 tcp 头, 后一种更加细分, 访问到具体 各个 bit;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* TCP header.
* Per RFC 793, September, 1981.
*/
struct tcphdr
{
__extension__ union
{
struct
{
uint16_t th_sport; /* source port */
uint16_t th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
# if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t th_x2:4; /* (unused) */
uint8_t th_off:4; /* data offset */
# endif
uint8_t th_flags;
# define TH_FIN 0x01
# define TH_SYN 0x02
# define TH_RST 0x04
# define TH_PUSH 0x08
# define TH_ACK 0x10
# define TH_URG 0x20
uint16_t th_win; /* window */
uint16_t th_sum; /* checksum */
uint16_t th_urp; /* urgent pointer */
};
struct
{
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
# if __BYTE_ORDER == __LITTLE_ENDIAN
uint16_t res1:4;
uint16_t doff:4;
uint16_t fin:1;
uint16_t syn:1;
uint16_t rst:1;
uint16_t psh:1;
uint16_t ack:1;
uint16_t urg:1;
uint16_t res2:2;
# else
# error "Adjust your <bits/endian.h> defines"
# endif
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
};
};
};
  • 源码太长了, 删减了 大小端判断, linux x64 基本是小端.
  • doff tcp 头部长度, >5 有可选字段

窗口缩放因子

刷八股文时候, tcp 滑动窗口基本最大值是 65535; 但是实际写 conntack 时候遇到个 窗口缩放因子;

跨洋线路,高延迟 高容量 window_size 65535 完全不够用, 哪怕没有丢包也只会收敛到很小的速度.

实际窗口 = window_size(16bit) << 缩放因子;

  • 变相将窗口拓展到了 32bit;

窗口缩放因子

  • RFC 1072 中引入 RFC 1323 完善; 位于 tcp 头可选字段;
  • 仅在握手时候确认固定值, 两侧都必须支持缩放才启用, 一侧不支持都禁用.
  • 3 个字节, 第一个字节是选项种类 (3 固定值) 第二个 选项长度, 第三个 窗口缩放值 (0 至 14)

![[Pasted image 20240228185056.png]]

取值:

  • 看 doff > 5,
  • 遍历找到字段选项 3, 取值
  • 还得确认对端也启用了窗口缩放.

校验和

需要个伪头部, 然后再进入 1's complement 加法流程

伪头部 对于 v4 v6 略有不同

  • v4: 12 字节; 源地址 目的地址 padding(8bit) 协议值 (TCP) TCP 长度 (header+payload)
  • v6: 源地址 目的地址 数据长度 (header+payload) padding(32bit) 下一个头部字段 (TCP 协议值)

Udp

struct udphdr 还是用 netinet/udp.h 的定义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* UDP header as specified by RFC 768, August 1980. */

struct udphdr
{
__extension__ union
{
struct
{
uint16_t uh_sport; /* source port */
uint16_t uh_dport; /* destination port */
uint16_t uh_ulen; /* udp length */
uint16_t uh_sum; /* udp checksum */
};
struct
{
uint16_t source;
uint16_t dest;
uint16_t len;
uint16_t check;
};
};
};

还是 udp 头部简单.

校验和

与 tcp 伪头部构造相同, 协议换成 udp;

Icmp

struct icmphdr: netinet/ip_icmp.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct icmphdr
{
uint8_t type; /* message type */
uint8_t code; /* type sub-code */
uint16_t checksum;
union
{
struct
{
uint16_t id;
uint16_t sequence;
} echo; /* echo datagram */
uint32_t gateway; /* gateway address */
struct
{
uint16_t __glibc_reserved;
uint16_t mtu;
} frag; /* path mtu discovery */
} un;
};

conntrack 跟踪 icmp 则取的是 id type 和 code;

最常见的类型就是 ICMP_ECHO ICMP_ECHOREPLY 一去一回.

校验和

不包括伪头部! 只计算 icmp 部分;

Icmp6

struct icmp6_hdr: 与 icmp 还是有些区别, 特别是具体类型.

1
2
3
4
5
6
7
8
9
10
11
12
struct icmp6_hdr
{
uint8_t icmp6_type; /* type field */
uint8_t icmp6_code; /* code field */
uint16_t icmp6_cksum; /* checksum field */
union
{
uint32_t icmp6_un_data32[1]; /* type-specific field */
uint16_t icmp6_un_data16[2]; /* type-specific field */
uint8_t icmp6_un_data8[4]; /* type-specific field */
} icmp6_dataun;
};

ICMP6_ECHO_REQUESTICMP6_ECHO_REPLY 一去一回.

校验和

icmpv6 是有伪头部的; 这一点和 icmp 完全不同

  • 毕竟 icmpv6 几乎是重新设计的协议,虽然还是叫 icmp, 还承接了 igmp arp 等等 v4 协议的功能.

伪头部包含以下字段:源地址 目的地址 ICMPv6 消息长度 一个三个字节的填充字段 下一个头部字段 (icmpv6 协议值)

[^1]: M Morris Mano; Michael D Ciletti. Digital design : with an introduction to the verilog hdl. 培生教育. 2013: 第 27 页. ISBN 9780273764526.