跟我一起学TCP/IP

Web、Mail、Ftp、DNS、Proxy、VPN、Samba、LDAP 等基础网络服务
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

跟我一起学TCP/IP

#1

帖子 723937936@qq.com » 2023-02-26 10:06

参考书:史蒂文斯先生的名著《TCP/IP Illustrated Volume 1: The Protocols》第一版
欢迎有兴趣的同学一起加入学习讨论
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#2

帖子 723937936@qq.com » 2023-02-26 10:56

第一章:介绍

关键概念强调:

主机(host):就理解成一台电脑
网络:将多台主机通过交换机(switch)连接在一起就组成一个网络
互联网(internet):将多个网络通过路由器连接在一起就组成了一个互联网,要区分internet和Internet,前者指将多个网络用路由器连接起来,后者是专有名词指英特网
ip地址:一个32位长的整数,32位分为两个部分,分别是netid和hostid,netid部分是由一个管理机构分配的(比如给某个公司分配一个B类网络),hostid是管理员(你自己)分配的;B类网络的hostid占16位,也就是有65536台主机,相当于一下子分配了65536个公网ip给这个公司
作者这里介绍的ip是说的是公网ip,如果是私有ip,那么netid和hostid都由管理员自己分配
netid标识一个网络,网络内的主机由hostid标识。参考书上图1.6的ip地址分类,像A类地址这样的,netid占7位,hostid占24位,那也就是说A类地址最多有128个网络,每个网络内可以有16777216台主机,这个网络太大了,所以就引入了另一个层次,在网络内再划分子网络,子网络也是网络,子网络之间也是通过路由器连接的,由路由器连接的所有子网络组成一个网络,网络与网络之间又通过路由器连接组成internet(首字母小写)
端口号:端口号用来标识进程。ip层负责将分组(packet)一跳一跳的传输到目的主机,分组到达目的主机后要交给哪个进程是由分组中携带的目的端口号信息决定的
udp数据报(udp datagram):udp层发送和接收的数据单元称为udp datagram
tcp段(tcp segment):tcp层发送和接收的数据单元称为tcp segment
ip数据报(ip datagram):ip层发送和接收的数据单元称为ip datagram
帧(frame):链路层发送和接收的数据单元称为frame
分组(packet):更一般的术语,基本等同于ip datagram,当一个路由器接收到一个ip datagram时,如果发现出口链路的mtu比较小,则需要执行分片操作,这样一个ip datagram可能变成2个或多个ip datagram,这些分片也称为分组,分组是一个一般的术语包含ip datagram和分片的ip datagram
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#3

帖子 723937936@qq.com » 2023-02-26 13:16

第二章:链路层(link layer)

这一章我们只关注以太网(ethernet)帧格式(见书上图2.1)

------------------------------------------------
| destination addr | source addr | type | data | crc |
------------------------------------------------
6 6 2 46-1500 4

以太网帧格式头(header)共14字节:包含目的mac地址、源mac地址、数据的类型
type字段:
0x0800 - ip datagram
0x0806 - arp request/reply
0x8035 - rarp request/reply

以太网帧对data的长度有要求:最低46字节,最高1500字节
也就是说最小的以太网帧长度为14+46=60字节,最大的以太网帧长度14+1500=1514字节(不考虑crc字段)

MTU(Maximum Transmission Unit):以太网帧可以携带的最大数据长度,即1500字节
Path MTU:链路层对传输数据的长度的限制是与具体链路层使用的技术有关,比如使用以太网技术组成的网络,那么在这个网络内部主机之间的链路上传输的数据单元最大为1500字节,但是如果使用其他的链路层技术组建网络,那这个网络的MTU则可能不同,也就说MTU是网络内部链路的属性。当使用路由器连接两个使用不同链路层技术组建的网络时,两个网络各自有自己的MTU限制,当两个网络里的主机相互通信时,通信链路的的MTU限制要使用最小的网络MTU(否则路由器在转发ip datagram时要执行分片操作),这个最小的MTU就是路径MTU。

发送主机要知道一条路线(route)的路径MTU,则需要一种发现技术,称为path MTU discovery mechanism
一种技术是发送主机在ip数据报头里指定DF(Dont Fragment)标志,然后发送较大的ip数据报,促使中间某个路由器需要执行分片却无法执行,进而丢弃该ip数据报,并向发送主机回送一个ICMP不可达消息。发送主机收到路由器回送的ICMP不可达消息后可以尝试减小发送的ip数据报长度,最终确定路径MTU的值。

