自制操作系统 - IP协议与ICMP协议

IP协议与ICMP协议

Posted by 王富杰 on Wednesday, April 17, 2024

一、IP协议

IP(Internet Protocol)是网络层协议,负责在网络中把数据从源主机传递到目标主机。它 不保证可靠性、不保证顺序、不保证不丢包,只是一个“最佳努力传输(best-effort)”协议。IP地址中有几类特殊地址:

  • 私有地址
    地址块 地址空间 地址数量
    10.0.0.0/8 10.0.0.0 ~ 10.255.255.255 16777216
    172.16.0.0/12 172.16.0.0 ~ 172.31.255.255 1048576
    192.168.0.0/16 192.168.0.0 ~ 192.168.255.255 65536
  • 受限广播地址 255.255.255.255
  • 多播地址 多播允许单个主机同时通过互联网向数个主机发送数据流。它通常用于音频和视频流,例如基于 IP 的有线电视网络。
  • 环回地址 127.0.0.0/8,一般是 127.0.0.1
  • 本机地址 0.0.0.0
  • 本网络地址 0.0.0.0/8

1.1、IP数据报

IP协议的报文结构如下所示: 图片加载失败

  • 版本(Version):4 bit,该值为 4,表示 IPv4;
  • 头长度(Internet Header Length IHL):4 bit,表示 IP 数据包头(黄色部分)的长度 / 4;如果没有选项,头长度为 20 / 4 = 5;所以 IP 头最长为 15 * 4 = 60;
  • 服务类型 (Type of Service TOS):8bit, 用于指示数据包的延迟,吞吐量、可靠性等;
  • 总长度 (Total Length):16bit,数据包的总长度,包括 IP头 和 数据区域;
  • 标识 (Identification):16bit,分片编号
  • 标志(Flags):3bit:
  • 0 bit: 保留
  • 1 bit: 0:可能分片,1:不分片
  • 2 bit: 0:最后一个分片,1:不是最后一个分片
  • 分片偏移 (Fragment Offset):13bit,表示当前分片所携带的数据在整个 IP 数据报中的相对偏移位置;
  • 生存时间(Time to Live TTL):8bit,用于防止 IP 包出现在循环网络中,IP 数据包每经过一个路由器,该值减一,如果 TTL 为 0,则丢弃,同时可能返回一个 ICMP 消息,表示目标不可达;
  • 上层协议: 8bit,1:ICMP 6:TCP 17:UDP
  • 校验和:用于检测IP头是否出错,与数据区域无关;
  • 选项:0 ~ 40 字节;如果没有对齐到 4 字节,最后需要填充 0 对齐到 3 字节,因为头长度的单位是 4 字节;

IP结构定义如下:

typedef struct ip_t
{
    u8 header : 4;  // 头长度
    u8 version : 4; // 版本号
    u8 tos;         // type of service 服务类型
    u16 length;     // 数据包长度
    // 以下用于分片
    u16 id;         // 标识,每发送一个分片该值加 1
    u8 offset0 : 5; // 分片偏移量高 5 位,以 8字节 为单位
    u8 flags : 3;   // 标志位,1:保留,2:不分片,4:不是最后一个分片
    u8 offset1;     // 分片偏移量低 8 位,以 8字节 为单位
    u8 ttl;        // 生存时间 Time To Live,每经过一个路由器该值减一,到 0 则丢弃
    u8 proto;      // 上层协议,1:ICMP 6:TCP 17:UDP
    u16 chksum;    // 校验和
    ip_addr_t src; // 源 IP 地址
    ip_addr_t dst; // 目的 IP 地址
    u8 payload[0]; // 载荷
} _packed ip_t;

1.2、IP协议实现

IP数据包需要有上层协议,因此我们需先定义上层协议,如下:

enum
{
    IP_PROTOCOL_NONE = 0,
    IP_PROTOCOL_ICMP = 1,
    IP_PROTOCOL_TCP = 6,
    IP_PROTOCOL_UDP = 17,
    IP_PROTOCOL_TEST = 254,
};

