LoginSignup
5
1

More than 3 years have passed since last update.

パケットフィルタの仕組みを理解しつつGoで特定のプロトコルパケットを取得(フィルタ)する

Last updated at Posted at 2020-03-21

理解しておくと良い前提知識

  • カーネル空間/ユーザー空間の違い
  • ネットワークの基礎
  • ソケットプログラミングの基礎

環境

$ go version
go version go1.13.5 linux/amd64

パケットフィルタの仕組み

特定のプロトコルパケットをフィルタする前にパケットフィルタの仕組みについて記す。
※cBPFとeBPFの違い、現在のBPFの機能がパケットフィルタだけではないとか、BPFの歴史/変遷などについてはこの記事では触れていませんのでご了承ください:bow:

例としてtcpdumpでのパケットフィルタを用いる。
tcpdumpなどのパケットキャプチャツールはソフトウェアとしてユーザ空間で動作します。
また、tcpdumpでキャプチャされているデータはカーネル空間からコピーされたデータとなっており、直接アプリケーションなどがやり取りするパケットを覗いているわけではありません。(そんなことしてたらオーバーヘッドがやばい)
ユーザランドでフィルタをしていてはカーネル空間から実際にフィルタしたいプロトコルのパケット以外のパケットも全てコピーしてしまう。
これを回避するためにカーネルのBPF(Berkeley Packet Filter)と呼ばれるパケットフィルタリング機能を使用している。
カーネル空間で仮想マシン(仮想的なレジスタマシン)を実行しフィルタをカーネル空間で実現している。

Goを用いてネットワークに流れるパケットを取得する

比較を行うために以下のコードを用いてフィルタを行わずパケットを取得します。
イーサネットフレームのタイプを用いて出力を制御。

package main

import (
    "encoding/binary"
    "flag"
    "fmt"
    "net"
    "os"
    "syscall"
)

// 他にも上位のプロトコルタイプは存在するがわかりやすくするため3種類のみ使用する
const (
    EthTypeArp  uint16 = 0x0806
    EthTypeIpv4 uint16 = 0x0800
    EthTypeIpv6 uint16 = 0x86dd
)

// MACヘッダ構造体
type EtherHeader struct {
    DstMacAddr net.HardwareAddr
    SrcMacAddr net.HardwareAddr
    ProtoType  uint16
}

// お約束のビッグエンディアン変換
func htons(host uint16) uint16 {
    return (host&0xff)<<8 | (host >> 8)
}

