自己動手編寫tcp/ip協(xié)議棧2:tcp包生成

首發(fā)于github page 自己動手編寫tcp/ip協(xié)議棧2:tcp包生成

數(shù)據(jù)結(jié)構(gòu)

上一篇文章較為簡單,所以沒有詳細(xì)講解數(shù)據(jù)結(jié)構(gòu)的設(shè)計,之后的文章難度會逐漸增加,所以這里先介紹一下數(shù)據(jù)結(jié)構(gòu)的設(shè)計。計算機(jī)網(wǎng)絡(luò)是分層結(jié)構(gòu),除物理層外每一層都有相應(yīng)的包結(jié)構(gòu)。從鏈路層到應(yīng)用層,每一層都會將下一層的包包裹起來,所以我們設(shè)計數(shù)據(jù)結(jié)構(gòu)的時候也設(shè)一層包裹一層的形式。基本的構(gòu)造方法如下:

packet_test.go

pack := NewIPPack(NewTcpPack(&RawPack{}))

ip對象包裹tcp對象,tcp對象包裹raw對象,生成的是ip對象。構(gòu)造函數(shù)的入?yún)⒍际墙涌?,所以如果你愿意,你也可以在tcp中再包裹一層ip對象。

pack := NewIPPack(NewTcpPack(NewIPPack(&RawPack{})))

這種寫法不僅是理論上可行,實際工程中也有意義。一些特殊的網(wǎng)絡(luò)工具確實是通過在tcp中包裹原始的一些數(shù)據(jù)包來實現(xiàn)如網(wǎng)絡(luò)代理之類的功能的。
網(wǎng)絡(luò)數(shù)據(jù)包的接口定義如下:

packet.go

type NetworkPacket interface {
    Decode(data []byte) (NetworkPacket, error)
    Encode() ([]byte, error)
}

構(gòu)造函數(shù)定義如下:

tcp.go

func NewTcpPack(payload NetworkPacket) *TcpPack {
    return &TcpPack{Payload: payload}
}

網(wǎng)絡(luò)包的接口定義非常簡單,Decode函數(shù)將數(shù)據(jù)包解碼為對象,Encode函數(shù)將對象編碼為數(shù)據(jù)包。

ip包生成

完整實現(xiàn)如下:

ip encode

func (i *IPPack) Encode() ([]byte, error) {
    var (
        payload []byte
        err     error
    )
    if i.Payload != nil {
        payload, err = i.Payload.Encode()
        if err != nil {
            return nil, err
        }
    }
    data := make([]byte, 0)
    if i.HeaderLength == 0 {
        i.HeaderLength = uint8(20 + len(i.Options))
    }
    data = append(data, i.Version<<4|i.HeaderLength/4)
    data = append(data, i.TypeOfService)
    if i.TotalLength == 0 {
        i.TotalLength = uint16(i.HeaderLength) + uint16(len(payload))
    }
    data = binary.BigEndian.AppendUint16(data, i.TotalLength)
    data = binary.BigEndian.AppendUint16(data, i.Identification)
    data = binary.BigEndian.AppendUint16(data, uint16(i.Flags)<<13|i.FragmentOffset)
    data = append(data, i.TimeToLive)
    data = append(data, i.Protocol)
    data = binary.BigEndian.AppendUint16(data, i.HeaderChecksum)
    data = append(data, i.SrcIP...)
    data = append(data, i.DstIP...)
    data = append(data, i.Options...)
    if i.HeaderChecksum == 0 {
        i.HeaderChecksum = calculateIPChecksum(data)
    }
    binary.BigEndian.PutUint16(data[10:12], i.HeaderChecksum)
    data = append(data, payload...)

    return data, nil
}

大部分字段的轉(zhuǎn)換都是一些基礎(chǔ)的位運(yùn)算,這里就不詳細(xì)解釋了。需要注意的是校驗和的生成。
校驗和的計算稍微有點(diǎn)繁瑣,而且也不是tcp,ip協(xié)議的重點(diǎn),如果想要盡快完成一個可以工作的tcp,ip協(xié)議實現(xiàn),可以暫時跳過,直接拷貝現(xiàn)成的實現(xiàn)代碼即可。
不過不能不管校驗和,校驗不通過的包會直接被丟棄。

