IETF発 ! 技術ハッカソンをやってみようの参加レポート
はじめに
JPNICさん主催で行われたIETF発 ! 技術ハッカソンをやってみように参加してきたのでレポートします。
今回のイベントではSRv6をテーマにして作ってみようというハッカソンが行われました。
SRv6とは
普段は僕はネットワーク屋さんではないので、SRv6とはなんぞやとChantGPTに聞いたりRFC読んだりして事前にキャッチアップしました。
端的にいうと普段IPルーティングは途中のルータのルーティングテーブルを参照してパケットが転送されていきますが、任意の箇所を通過するように、
IPv6のNext HeaderにIPv6アドレスのリストを入れるとそのリストを通過するようにルーティングを制御できる仕組みです。(自分の理解)
LinuxはSRv6はをサポートしているので、ip netns
コマンドでNetwork Namespaceを作って動作確認をすることができます。
以下のBlogがとても参考になりました。記事中にあるスクリプトでNetwork Namespaceを作ってpingコマンドを実行すると挙動がわかります。
作成したもの
pingコマンドではSengment Routingを直接指定することはできませんが、SRv6ヘッダを付与してpingを送れたら便利かな?と思いコマンドを作成しました。
解説
実装はGoを使いました。
Goでパケットを操作するライブラリとして gopacket がありますが、SRv6は対応していないので自分でSRv6用の構造体を定義します。
type segmentRoutingHeader struct {
nextHeader uint8
hdrLen uint8
routingType uint8
segLeft uint8
lastEntry uint8
flags uint8
tags []byte
segmentList []net.IP
}
自分で定義した型に SerializeTo
を実装しておくと gopacket がいい感じにbytesのパケットデータにしてくれるので実装します。
func (srh *segmentRoutingHeader) SerializeTo(b gopacket.SerializeBuffer, opts gopacket.SerializeOptions) error {
bytes, err := b.PrependBytes(8 + len(srh.segmentList)*16)
if err != nil {
return err
}
bytes[0] = srh.nextHeader
bytes[1] = srh.hdrLen
bytes[2] = srh.routingType
bytes[3] = srh.segLeft
bytes[4] = srh.lastEntry
bytes[5] = srh.flags
copy(bytes[6:7], srh.tags)
for i, ip := range srh.segmentList {
offset := 8 + i*16
copy(bytes[offset:offset+16], ip.To16())
}
return nil
}
このSRv6ヘッダを含めたICMPv6のping要求のパケットを作成する関数を作ります。
func createICMPv6EchoRequest(icmpReq icmpRequestInfo) []byte {
// Ethernetヘッダを作成
ethernet := &layers.Ethernet{
SrcMAC: icmpReq.srcMac,
DstMAC: parseMac("62:3b:ab:c6:56:de"), // r2 mac addr
EthernetType: layers.EthernetTypeIPv6,
}
// IPv6ヘッダを作成
ip6 := &layers.IPv6{
Version: 6,
NextHeader: layers.IPProtocolIPv6Routing,
//NextHeader: layers.IPProtocolICMPv6,
HopLimit: 64,
FlowLabel: 0xb3124,
SrcIP: net.ParseIP(icmpReq.srcIp), // "fc00:a::1"
DstIP: net.ParseIP(icmpReq.srv6Ip), // "fc00:e::2"
}
segList := []net.IP{
net.ParseIP(icmpReq.destIp), // "fc00:d::2"
net.ParseIP(icmpReq.srv6Ip), // "fc00:e::2"
}
// Segment Routing Headerを作成
srh := segmentRoutingHeader{
nextHeader: uint8(layers.IPProtocolICMPv6),
hdrLen: uint8(len(segList) * 16 / 8),
routingType: 4,
segLeft: 1,
lastEntry: 1,
flags: 0,
tags: []byte{0x00, 0x00},
segmentList: segList,
}
// ICMPv6 Echo Requestの作成
icmp6 := &layers.ICMPv6{
TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeEchoRequest, 0),
Checksum: 0x879c - icmpReq.seq,
}
// ICMPv6 Echo RequestのPayload
var payload []byte
payload = append(payload, []byte{0x00, 0x01}...) // identifier
payload = append(payload, []byte{0x00, byte(icmpReq.seq)}...) // sequence
payload = append(payload, []byte{0x00, 0x00, 0x00, 0x00}...) // payload
// パケットのバッファを作成
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: false,
}
// パケットをシリアライズ
err := gopacket.SerializeLayers(buf, opts, ethernet, ip6, &srh, icmp6, gopacket.Payload(payload))
if err != nil {
log.Fatal(err)
}
return buf.Bytes()
}
main関数ではPing要求を送信して、応答があればPrintします。
func main() {
var iface = flag.String("I", "r1-r2", "Interface to read packets from")
var sr = flag.String("sr", "fc00:e::2", "SRv6 Header IPv6 Addr")
var srcIp = flag.String("src", "fc00:a::1", "Source IPv6 Addr")
flag.Parse()
args := flag.Args()
if len(args) != 1 {
fmt.Println("Usage: ping-srv6 -I <interface> <target>")
os.Exit(1)
}
dstIP := args[0]
pingInterval := 1 * time.Second
handle, err := pcap.OpenLive(*iface, 1600, true, pcap.BlockForever)
if err != nil {
log.Fatal(err)
}
defer handle.Close()
// Pingのシーケンス番号
seq := uint16(0)
// Pingを送信する
go func() {
for {
time.Sleep(pingInterval)
echoRequest := createICMPv6EchoRequest(icmpRequestInfo{
srcMac: getMacAddr(*iface),
srcIp: *srcIp,
destIp: dstIP,
srv6Ip: *sr,
seq: seq,
})
seq++
// パケットを送信
if err := handle.WritePacketData(echoRequest); err != nil {
log.Fatalf("Failed to send packet: %v", err)
}
}
}()
for {
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
for packet := range packetSource.Packets() {
icmpLayer := packet.Layer(layers.LayerTypeICMPv6)
reply := icmpLayer.(*layers.ICMPv6)
if reply.TypeCode.Type() == layers.ICMPv6TypeEchoReply {
fmt.Printf("recieve echo reply from %s\n", dstIP)
break
}
}
}
}
動作確認を行います。
SRv6のBlog記事にあるスクリプトを実行するとNetwork Namespaceを用いて以下のような環境が作成されます。
スクリプトで以下の処理により、fc00:d::/64
宛てのパケットに fc00:e::2
がSegment Listに足されます。
for mode in encap inline
do
# add seg6 route
ip netns exec r1 ip -6 route del fc00:d::/64
ip netns exec r1 ip -6 route add fc00:d::/64 encap seg6 mode $mode \
segs fc00:e::2 dev r1-r2
ip netns exec r1 ip -6 route show
done
この状態でhost1からhost2にpingを実行すると、r1を通過するときにSegment Listが足されるので、r4を経由することになります。
これと同じように動作するように自作プログラムでpingを送信してみます。
gitからcloneして環境の作成とプログラムをビルドします。
$ git clone https://github.com/sat0ken/go-ping-srv6.git
$ cd go-ping-srv6
$ sudo ./test_srv6.sh
$ go build -o ping-srv6 .
プログラムを実行します。
$ sudo ip netns exec r1 ./ping-srv6 fc00:d::2
recieve echo reply from fc00:d::2
recieve echo reply from fc00:d::2
Segment Listに指定したRouter4でtcpdumpをすると、pingが通っていることがわかります。
$ sudo ip netns exec r4 tcpdump -l
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on r4-r2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
11:14:22.655283 IP6 fc00:a::1 > fc00:e::2: RT6 (len=4, type=4, segleft=1, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 6, length 12
11:14:22.655310 IP6 fc00:a::1 > fc00:d::2: RT6 (len=4, type=4, segleft=0, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 6, length 12
11:14:23.656267 IP6 fc00:a::1 > fc00:e::2: RT6 (len=4, type=4, segleft=1, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 7, length 12
11:14:23.656294 IP6 fc00:a::1 > fc00:d::2: RT6 (len=4, type=4, segleft=0, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 7, length 12
11:14:24.657036 IP6 fc00:a::1 > fc00:e::2: RT6 (len=4, type=4, segleft=1, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 8, length 12
11:14:24.657064 IP6 fc00:a::1 > fc00:d::2: RT6 (len=4, type=4, segleft=0, last-entry=1, tag=0, [0]fc00:d::2, [1]fc00:e::2) ICMP6, echo request, id 1, seq 8, length 12
おわりに
というわけでJPNICさん主催のハッカソンに参加して、SRv6に対応させたpingコマンドを作ってみました。
SRv6自体知らなかったので勉強になりましたし、IETFでは仕様について議論するだけでなくこんなこともやっているんだなーと知る一助になりました。
運営と参加された皆様お疲れ様でした!
Discussion