或者反过来,发送主机先发送较小的ip数据报,然后不断增大ip数据报的大小直到收到ICMP不可达消息,则表示发送的ip数据报超过了路径MTU的大小。

主机需要知道路径MTU的目的是为了避免发送的ip数据报在中间的某个路由器被分片,因为分片被认为是不好的。

可以用netstat命令来获取接口的MTU

代码: 全选

$ netstat -i
Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR 囗囗囗
enp0s3    1500  1397582      0      0 0        696458      0      0      0 BMRU
lo       65536   759726      0      0 0        759726      0      0      0 LRU
编程获取接口的MTU(参考UNP 17.7小节)

代码: 全选

#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

int main(int argc, char *argv[])
{
    if (argc != 2) {
        printf("usage: mtu interface\n");
        exit(1);
    }
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket");
        exit(1);
    }
    struct ifreq req;
    bzero(&req, sizeof(req));
    strcpy(req.ifr_name, argv[1]);
    if (ioctl(sockfd, SIOCGIFMTU, &req) < 0) {
        perror("ioctl");
        exit(1);
    }
    printf("%ld\n", (long)req.ifr_mtu);
}

代码: 全选

$ cc mtu.cpp -o mtu
$ ./mtu enp0s3
1500
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#4

帖子 723937936@qq.com » 2023-02-26 20:13

观察以太网帧格式

UNP 29.4小节讲述链路层的访问接口,linux链路层访问接口的详细描述可参考man 7 packet

访问链路层的接口如下:

代码: 全选

int sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
第一个参数指定协议族为AF_PACKET表示应用层直接访问链路层
第二个参数可以指定SOCK_RAW或SOCK_DGRAM
SOCK_RAW:通过sockfd接收的帧包含头
SOCK_DGRAM:通过sockfd接收的帧不包含头
第三个参数指定要接收的协议类型,协议类型定义在头文件<linux/if_ether.h>中
ETH_P_ALL:表示接收所有协议类型
ETH_P_IP:表示只接收ip协议类型
ETH_P_ARP:表示只接收arp协议类型

如果只想接收指定接口的帧,则需要调用bind函数指定接口索引,接口索引通过如下函数获得

代码: 全选

#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
完整的代码参见https://gitee.com/q723937936/tcpip/blob ... nkdump.cpp

执行示例:

代码: 全选

sudo ./linkdump enp0s3 | head -5
broadcast:60:FFFFFFFFFFFF2C6104BAFFFA080600010800060400012C6104BAFFFAC0A80001FFFFFFFFFFFFC0A80001000000000000000000000000000000000000
outgoing:238:909C4AC0BED0080027A2A34A0800451000E04D5E400040066AE5C0A8006EC0A800060016C2A8147D9ED4083A090B801801F5829700000101080A45FE
broadcast:60:FFFFFFFFFFFF2C6104BAFFFA080600010800060400012C6104BAFFFAC0A80001FFFFFFFFFFFFC0A800B3000000000000000000000000000000000000
host:66:080027A2A34A909C4AC0BED0080045480034000040004006B8B7C0A80006C0A8006EC2A80016083A090B147D9F80801007FD870C00000101080A2E1A
outgoing:502:909C4AC0BED0080027A2A34A0800451001E84D5F4000400669DCC0A8006EC0A800060016C2A8147D9F80083A090B801801F5839F00000101080A45FE
主要观察以太网帧格式的头,即前14个字节

上述输出的第一个帧是一个arp请求消息:
目的mac地址:FFFFFFFFFFFF 表示链路层的广播地址
源mac地址:2C6104BAFFFA
帧类型:0806 表示arp消息
帧长度为60字节,消息后面的一些00是填充字节,因为以太网帧最小长度是60字节
从上面的输出可以看出,应用接收到的以太网帧的尾部没有包含CRC字段(4个字节)

上述输出的第二个帧是一个ip数据报:
目的mac地址:909C4AC0BED0
源mac地址:080027A2A34A
帧类型:0800 表示ip数据报
帧长度为238字节,为了输出显得整洁,程序只打印了前60个字节
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#5

帖子 723937936@qq.com » 2023-02-27 23:13

第三章:IP协议