校驗和

計算校驗和要先生成ip的頭的數(shù)據(jù)包,生成的包中checksum字段為0,然后對數(shù)據(jù)包進(jìn)行校驗和計算。
rfc原文如下
checksum

In outline, the Internet checksum algorithm is very simple:
(1)  Adjacent octets to be checksummed are paired to form 16-bit
    integers, and the 1's complement sum of these 16-bit integers is
    formed.
(2)  To generate a checksum, the checksum field itself is cleared,
    the 16-bit 1's complement sum is computed over the octets
    concerned, and the 1's complement of this sum is placed in the
    checksum field.

翻譯過來就是相鄰的8位字節(jié)組成16位整數(shù),然后對這些整數(shù)求反碼(1's complement)和,最后對這個和取反碼。
下面還有另外一段原文補(bǔ)充:

On a 2's complement machine, the 1's complement sum must be
computed by means of an "end around carry", i.e., any overflows
from the most significant bits are added into the least
significant bits. See the examples below.

翻譯過來就是在補(bǔ)碼(2's complement)表示的機(jī)器上對于溢出的處理,將溢出的部分加到最低位。
所以計算反碼和的實現(xiàn)如下

packet.go

func OnesComplementSum(data []byte) uint16 {
    var sum uint16
    for i := 0; i < len(data); i += 2 {
        sum += binary.BigEndian.Uint16(data[i : i+2])
        // if sum is less than the current byte, it means there is a carry
        if sum < binary.BigEndian.Uint16(data[i:i+2]) {
            sum++ // handle carry
        }
    }
    return sum
}

聰明的讀者可能已經(jīng)發(fā)現(xiàn)了,這個函數(shù)要求入?yún)⑹桥紨?shù)長度的字節(jié)數(shù)組。rfc中對奇數(shù)的情況這樣說明

A, B, C, D, ... , Y, Z.  Using the notation [a,b] for the 16-bit
integer a*256+b, where a and b are bytes, then the 16-bit 1's
complement sum of these bytes is given by one of the following:

    [A,B] +' [C,D] +' ... +' [Y,Z]              [1]

    [A,B] +' [C,D] +' ... +' [Z,0]              [2]

where +' indicates 1's complement addition. These cases
correspond to an even or odd count of bytes, respectively.

也就是如果字節(jié)數(shù)是奇數(shù),那么在末尾填充一個0字節(jié)。
綜上,ip包的校驗和計算如下:

ip checksum

// https://datatracker.ietf.org/doc/html/rfc1071#autoid-1
func calculateIPChecksum(headerData []byte) uint16 {
    if len(headerData)%2 == 1 {
        headerData = append(headerData, 0)
    }
    return ^OnesComplementSum(headerData)
}

tcp包生成

tcp encode

func (t *TcpPack) Encode() ([]byte, error) {
    data := make([]byte, 0)
    data = binary.BigEndian.AppendUint16(data, t.SrcPort)
    data = binary.BigEndian.AppendUint16(data, t.DstPort)
    data = binary.BigEndian.AppendUint32(data, t.SequenceNumber)
    data = binary.BigEndian.AppendUint32(data, t.AckNumber)
    if t.DataOffset == 0 {
        t.DataOffset = uint8(20 + len(t.Options))
    }
    data = append(data, ((t.DataOffset>>2)<<4)|t.Reserved)
    data = append(data, t.Flags)
    data = binary.BigEndian.AppendUint16(data, t.WindowSize)
    data = binary.BigEndian.AppendUint16(data, t.Checksum)
    data = binary.BigEndian.AppendUint16(data, t.UrgentPointer)
    data = append(data, t.Options...)
    if t.Payload != nil {
        payload, err := t.Payload.Encode()
        if err != nil {
            return nil, err
        }
        data = append(data, payload...)
    }
    if t.Checksum == 0 {
        if t.PseudoHeader == nil {
            return nil, errors.New("pseudo header is required to calculate tcp checksum")
        }
        t.Checksum = calculateTcpChecksum(t.PseudoHeader, data)
        binary.BigEndian.PutUint16(data[16:18], t.Checksum)
    }
    return data, nil
}