// イーサネットヘッダのタイプを見て出力の内容を制御
func analyzePacket(data []byte) {
    // パース
    dstMacAddr := data[:6]
    srcMacAddr := data[6:12]
    protoType := binary.BigEndian.Uint16(data[12:14])
    eh := &EtherHeader{
        DstMacAddr: dstMacAddr,
        SrcMacAddr: srcMacAddr,
        ProtoType:  protoType,
    }

    // ここでイーサネットヘッダーのタイプを判別
    switch eh.ProtoType {
    case EthTypeArp:
        fmt.Println("ARP Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    case EthTypeIpv4:
        fmt.Println("IPv4 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    case EthTypeIpv6:
        fmt.Println("IPv6 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    default:
        fmt.Println("Another Type Packet")
    }
}

func main() {
    // 引数のパース
    flag.Parse()

    // リンクレイヤ(L2)のヘッダを含むすべてのプロトコルパケットの生データを取得する
    fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
    if err != nil {
        fmt.Println(err.Error())
    }

    // 終了したときにファイルディスクリプタを閉じる
    defer syscall.Close(fd)

    // インターフェースのインデックスを取得する
    ifIndex, err := net.InterfaceByName(flag.Arg(0))
    if err != nil {
        fmt.Println(err.Error())
    }

    // インターフェースにソケットをバインドする
    addr := syscall.SockaddrLinklayer{Protocol: htons(syscall.ETH_P_ALL), Ifindex: ifIndex.Index}
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println(err.Error())
    }

    // プロミスキャス・モードで受信
    if err := syscall.SetLsfPromisc(flag.Arg(0), true); err != nil {
        fmt.Println(err.Error())
    }

    // ファイルの作成
    file := os.NewFile(uintptr(fd), "")

    // パケットをforループで読み続け出力
    for {
        buf := make([]byte, 2048)
        num, err := file.Read(buf)
        if err != nil {
            fmt.Println(err.Error())
            break
        } else {
            analyzePacket(buf[:num])
        }
    }
}

出力が制御できることを確認する。

$ sudo ./packet-analyzer eth1 # ネットワークインターフェースの名前
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> a0:56:f3:cc:89:31 dst-> 01:00:5e:00:00:fb
IPv6 Packet: src-> a0:56:f3:cc:89:31 dst-> 33:33:00:00:00:fb
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> d4:f5:47:26:8d:7f
IPv4 Packet: src-> d4:f5:47:26:8d:7f dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> d4:f5:47:26:8d:7f
IPv4 Packet: src-> a0:56:f3:cc:89:31 dst-> 01:00:5e:00:00:fb
IPv6 Packet: src-> a0:56:f3:cc:89:31 dst-> 33:33:00:00:00:fb
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
IPv4 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv6 Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
IPv6 Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80

特定のプロトコルをフィルタしてみる

先程記述したプログラムからARPパケットのみを出力するようBPF(LSF)を用いてフィルタしてみる。
プログラムに少し追記します。

package main

import (
    "encoding/binary"
    "flag"
    "fmt"
    "net"
    "os"
    "syscall"
)

const (
    EthTypeArp  uint16 = 0x0806
    EthTypeIpv4 uint16 = 0x0800
    EthTypeIpv6 uint16 = 0x86dd
)

type EtherHeader struct {
    DstMacAddr net.HardwareAddr
    SrcMacAddr net.HardwareAddr
    ProtoType  uint16
}

func htons(host uint16) uint16 {
    return (host&0xff)<<8 | (host >> 8)
}

func analyzePacket(data []byte) {
    dstMacAddr := data[:6]
    srcMacAddr := data[6:12]
    protoType := binary.BigEndian.Uint16(data[12:14])
    eh := &EtherHeader{
        DstMacAddr: dstMacAddr,
        SrcMacAddr: srcMacAddr,
        ProtoType:  protoType,
    }

    switch eh.ProtoType {
    case EthTypeArp:
        fmt.Println("ARP Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    case EthTypeIpv4:
        fmt.Println("IPv4 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    case EthTypeIpv6:
        fmt.Println("IPv6 Packet: src->", eh.SrcMacAddr, "dst->", eh.DstMacAddr)

    default:
        fmt.Println("Another Type Packet")
    }
}

func main() {
    flag.Parse()

    fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
    if err != nil {
        fmt.Println(err.Error())
    }

    defer syscall.Close(fd)

    ifIndex, err := net.InterfaceByName(flag.Arg(0))
    if err != nil {
        fmt.Println(err.Error())
    }

    addr := syscall.SockaddrLinklayer{Protocol: htons(syscall.ETH_P_ALL), Ifindex: ifIndex.Index}
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println(err.Error())
    }

    if err := syscall.SetLsfPromisc(flag.Arg(0), true); err != nil {
        fmt.Println(err.Error())
    }

    // ここでカーネルにフィルタを適用する
    // ARPパケットのみを扱う際の命令を定義し、適用する(ARP以外のパケットはアプリケーションで受信しない)
    rawInstructions := []syscall.SockFilter{
        {0x28, 0, 0, 0x0000000c},
        {0x15, 0, 1, 0x00000806},
        {0x6, 0, 0, 0x00040000},
        {0x6, 0, 0, 0x00000000},
    }
    if err := syscall.AttachLsf(fd, rawInstructions); err != nil {
        fmt.Println(err.Error())
    }

    file := os.NewFile(uintptr(fd), "")

    for {
        buf := make([]byte, 2048)
        num, err := file.Read(buf)
        if err != nil {
            fmt.Println(err.Error())
            break
        } else {
            analyzePacket(buf[:num])
        }
    }
}

出力がARPパケットのみになっていることを確認する。

$ sudo ./packet-analyzer eth1
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> d4:f5:47:26:8d:7f dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27
ARP Packet: src-> 3c:28:6d:d0:6f:e2 dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 6c:62:6d:e9:b4:9a dst-> ff:ff:ff:ff:ff:ff
ARP Packet: src-> 48:ad:08:d4:5e:27 dst-> 68:ec:c5:70:cd:80
ARP Packet: src-> 68:ec:c5:70:cd:80 dst-> 48:ad:08:d4:5e:27

追記したコードについての説明です。

下記でBPFの命令セットを定義します。
この命令セット(packet-matching code)はtcpdump -dd arpで得ることができます。

rawInstructions := []syscall.SockFilter{
    {0x28, 0, 0, 0x0000000c},
    {0x15, 0, 1, 0x00000806},
    {0x6, 0, 0, 0x00040000},
    {0x6, 0, 0, 0x00000000},
}

その後、syscall.AttachLsfを用いて命令をソケットにアタッチすることでフィルタすることができます。

if err := syscall.AttachLsf(fd, rawInstructions); err != nil {
    fmt.Println(err.Error())
}

google/gopacketでの実装

今回はsyscallを直接叩きましたがgoogle/gopacketではCGoを用いて実装されているようです!
libpcapPCAP_APIを呼び出して使用しています。

最後に

理解を深めるために実装を読んだり、BPFの歴史について調べたり、なかなか面白みがありました!
間違っている所などもあるかもしれませんが、その時はご教授いただけると幸いです。:bow:

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1