php小編草莓將為大家介紹如何確保為本機“ping”實現創建的IPv4數據包的正確性。在網絡通信中,使用Ping命令可以測試主機之間的連通性。但是,在實際應用中,我們需要確保所發送的IPv4數據包的正確性,以避免出現錯誤或丟失的情況。為此,我們可以采取一些措施來保證數據包的準確性和完整性,從而確保我們獲得準確的Ping結果。接下來,讓我們一起來了解這些措施。
問題內容
概述
我一直在從事一個業余項目,該項目本質上是一個網絡故障排除工具。我的目的是加深對網絡基礎知識的理解,并熟練使用操作系統提供的故障排除工具。
這是一個 CLI 應用程序,它將獲取主機名并嘗試診斷問題(如果有)。計劃是首先實現 ping 和 traceroute,然后根據我的舒適程度逐步實現其他工具。
但是,我的 ping 實現并不準確,因為 IPv4 數據包格式錯誤。這就是wireshark 必須說的。
1 0.000000 192.168.0.100 142.250.195.132 ICMP 300 Unknown ICMP (obsolete or malformed?)
登錄后復制
代碼
這是我實現 ping
的方法
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])<> 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
}
登錄后復制
自省
我不確定數據包的畸形是由單一原因還是多種原因造成的。
我覺得問題出在這兩個地方之一(或兩者?):
-
標頭長度計算不正確 我手動計算的長度為
40 字節(wordsize = 4 字節)
。按照可防止結構體損壞的順序編寫結構體字段。我參考這個來源來了解各種類型的大小。
// 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
}
登錄后復制
校驗和計算不正確我實現了互聯網校驗和算法。如果這不是我應該在這里做的事情,請告訴我。
實現中缺少一些部分,例如配置計數、為數據包分配序列號等,但在此之前需要修復基本實現,即接收 ICMP ECHO 數據包的響應。很高興知道我在哪里犯了錯誤。
謝謝!
2023 年 8 月 24 日更新
考慮到我在評論中得到的建議,我已經更新了代碼,即修復字節順序并使用原始字節作為源地址、目標地址。然而,僅此并不能解決問題,數據包仍然格式錯誤,因此肯定還有其他問題。
解決方法
我終于讓它工作了。我應該談談代碼的幾個問題。
序列化問題
正如 Andy 正確指出的那樣,我發送的是 JSON 對象,而不是按網絡字節順序發送原始字節。這是使用 binary.Write(buf, binary.BigEndian, field)
修復的
但是,由于此方法僅適用于固定大小的值,因此我必須對每個結構體字段執行此操作,從而使代碼重復且有些難看。
結構優化和序列化是不同的問題。
我知道將 Version
和 IHL
字段組合在一起以優化內存的做法,這就是為什么我的結構中有這個單個字段 VersionIHL
。但是在序列化時,字段值(在本例中為 4 和 5)將被單獨序列化,而我沒有這樣做。相反,我將整個 VersionIHL
字段的值轉換為字節。
結果,我發現自己在字節流中發送了一個意外的八位字節 69
,該字節流來自將 4
和 5
組合在一起的 0100 0101
。
不完整的 ICMP 數據包
我的 ICMP 結構不包含標識符和序列號字段。 Wikipedia 上的 ICMP 數據報標頭部分提供的信息感覺有點通用。但是,我發現 RFC 頁面(第 14 頁) 上的詳細信息要多得多富有洞察力。
考慮到 ping 實用程序的序列號的重要性,這感覺很奇怪。在實現過程中,我經常發現自己想知道序列號在代碼中的適當位置。直到我偶然發現 RFC 頁面,我才清楚地了解何時何地合并序列號。
對于任何可能感興趣的人,這里是功能代碼我已經整理好了。