上层协议有ICMP、TCP、UDP。这些协议我们还没有实现,可以使用测试协议来进行IP的测试工作。接下来就是IP包的实现

err_t ip_input(netif_t *netif, pbuf_t *pbuf)
{
    ip_t *ip = pbuf->eth->ip;
    if (ip->version != IP_VERSION_4)      // 只支持 IPv4
    {
        return -EPROTO;
    }
    if (!ip->ttl)     // ttl 耗尽
        return -ETIME;
    if (ip->header != sizeof(ip_t) / 4)     // 不支持选项
    {
        return -EOPTION;
    }
    if (!ip_addr_cmp(ip->dst, netif->ipaddr))    // 不是本机 ip
        return -EADDR;

    ip->length = ntohs(ip->length);
    ip->id = ntohs(ip->id);

    switch (ip->proto)    // 判断上层协议类型
    {
    case IP_PROTOCOL_TCP:
        LOGK("IP:TCP received\n");
        break;
    case IP_PROTOCOL_UDP:
        LOGK("IP:UDP received\n");
        break;
    case IP_PROTOCOL_ICMP:
        LOGK("IP:ICMP received\n");
        break;
    default:
        LOGK("IP {%X}: %r -> %r \n", ip->proto, ip->src, ip->dst);
        return -EPROTO;
    }
    return EOK;
}

err_t ip_output(netif_t *netif, pbuf_t *pbuf, ip_addr_t dst, u8 proto, u16 len)
{
    ip_t *ip = pbuf->eth->ip;

    ip->header = 5;
    ip->version = 4;
    ip->flags = 0;
    ip->offset0 = 0;
    ip->offset1 = 0;
    ip->tos = 0;
    ip->id = 0;
    ip->ttl = IP_TTL;
    ip->proto = proto;
    u16 length = sizeof(ip_t) + len;
    ip->length = htons(length);
    ip_addr_copy(ip->dst, dst);     // 一定要先对 dst 赋值,因为 dst 指针可能是下面的 src
    ip_addr_copy(ip->src, netif->ipaddr);

    ip->chksum = 0;
    ip->chksum = ip_chksum(ip, sizeof(ip_t));

    return arp_eth_output(netif, pbuf, ip->dst, ETH_TYPE_IP, length);
}

void ip_init()
{
}

如上是发送和接收IP包的实现,以太网帧接收到的包如何为IP包,就可以调用ip_input来进行处理了。

二、ICMP协议

ICMP (Internet Control Message Protocol) Internet 控制报文协议,用于在主机之间传递控制消息,比如测试网络连通性,主机是否可达,路由是否可用等,虽然没有传输信息,但是确实网络中很重要的协议。它的数据报文如下所示: 图片加载失败 ICMP数据包嵌在IP协议中,即IP数据包的载体可能是一个ICMP数据包。ICMP 类型有三种:0:Echo Replay,查询应答 3:Destination Unreachable Message,目标不可达 8:Echo,查询

以下为ICMP协议的实现:

enum
{
    ICMP_ER = 0,   // Echo reply
    ICMP_DUR = 3,  // Destination unreachable
    ICMP_SQ = 4,   // Source quench
    ICMP_RD = 5,   // Redirect
    ICMP_ECHO = 8, // Echo
    ICMP_TE = 11,  // Time exceeded
    ICMP_PP = 12,  // Parameter problem
    ICMP_TS = 13,  // Timestamp
    ICMP_TSR = 14, // Timestamp reply
    ICMP_IRQ = 15, // Information request
    ICMP_IR = 16,  // Information reply
};

typedef struct icmp_echo_t
{
    u8 type;       // 类型
    u8 code;       // 状态码
    u16 chksum;    // 校验和
    u16 id;        // 标识
    u16 seq;       // 序号
    u8 payload[0]; // 可能是特殊的字符串
} _packed icmp_echo_t;

typedef struct icmp_t
{
    u8 type;       // 类型
    u8 code;       // 状态码
    u16 chksum;    // 校验和
    u32 RESERVED;  // 保留
    u8 payload[0]; // 载荷
} _packed icmp_t;


