Skip to content

自作TCP/IPプロトコルでTCPサーバーを作る

2023/04/02

Go

目次

目次

  1. はじめに
  2. 3 ウェイハンドシェイク
    1. コネクション確立
    2. コネクション終了
  3. 実装
    1. TUN/TAP
    2. ヘッダーのシリアライズ・デシリアライズ
      1. IP ヘッダー
      2. TCP ヘッダー
  4. 終わりに

はじめに

本記事では、自作 TCP/IP プロトコルと RAW_SOCKET を用いて、簡易的なサーバーを作成します。

SOCK_STREAM を用いて作成したシンプルな TCP サーバーを動かし、Wireshark でキャプチャしたパケットのやり取りを再現することを目指します。
クライアントからcurlを実行し、サーバーがHello World!をデータに含んだパケットを返すことがゴールです。

sample

最終的なコードのリポジトリはこちらです。README の Tutorial を参考に動かすことが出来るはずです。

kawa1214/tcp-ip-go(Github)

(TCP/IP プロトコルやサーバーとしての機能は非常に不足しているのでご了承ください。)

パケットを確認するためのシンプルなサーバー

3 ウェイハンドシェイク

今回作成した TCP サーバーのパケットです。1 から 3 がコネクション確立、7 から 9 がコネクション切断のパケットになります。

capture

コネクション確立

  1. クライアントからサーバーに対して SYN パケットを送ります。コネクション開始の要求を行います。
  2. サーバーからクライアントに対して SYN-ACK パケットを送ります。コネクション確立の承認を行います。
  3. クライアントからサーバーに対して ACK パケットを送ります。コネクション確立の承認を受け取ったことを伝えます。
クライアントサーバー1. SYN2. SYN, ACK3. ACKクライアントサーバー

コネクション終了

  1. クライアントからサーバーに FIN パケットを送りコネクションの終了をリクエストをリクエストします。
  2. サーバーからクライアントに FIN,ACK パケットを送り、コネクションの終了を承認します。
  3. クライアントからサーバーに ACK パケットを送り、コネクション終了の承認を受け取ったことを伝えます。
クライアントサーバー1. FIN, ACK2. FIN, ACK3. ACKクライアントサーバー

実装

コードベースで紹介します。詳しい実装はリポジトリをご覧ください。

kawa1214/tcp-ip-go(Github)

TUN/TAP

今回は TUN/TAP で仮想ネットワークデバイスを作成しその中で通信しています。

// NewTun creates and initializes a new TUN device.
func NewTun() (*Tun, error) {
	file, err := os.OpenFile("/dev/net/tun", os.O_RDWR, 0)
	if err != nil {
		return nil, fmt.Errorf("open error: %s", err.Error())
	}

	ifr := Ifreq{}
	copy(ifr.IfrName[:], []byte("tun0"))
	ifr.IfrFlags = IFF_TUN | IFF_NO_PI

	_, _, sysErr := syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), uintptr(TUNSETIFF), uintptr(unsafe.Pointer(&ifr)))
	if sysErr != 0 {
		return nil, fmt.Errorf("ioctl error: %s", sysErr.Error())
	}

	return &Tun{
		file:  file,
		ifreq: &ifr,
	}, nil
}

// Close closes the TUN device.
func (t *Tun) Close() error {
	return t.file.Close()
}