IP Header格式
Screen Shot 2023-02-27 at 10.01.38 PM.png
  • version:ip协议的版本,这里是ipv4,所以填4
  • header length:这个字段的单位是4字节,不带ip选项的ip数据报的ip header长度是20字节,所以这里填5,这个字段只有4位,最大值是15,所以ip header的最大长度是15*4=60字节,也就是说ip选项最大40字节
  • TOS:服务类型,这个字段先不管,填0
  • total length:这个字段填的是ip数据报的总长度
  • identification、flags、fragment offset:这三个字段跟ip数据报的分片有关,先不管
  • TTL:决定ip数据报可以经过的路由器数量,每经过一个路由器就会减1,当TTL为0时,路由器丢掉该ip数据报,traceroute命令利用这个特性来追踪ip数据报经过了哪些路由器
  • protocol:ip数据报承载的上层协议类型:tcp-6、udp-17、icmp-1、igmp-2
  • checksum:ip数据报头的校验和
  • source ip address:源ip地址
  • destination ip address:目的ip地址
  • options:ip选项,目前应该是没什么用,先不管
IP路由

IP路由机制:

当IP层收到一个ip数据报时,他搜索路由表,找到一个匹配条目,将该ip数据报发送给该条目指定的网关或直接发送给目的主机

IP层收到ip数据报有两种情况:
主机自己产生的ip数据报
从接口收到ip数据报(这种情况是主机被配置成一个路由器)

匹配路由条目是根据要发送的ip数据报的目的地址与路由表里的条目的Destination地址进行比较,Destination可能是一个主机地址也可能是一个网络地址
如果要发送的ip数据报的目的地址与某个条目的Destination地址完全匹配,则该条目指定一个主机的路由信息
如果要发送的ip数据报的目的地址的netid与某个条目的Destination地址完全匹配,则该条目指定一个网络的路由信息

下面根据具体路由表信息进行说明

路由表(routing table)查看命令

代码: 全选

$ netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 enp0s3
192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 enp0s3
$
$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    100    0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 enp0s3
192.168.0.0     0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
上面两个命令都可以查看IP层维护的路由表信息

上面路由表的第一条目的Destination表示默认路由条目,Gateway为192.168.0.1是我的路由器的ip地址
上面路由表的第三条目的Destination表示一个网络(由Genmask 255.255.255.0表示),Gateway为0.0.0.0,表示我的主机连接在这个网络上,所以要发送ip数据报会直接发送给目的主机,不需要经过路由器
上面路由表的第二条目看起来和第三条目一样,Destination也表示一个网络,具体是什么网络,我也不清楚,先不管了
Iface字段表示要发送的ip数据报从哪个接口出去

下面添加两条主机路由条目

代码: 全选

$ sudo route add -host 192.168.0.139 dev enp0s3
$ sudo route add -host 110.242.68.66 gw 192.168.0.1 dev enp0s3
$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    100    0        0 enp0s3
110.242.68.66   192.168.0.1     255.255.255.255 UGH   0      0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 enp0s3
192.168.0.0     0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
192.168.0.139   0.0.0.0         255.255.255.255 UH    0      0        0 enp0s3
Flags里的H的表示Destination是主机地址,G表示Gateway是网关地址
Genmask为255.255.255.255也表示Destination是主机地址

编程接口

UNP 17.9小节介绍了使用ioctl来添加删除路由表的接口,但ioctl不支持读取路由表信息,route命令添加、删除路由表信息使用ioctl接口
UNP 18.4小节介绍了使用sysctl来读取路由表的接口,但Linux上sysctl接口不推荐使用
UNP 18章介绍的routing socket在Linux上不支持
Linux上可以通过/proc/net/route来获取路由表信息,netstat -r和route -n使用的方法
Linux添加、删除、获取路由表信息的推荐方法是使用netlink接口,该方法是新的软件包iproute2使用的方法,参考man 7 netlink和man 7 rtnetlink
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#6

帖子 723937936@qq.com » 2023-02-28 23:10

子网划分

回顾IP地址的分类
Screen Shot 2023-02-28 at 9.35.06 PM.png
从上图可以看出,A类和B类的IP地址给hostid分别分配了24位和16位,也就是说在A类网络内可以有2^24-2个主机,B类网络内可以有2^16-2个主机
很少有网络会连接这么多的主机,因此管理员一般会将hostid进一步划分成subnetid和hostid两部分。

上面说的网络内的主机数-2是因为hostid全零和全1是特殊的IP地址

划分子网后的网络拓扑结构如下:
Screen Shot 2023-02-28 at 10.37.24 PM.png
以B类网络举例:

