目次
はじめに
本記事では、自作 TCP/IP プロトコルと RAW_SOCKET を用いて、簡易的なサーバーを作成します。
SOCK_STREAM を用いて作成したシンプルな TCP サーバーを動かし、Wireshark でキャプチャしたパケットのやり取りを再現することを目指します。
クライアントからcurl
を実行し、サーバーがHello World!
をデータに含んだパケットを返すことがゴールです。
最終的なコードのリポジトリはこちらです。README の Tutorial を参考に動かすことが出来るはずです。
(TCP/IP プロトコルやサーバーとしての機能は非常に不足しているのでご了承ください。)
3 ウェイハンドシェイク
今回作成した TCP サーバーのパケットです。1 から 3 がコネクション確立、7 から 9 がコネクション切断のパケットになります。
コネクション確立
- クライアントからサーバーに対して SYN パケットを送ります。コネクション開始の要求を行います。
- サーバーからクライアントに対して SYN-ACK パケットを送ります。コネクション確立の承認を行います。
- クライアントからサーバーに対して ACK パケットを送ります。コネクション確立の承認を受け取ったことを伝えます。
コネクション終了
- クライアントからサーバーに FIN パケットを送りコネクションの終了をリクエストをリクエストします。
- サーバーからクライアントに FIN,ACK パケットを送り、コネクションの終了を承認します。
- クライアントからサーバーに ACK パケットを送り、コネクション終了の承認を受け取ったことを伝えます。
実装
コードベースで紹介します。詳しい実装はリポジトリをご覧ください。
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 の一部は理解できたかなと思いますが、未だに分からないことが多いという印象です。
(カーネルはすごいという気持ちになりました……)