tcp包的生成也只有校驗和比較復(fù)雜,同樣的,如果想要盡快完成一個可以工作的tcp,ip協(xié)議實現(xiàn),可以暫時跳過,直接拷貝現(xiàn)成的實現(xiàn)代碼即可。

校驗和

tcp包的校驗和計算在需要對tcp包頭加上一些額外數(shù)據(jù),然后再使用函數(shù)計算這個數(shù)據(jù)包的校驗和。rfc原文如下

pseudo-header

The checksum also covers a pseudo-header (Figure 2) conceptually prefixed to the TCP header.
+--------+--------+--------+--------+
|           Source Address          |
+--------+--------+--------+--------+
|         Destination Address       |
+--------+--------+--------+--------+
|  zero  |  PTCL  |    TCP Length   |
+--------+--------+--------+--------+
Figure 2: IPv4 Pseudo-header

Pseudo-header components for IPv4:
    Source Address: the IPv4 source address in network byte order
    Destination Address: the IPv4 destination address in network byte order
    zero: bits set to zero
    PTCL: the protocol number from the IP header
    TCP Length: the TCP header length plus the data length in octets (this is not an explicitly transmitted quantity but is computed), and it does not count the 12 octets of the pseudo-header.

所以我們先要生成偽頭,然后計算校驗和,偽頭的數(shù)據(jù)都可以簡單地從ip包中獲取到。生成新數(shù)據(jù)包后再使用計算ip校驗和相同的函數(shù)計算校驗和即可,最終實現(xiàn)如下:

tcp checksum

func (t *TcpPack) SetPseudoHeader(srcIP, dstIP []byte) {
    t.PseudoHeader = &PseudoHeader{SrcIP: srcIP, DstIP: dstIP}
}

// https://datatracker.ietf.org/doc/html/rfc1071#autoid-1
func calculateTcpChecksum(pseudo *PseudoHeader, headerPayloadData []byte) uint16 {
    length := uint32(len(headerPayloadData))
    pseudoHeader := make([]byte, 0)
    pseudoHeader = append(pseudoHeader, pseudo.SrcIP...)
    pseudoHeader = append(pseudoHeader, pseudo.DstIP...)
    pseudoHeader = binary.BigEndian.AppendUint32(pseudoHeader, uint32(ProtocolTCP))
    pseudoHeader = binary.BigEndian.AppendUint32(pseudoHeader, length)

    sumData := make([]byte, 0)
    sumData = append(sumData, pseudoHeader...)
    sumData = append(sumData, headerPayloadData...)

    if len(sumData)%2 == 1 {
        sumData = append(sumData, 0)
    }

    return ^OnesComplementSum(sumData)
}

校驗和計算性能優(yōu)化

校驗和計算有非常多的優(yōu)化方法,這里介紹一種使用uint32計算的優(yōu)化方法。
直接使用uint32計算,所有溢出的部分都加到了高16位,然后我們把高16位加回到低16位即可,如果再次溢出則繼續(xù)加回到低16位,直到不再溢出為止。

func OnesComplementSum(data []byte) uint16 {
    var sum uint32
    for i := 0; i < len(data); i += 2 {
        sum += uint32(binary.BigEndian.Uint16(data[i : i+2]))
    }
    // Add the carry bits back in
    for sum > 0xffff {
        sum = (sum & 0xffff) + (sum >> 16)
    }
    return uint16(sum)
}

注意事項

  • 我的協(xié)議棧項目主要以教學(xué)為目的,所以我優(yōu)先保證代碼的可讀性,其次是性能,所以很多實現(xiàn)都不是最優(yōu)的。實際生產(chǎn)級別的代碼會做大量的性能優(yōu)化、錯誤處理、邊界檢查,一定程度上犧牲可讀性換來更高的性能和安全性。
  • 現(xiàn)有的實現(xiàn)中ip id始終為0,這是為了簡化實現(xiàn),ip id主要在ip分片的時候使用,所以這里可以先忽略,現(xiàn)在的實現(xiàn)在小包的情況下可以正常工作。
  • ip, tcp都有options字段,涉及到一些擴(kuò)展的網(wǎng)絡(luò)功能,也可以先忽略不實現(xiàn)。

推薦閱讀

總結(jié)

至此,我們已經(jīng)完成了tcp包的生成,下一篇文章我們將開始實現(xiàn)tcp三次握手。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容