B类网络的hostid有16位,我们给subnetid分8位,剩下8位给hostid
以书上的140.252这个B类网络为例,在140.252这个B类网络中有2^8-2个子网络,在每个子网络内有2^8-2个主机
因为划分子网络对于(父)网络是透明的,所以在(父)网络看来hostid(16位)全0和全1是无效的地址,所以子网络的个数也要减2

C类网络一般不划分子网络,或者说只包含一个子网络,也就是subnetid的位数为0,即只有2^0=1个子网络

对于连接(父)网络的路由器(后面称为外部路由器)来说,他的路由表包含许多路由条目(每个子网络对应一条),当外部路由器收到一个ip数据报,他检查目的ip属于哪个子网,然后将ip数据报转发到对应子网络的路由器,然后由子网络的路由器将ip数据报直接发送给目的主机


子网掩码(subnet mask)

每个网络接口除了需要配置一个ip地址,还需要配置一个子网掩码
从接口的ip地址可以知道主机属于哪个网络(根据ip的前几位可以确定ip属于哪类网络,确定网络的类后,可以根据相应的类,确定netid位数)
但是从ip地址无法确定主机属于哪个子网络,subnetid和hostid的边界需要用子网掩码来确定
子网掩码的全1部分对应ip地址的netid和subnetid,全0部分对应ip地址的hostid

同一个子网内的所有主机的子网掩码必须相同

从上面子网掩码的介绍,似乎子网掩码没什么作用,因为一个主机从接口收到ip数据报时,是直接比较ip数据报的目的ip地址与自己接口的ip地址,并不使用子网掩码,但是如果收到的ip数据报是一个面向子网的广播数据报(目的地址的hostid全1),那么接收主机就要比较netid和subnetid了,这时就需要使用子网掩码

在路由器的路由表条目里也有一个称为genmask的字段,该字段表示generality netmask的意思,因为外部中间路由器,他只将ip数据报转发到目的网络的外部路由器,这时候是没有子网概念的,所以不能叫subnet mask

广播IP地址
  • 全1的ip地址是limited broadcast地址,limited的意思是说目的地址为全1的ip数据报,路由器绝对不会转发
  • netid和subnetid不为0,hostid为1,面向子网的广播地址,这样的ip数据报会被广播到指定的子网的所有主机,路由器会转发
ifconfig命令

ifconfig命令可以用来查看接口信息和配置接口的ip地址和子网掩码

代码: 全选

// 配置ip地址和子网掩码
# ifconfig eth0 192.168.0.101 netmask 255.255.0.0
// 查看ip地址、子网掩码以及其他接口相关信息
$ ifconfig
ifconfig命令使用ioctl编程接口进行接口操作(UNP 17章有详细介绍)
723937936@qq.com
帖子: 51
注册时间: 2023-02-26 9:59
系统: ubuntu

Re: 跟我一起学TCP/IP

#7

帖子 723937936@qq.com » 2023-03-01 21:34

第四章:ARP协议

首先回顾一下子网掩码的作用,除了上一篇说的作用外,我发现设置接口的子网掩码,会自动更新路由表,
可能某个神秘的程序在监听接口状态(rtnetlink(7)接口可以监听接口状态变化事件),然后更新路由表

代码: 全选

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    100    0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 enp0s3
192.168.0.0     0.0.0.0         255.255.255.0   U     100    0        0 enp0s3
上面的最后一个条目描述的是主机所在的子网(192.168.0这个子网)的路线(route),也就说如果这台主机向同一个子网内的其他主机发送ip数据报,则ip路由模块会选择这条路线,并将IP数据报直接发送给目的主机(从enp0s3接口输出)

代码: 全选