static err_t icmp_echo_reply(netif_t *netif, pbuf_t *pbuf)  // ICMP 查询响应
{
    ip_t *ip = pbuf->eth->ip;
    if (ip_addr_isbroadcast(ip->dst, netif->netmask) || ip_addr_ismulticast(ip->dst))
        return -EPROTO;
    if (!ip_addr_cmp(ip->dst, netif->ipaddr))
        return -EPROTO;

    icmp_t *icmp = ip->icmp;
    icmp->type = ICMP_ER;
    icmp->chksum = 0;
    u16 len = ip->length - sizeof(ip_t);
    icmp->chksum = ip_chksum(icmp, len);
    LOGK("IP ICMP ECHO REPLY: %r -> %r \n", ip->dst, ip->src);
    pbuf->count++;
    return ip_output(netif, pbuf, ip->src, IP_PROTOCOL_ICMP, len);
}

err_t icmp_input(netif_t *netif, pbuf_t *pbuf)   // icmp协议输入
{
    ip_t *ip = pbuf->eth->ip;
    icmp_t *icmp = ip->icmp;
    switch (icmp->type)
    {
    case ICMP_ER:    // 我们ping对方,对方给我们回复了,做一下打印。
        LOGK("IP ICMP REPLY: %r -> %r \n", ip->src, ip->dst);
        break;
    case ICMP_ECHO:           // 如果别人ping我们,就进行回复
        return icmp_echo_reply(netif, pbuf);
        break;
    default:
        LOGK("IP ICMP other: %r -> %r\n", ip->src, ip->dst);
        break;
    }
    return EOK;
}

err_t icmp_echo(netif_t *netif, pbuf_t *pbuf, ip_addr_t dst)    // ICMP 查询
{
    ip_t *ip = pbuf->eth->ip;
    icmp_echo_t *echo = ip->echo;
    echo->code = 0;
    echo->type = ICMP_ECHO;
    echo->id = 1;
    echo->seq = 1;
    char message[] = "Onix icmp echo 1234567890 asdfghjkl;'";
    strcpy(echo->payload, message);
    u32 len = sizeof(icmp_echo_t) + sizeof(message);
    echo->chksum = 0;
    echo->chksum = ip_chksum(echo, len);
    LOGK("IP ICMP ECHO: %r \n", dst);
    return ip_output(netif, pbuf, dst, IP_PROTOCOL_ICMP, len);
}

void icmp_init()
{
}

如上,icmp_echo是请求别人,例如ping其他主机。另外在接收以太网帧时,如果判断为ICMP协议,就可以调用icmp_input进行处理。来决定是响应还是打印,这个协议主要用于ping命令。

三、IP校验和

IP校验和的核心作用是:确保IP数据报头部在传输过程中没有发生错误。

发送方计算:

  • 在发送IP数据报之前,发送方将IP头部的“校验和”字段置为0。
  • 将整个IP头部(通常是20字节的固定部分,不含选项或包含选项)视为一系列16位(2字节)的字。
  • 对这些16位的字进行二进制反码求和。
  • 将求和结果的二进制反码 填入“校验和”字段。

(二进制反码求和的一个特性是:当将所有16位字,包括校验和本身,相加时,如果传输无误,结果应该是一个全1的二进制数,即16进制的0xFFFF)。

接收方验证:

  • 接收方收到IP数据报后,同样将整个IP头部(这次包括发送方填写的校验和字段)视为一系列16位的字。
  • 对这些字进行同样的二进制反码求和。
  • 如果计算结果是全1(0xFFFF),则认为IP头部在传输过程中没有出错,数据报是有效的。

如果计算结果不是全1,则证明头部在传输中发生了错误,接收方会** silently discard(静默丢弃)** 这个数据包。它不会通知发送方,因为IP协议本身是无连接的、不可靠的。上层协议(如TCP)负责检测数据包的丢失并进行重传。

「真诚赞赏,手留余香」

WangFuJie Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付