Home  >  Article  >  Backend Development  >  How do I ensure the correctness of the IPv4 packets created for the native "ping" implementation?

How do I ensure the correctness of the IPv4 packets created for the native "ping" implementation?

王林
王林forward
2024-02-09 08:36:25949browse

如何确保为本机“ping”实现创建的 IPv4 数据包的正确性?

php editor Strawberry will introduce to you how to ensure the correctness of the IPv4 packets created for the local "ping" implementation. In network communication, use the Ping command to test the connectivity between hosts. However, in practical applications, we need to ensure the correctness of the IPv4 data packets sent to avoid errors or losses. To this end, we can take some measures to ensure the accuracy and completeness of data packets to ensure that we get accurate Ping results. Next, let’s take a look at these measures.

Question content

Overview

I've been working on a side project that is essentially a network troubleshooting tool. My goal is to deepen my understanding of networking basics and become proficient in using the troubleshooting tools provided by the operating system.

This is a CLI application that will get the hostname and try to diagnose the problem if any. The plan is to implement ping and traceroute first and then gradually implement other tools based on my comfort level.

However, my ping implementation is not accurate because the IPv4 packets are malformed. This is what wireshark has to say.

1   0.000000    192.168.0.100   142.250.195.132 ICMP    300 Unknown ICMP (obsolete or malformed?)

Code

This is how I implement ping

<code>package ping

import (
    "encoding/json"
    "net"

    "github.com/pkg/errors"
)

var (
    IcmpProtocolNumber uint8 = 1
    IPv4Version        uint8 = 4
    IPv4IHL            uint8 = 5
    ICMPHeaderType     uint8 = 8
    ICMPHeaderSubtype  uint8 = 0
)

type NativePinger struct {
    SourceIP string
    DestIP   string
}

type ICMPHeader struct {
    Type     uint8
    Code     uint8
    Checksum uint16
}

type ICMPPacket struct {
    Header  ICMPHeader
    Payload interface{}
}

type IPv4Header struct {
    SourceIP       string
    DestinationIP  string
    Length         uint16
    Identification uint16
    FlagsAndOffset uint16
    Checksum       uint16
    VersionIHL     uint8
    DSCPAndECN     uint8
    TTL            uint8
    Protocol       uint8
}

type IPv4Packet struct {
    Header  IPv4Header
    Payload *ICMPPacket
}

func (p *NativePinger) createIPv4Packet() (*IPv4Packet, error) {
    versionIHL := (IPv4Version << 4) | IPv4IHL

    icmpPacket := &ICMPPacket{
        Header: ICMPHeader{
            Type: ICMPHeaderType,
            Code: ICMPHeaderSubtype,
        },
    }
    ipv4Packet := &IPv4Packet{
        Header: IPv4Header{
            VersionIHL:     versionIHL,
            DSCPAndECN:     0,
            Identification: 0,
            FlagsAndOffset: 0,
            TTL:            64,
            Protocol:       IcmpProtocolNumber,
            SourceIP:       p.SourceIP,
            DestinationIP:  p.DestIP,
        },
        Payload: icmpPacket,
    }
    ipv4Packet.Header.Length = 40

    bytes, err := json.Marshal(icmpPacket)
    if err != nil {
        return nil, errors.Wrapf(err, "error converting ICMP packet to bytes")
    }

    icmpPacket.Header.Checksum = calculateChecksum(bytes)

    bytes, err = json.Marshal(ipv4Packet)
    if err != nil {
        return nil, errors.Wrapf(err, "error converting IPv4 packet to bytes")
    }

    ipv4Packet.Header.Checksum = calculateChecksum(bytes)

    return ipv4Packet, nil
}

func calculateChecksum(data []byte) uint16 {
    sum := uint32(0)

    // creating 16 bit words
    for i := 0; i < len(data)-1; i++ {
        word := uint32(data[i])<<8 | uint32(data[i+1])
        sum += word
    }
    if len(data)%2 == 1 {
        sum += uint32(data[len(data)-1])
    }

    // adding carry bits with lower 16 bits
    for (sum >> 16) > 0 {
        sum = (sum & 0xffff) + (sum >> 16)
    }

    // taking one's compliment
    checksum := ^sum
    return uint16(checksum)
}

func (p *NativePinger) ResolveAddress(dest string) error {
    ips, err := net.LookupIP(dest)
    if err != nil {
        return errors.Wrapf(err, "error resolving address of remote host")
    }

    for _, ip := range ips {
        if ipv4 := ip.To4(); ipv4 != nil {
            p.DestIP = ipv4.String()
        }
    }

    // The destination address does not need to exist as unlike tcp, udp does not require a handshake.
    // The goal here is to retrieve the outbound IP. Source: https://stackoverflow.com/a/37382208/3728336
    //
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
        return errors.Wrapf(err, "error resolving outbound ip address of local machine")
    }
    defer conn.Close()

    p.SourceIP = conn.LocalAddr().(*net.UDPAddr).IP.String()

    return nil
}