// 我通过Ubuntu的网络接口配置界面配置子网掩码为255.255.255.128,后查看接口信息
$ ifconfig enp0s3
enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.0.22  netmask 255.255.255.128  broadcast 192.168.0.127
        inet6 fe80::8253:abe9:8fab:a110  prefixlen 64  scopeid 0x20<link>
        ether 08:00:27:a2:a3:4a  txqueuelen 1000  (Ethernet)
        RX packets 1318603  bytes 300408996 (300.4 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1195589  bytes 524945167 (524.9 MB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

// 查看路由表,发现Genmask一列变化了
$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG    100    0        0 enp0s3
169.254.0.0     0.0.0.0         255.255.0.0     U     1000   0        0 enp0s3
192.168.0.0     0.0.0.0         255.255.255.128 U     100    0        0 enp0s3
ip数据报在输出前进行路由选择时是将ip数据报的目的地址与Genmask进行&操作,然后与Destination字段进行比较,如果相同则通过Iface字段指定的接口输出,所以如果主机的子网掩码设置错误,可能会导致找不到匹配的路由条目,从而无法通信

一句话概括,主机接口的子网掩码,决定主机所在的子网,主机所在子网又决定了子网路由条目(就是上面路由表里的最后一条路由条目)


ARP协议帧格式
Screen Shot 2023-03-01 at 8.09.38 PM.png
ARP协议的作用是在局域网中,已知某台主机的IP地址查询那台主机的MAC地址
ARP协议是由IP层调用的,是IP层为了确定下一跳或下一个目的主机的硬件地址而发起的ARP查询请求,也就是说ARP请求发生在在IP路由之后,
如果IP在路由选择时无法在路由表里找到匹配的route,则不会发起ARP请求,因为此时,IP层不知道将IP数据报发送到哪里

字段说明:
前三个字段是以太网帧格式,共14字节,在这里frame type为0x0806表示ARP协议
hard type:这里是以太网,值为1
prot type:这里是ipv4,值为0x0800
hard size:以太网地址的长度,值为6,单位是字节
prot size:ipv4地址的长度,值为4,单位是字节
op:操作类型:1-arp request; 2-arp reply
sender Ethernet addr:发送者的以太网地址
sender IP addr:发送者的IP地址
target Ethernet addr:要查询的主机的硬件地址
target IP addr:要查询的主机的IP地址

在arp请求消息中,target Ethernet addr是未填充的,因为就是要查询目标主机的硬件地址
在arp应答消息中,target Ethernet addr被填充,并交换两个sender和target字段

纸上得来终觉浅,绝知此事要躬行,我们不适用tcpdump,而是通过编程的方式来加深记忆

使用packet socket抓取arp request/reply

代码地址:https://gitee.com/q723937936/tcpip/blob ... rpdump.cpp

起一个终端,使用arping 发送arp request

代码: 全选

$ arping 192.168.0.5
ARPING 192.168.0.5 from 192.168.0.110 enp0s3
Unicast reply from 192.168.0.5 [90:9C:4A:C0:BE:D0]  0.770ms
Unicast reply from 192.168.0.5 [90:9C:4A:C0:BE:D0]  1.130ms
Unicast reply from 192.168.0.5 [90:9C:4A:C0:BE:D0]  1.227ms
Unicast reply from 192.168.0.5 [90:9C:4A:C0:BE:D0]  1.328ms
再起一个终端,观察arp request/reply消息

代码: 全选

$ sudo ./arpdump | grep '192.168.0.5'
Request: who-has 192.168.0.5 tell 192.168.0.110
Reply: 192.168.0.5 is-at 90:9c:4a:c0:be:d0
Request: who-has 192.168.0.5 tell 192.168.0.110
Reply: 192.168.0.5 is-at 90:9c:4a:c0:be:d0
Request: who-has 192.168.0.5 tell 192.168.0.110
Reply: 192.168.0.5 is-at 90:9c:4a:c0:be:d0
Request: who-has 192.168.0.5 tell 192.168.0.110
Reply: 192.168.0.5 is-at 90:9c:4a:c0:be:d0
arping会不停的向指定主机发送arp请求,从arpdump的输出,可以确认目标主机返回的arp应答


Gratuitous ARP

免费ARP没有什么特殊之处,主要用途就是查询子网内有没有IP冲突,比如我想将我的主机ip设为为192.168.0.88,我可以先用
arping 192.168.0.88来查询这个IP是否被别的主机配置了,如果收到应答,则说明这个IP已经被使用了

arp命令

arp命令可以手工修改arp cache的内容

代码: 全选

// 查询arp cache
$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.2              ether   a4:45:19:6b:e4:d8   C                     enp0s3
192.168.0.5              ether   90:9c:4a:c0:be:d0   C                     enp0s3
192.168.0.1              ether   2c:61:04:ba:ff:fa   C                     enp0s3

// 删除arp条目
$ sudo arp -d 192.168.0.2
$ arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.0.5              ether   90:9c:4a:c0:be:d0   C                     enp0s3
192.168.0.1              ether   2c:61:04:ba:ff:fa   C                     enp0s3

// 添加arp条目
$ sudo arp -s 192.168.0.2 a4:45:19:6b:e4:d8
$ arp -n
Address