// Read packets with TUN Device.
func (t *Tun) Read(buf []byte) (uintptr, error) {
	n, _, sysErr := syscall.Syscall(syscall.SYS_READ, t.file.Fd(), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
	if sysErr != 0 {
		return 0, fmt.Errorf("read error: %s", sysErr.Error())
	}

	return n, nil
}

// Write packets with TUN Device.
func (t *Tun) Write(buf []byte) (uintptr, error) {
	n, _, sysErr := syscall.Syscall(syscall.SYS_WRITE, t.file.Fd(), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
	if sysErr != 0 {
		return 0, fmt.Errorf("write error: %s", sysErr.Error())
	}

	return n, nil
}

ヘッダーのシリアライズ・デシリアライズ

IP ヘッダーのフォーマットを確認しつつシリアライズ・デシリアライズの実装をします。Wireshark を眺めつつ期待したパケットになっているかを確認すると進めやすいです。

IP ヘッダー

こちらの IP ヘッダーのフォーマットを参考に実装します。

https://www.infraexpert.com/study/tcpip1.html

type Header struct {
	Version        uint8
	IHL            uint8
	TOS            uint8
	TotalLength    uint16
	ID             uint16
	Flags          uint8
	FragmentOffset uint16
	TTL            uint8
	Protocol       uint8
	Checksum       uint16
	SrcIP          [4]byte
	DstIP          [4]byte
}

const (
	IP_VERSION   = 4
	IHL          = 5
	TOS          = 0
	TTL          = 64
	LENGTH       = IHL * 4
	TCP_PROTOCOL = 6
)

// New creates a new IP header from packet.
func Parse(pkt []byte) (*Header, error) {
	if len(pkt) < 20 {
		return nil, fmt.Errorf("invalid IP header length")
	}

	header := &Header{
		Version:        pkt[0] >> 4,
		IHL:            pkt[0] & 0x0F,
		TOS:            pkt[1],
		TotalLength:    binary.BigEndian.Uint16(pkt[2:4]),
		ID:             binary.BigEndian.Uint16(pkt[4:6]),
		Flags:          pkt[6] >> 5,
		FragmentOffset: binary.BigEndian.Uint16(pkt[6:8]) & 0x1FFF,
		TTL:            pkt[8],
		Protocol:       pkt[9],
		Checksum:       binary.BigEndian.Uint16(pkt[10:12]),
	}

	copy(header.SrcIP[:], pkt[12:16])
	copy(header.DstIP[:], pkt[16:20])

	return header, nil
}

// Create a new IP header.
func New(srcIP, dstIP [4]byte, len int) *Header {
	return &Header{
		Version:     IP_VERSION,
		IHL:         IHL,
		TOS:         TOS,
		TotalLength: uint16(LENGTH + len),
		ID:          0,
		Flags:       0x40,
		TTL:         64,
		Protocol:    TCP_PROTOCOL,
		Checksum:    0,
		SrcIP:       srcIP,
		DstIP:       dstIP,
	}
}

// Return a byte slice of the packet.
func (h *Header) Marshal() []byte {
	versionAndIHL := (h.Version << 4) | h.IHL
	flagsAndFragmentOffset := (uint16(h.FragmentOffset) << 13) | (h.FragmentOffset & 0x1FFF)

	pkt := make([]byte, 20)
	pkt[0] = versionAndIHL
	pkt[1] = 0
	binary.BigEndian.PutUint16(pkt[2:4], h.TotalLength)
	binary.BigEndian.PutUint16(pkt[4:6], h.ID)
	binary.BigEndian.PutUint16(pkt[6:8], flagsAndFragmentOffset)
	pkt[8] = h.TTL
	pkt[9] = h.Protocol
	binary.BigEndian.PutUint16(pkt[10:12], h.Checksum)
	copy(pkt[12:16], h.SrcIP[:])
	copy(pkt[16:20], h.DstIP[:])

	re

TCP ヘッダー

こちらの TCP ヘッダーのフォーマットを参考に実装します。

https://www.infraexpert.com/study/tcpip8.html

const (
	LENGTH      = 20
	WINDOW_SIZE = 65535
	PROTOCOL    = 6
)

type Header struct {
	SrcPort    uint16
	DstPort    uint16
	SeqNum     uint32
	AckNum     uint32
	DataOff    uint8
	Reserved   uint8
	Flags      HeaderFlags
	WindowSize uint16
	Checksum   uint16
	UrgentPtr  uint16
}

type HeaderFlags struct {
	CWR bool
	ECE bool
	URG bool
	ACK bool
	PSH bool
	RST bool
	SYN bool
	FIN bool
}

// New creates a new TCP header from packet.
func Parse(pkt []byte) (*Header, error) {
	if len(pkt) < 20 {
		return nil, fmt.Errorf("invalid TCP header length")
	}

	flags := parseFlag(pkt[13])

	header := &Header{
		SrcPort:    binary.BigEndian.Uint16(pkt[0:2]),
		DstPort:    binary.BigEndian.Uint16(pkt[2:4]),
		SeqNum:     binary.BigEndian.Uint32(pkt[4:8]),
		AckNum:     binary.BigEndian.Uint32(pkt[8:12]),
		DataOff:    pkt[12] >> 4,
		Reserved:   pkt[12] & 0x0E,
		Flags:      flags,
		WindowSize: binary.BigEndian.Uint16(pkt[14:16]),
		Checksum:   binary.BigEndian.Uint16(pkt[16:18]),
		UrgentPtr:  binary.BigEndian.Uint16(pkt[18:20]),
	}

	return header, nil
}

// Create a new TCP header.
func New(srcPort, dstPort uint16, seqNum, ackNum uint32, flags HeaderFlags) *Header {
	return &Header{
		SrcPort:    srcPort,
		DstPort:    dstPort,
		SeqNum:     seqNum,
		AckNum:     ackNum,
		DataOff:    0x50,
		Reserved:   0x12,
		Flags:      flags,
		WindowSize: uint16(WINDOW_SIZE),
		Checksum:   0,
		UrgentPtr:  0,
	}
}

// Return a byte slice of the packet.
func (h *Header) Marshal() []byte {
	f := h.Flags.marshal()

	pkt := make([]byte, 20)
	binary.BigEndian.PutUint16(pkt[0:2], h.SrcPort)
	binary.BigEndian.PutUint16(pkt[2:4], h.DstPort)
	binary.BigEndian.PutUint32(pkt[4:8], h.SeqNum)
	binary.BigEndian.PutUint32(pkt[8:12], h.AckNum)
	pkt[12] = h.DataOff
	pkt[13] = f
	binary.BigEndian.PutUint16(pkt[14:16], h.WindowSize)
	binary.BigEndian.PutUint16(pkt[16:18], h.Checksum)
	binary.BigEndian.PutUint16(pkt[18:20], h.UrgentPtr)

	return pkt
}

// Return a string representation of the packet byte.
func parseFlag(f uint8) HeaderFlags {
	return HeaderFlags{
		CWR: f&0x80 == 0x80,
		ECE: f&0x40 == 0x40,
		URG: f&0x20 == 0x20,
		ACK: f&0x10 == 0x10,
		PSH: f&0x08 == 0x08,
		RST: f&0x04 == 0x04,
		SYN: f&0x02 == 0x02,
		FIN: f&0x01 == 0x01,
	}
}

// Return a byte slice of the packet.
func (f *HeaderFlags) marshal() uint8 {
	var packedFlags uint8
	if f.CWR {
		packedFlags |= 0x80
	}
	if f.ECE {
		packedFlags |= 0x40
	}
	if f.URG {
		packedFlags |= 0x20
	}
	if f.ACK {
		packedFlags |= 0x10
	}
	if f.PSH {
		packedFlags |= 0x08
	}
	if f.RST {
		packedFlags |= 0x04
	}
	if f.SYN {
		packedFlags |= 0x02
	}
	if f.FIN {
		packedFlags |= 0x01
	}
	return packedFlags
}

終わりに

Go の net パッケージをコードリーディングした際に TCP/IP 周りの理解が不足していると感じたことがモチベーションでした。(Go net/http パッケージ(server) Walkthrough(記事はこちら))

パケットのフォーマットやハンドシェイクなど、TCP/IP の一部は理解できたかなと思いますが、未だに分からないことが多いという印象です。

(カーネルはすごいという気持ちになりました……)