func (p *NativePinger) Ping(host string) error {
    if err := p.ResolveAddress(host); err != nil {
        return errors.Wrapf(err, "error resolving source/destination addresses")
    }

    packet, err := p.createIPv4Packet()
    if err != nil {
        return errors.Wrapf(err, "error creating IPv4Packet")
    }

    conn, err := net.Dial("ip4:icmp", packet.Header.DestinationIP)
    if err != nil {
        return errors.Wrapf(err, "error eshtablishing connection with %s", host)
    }
    defer conn.Close()

    bytes, err := json.Marshal(packet)
    if err != nil {
        return errors.Wrapf(err, "error converting IPv4 packet into bytes")
    }

    _, err = conn.Write(bytes)
    if err != nil {
        return errors.Wrapf(err, "error sending ICMP echo request")
    }

    buff := make([]byte, 2048)
    _, err = conn.Read(buff) // The implementation doesn't proceed beyond this point
    if err != nil {
        return errors.Wrapf(err, "error receiving ICMP echo response")
    }

    return nil
}

</code>

Introspection

I'm not sure if the packet malformation is caused by a single cause or multiple causes. I think the problem lies in one of these two places (or both?):

  1. Incorrect header length calculation I manually calculated the length to be 40 bytes (wordsize = 4 bytes) . Write structure fields in an order that prevents structure corruption. I refer to this source for information on various types of sizes.
<code>// 1 word (4 bytes)
type ICMPHeader struct {
    Type     uint8  // 8 bit
    Code     uint8  // 8 bit
    Checksum uint16 // 16 bit
}

// 3 words (3*4 = 12 bytes)
type ICMPPacket struct {
    Header  ICMPHeader  // 1 word
    Payload interface{} // 2 words
}

// 7 words (7*4 = 28 bytes)
type IPv4Header struct {
    // Below group takes 4 words (each string takes 2 words)
    SourceIP      string
    DestinationIP string

    // Following group takes 2 words (each 16 bits)
    Length         uint16
    Identification uint16
    FlagsAndOffset uint16
    Checksum       uint16

    // Below group takes 1 word (each takes 8 bits)
    VersionIHL uint8
    DSCPAndECN uint8
    TTL        uint8
    Protocol   uint8
}

// 10 words (40 bytes)
type IPv4Packet struct {
    Header  IPv4Header // 7 words as calculated above
    Payload ICMPPacket // 3 words as calculated above
}
</code>
  1. Checksum calculation is incorrectI implemented the Internet checksum algorithm. If this isn't what I'm supposed to be doing here, please tell me.

There are some parts missing in the implementation, such as configuring counts, assigning sequence numbers to packets, etc., but before that the basic implementation needs to be fixed, i.e. receiving responses for ICMP ECHO packets. Good to know where I made the mistake.

Thanks!

Updated August 24, 2023

Taking into account the advice I got in the comments, I have updated the code to fix the byte order and use raw bytes for the source address, destination address. However, this alone doesn't solve the problem, the packet is still malformed, so there must be something else going on.

Solution

I finally got it working. I should talk about a few issues with the code.

Serialization problem

As Andy correctly pointed out, I'm sending a JSON object, not raw bytes in network byte order. This is fixed using binary.Write(buf, binary.BigEndian, field)

However, since this method only works with fixed-size values, I have to do this for each struct field, making the code repetitive and somewhat ugly.

Structural optimization and serialization are different issues.

I am aware of the practice of combining the Version and IHL fields together to optimize memory, that's why I have this single field VersionIHL in my structure. But when serializing, the field values ​​(4 and 5 in this case) will be serialized individually, and I didn't do that. Instead, I convert the entire VersionIHL field's value into bytes.

As a result, I found myself sending an unexpected octet 69 in the byte stream from combining 4 and 5 grouped together 0100 0101.

Incomplete ICMP packet

My ICMP structure does not contain identifier and sequence number fields. The information provided in the ICMP datagram header section on Wikipedia feels a bit generic. However, I found the details on the RFC page (page 14) to be much more insightful.

This feels strange considering the importance of the serial number of the ping utility. During implementation, I often found myself wondering where the serial number was placed appropriately in the code. It wasn't until I stumbled across the RFC page that I had a clear idea of ​​when and where to incorporate serial numbers.

For anyone who might be interested, here is the function code I've put it together.

The above is the detailed content of How do I ensure the correctness of the IPv4 packets created for the native "ping" implementation?. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:stackoverflow.com. If there is any infringement, please contact admin@php.cn delete