Zenn
🐬

IETF発 ! 技術ハッカソンをやってみようの参加レポート

2025/02/18に公開

はじめに

JPNICさん主催で行われたIETF発 ! 技術ハッカソンをやってみように参加してきたのでレポートします。
今回のイベントではSRv6をテーマにして作ってみようというハッカソンが行われました。

https://jpnic.connpass.com/event/343083/

SRv6とは

普段は僕はネットワーク屋さんではないので、SRv6とはなんぞやとChantGPTに聞いたりRFC読んだりして事前にキャッチアップしました。

https://tex2e.github.io/rfc-translater/html/rfc8754.html

端的にいうと普段IPルーティングは途中のルータのルーティングテーブルを参照してパケットが転送されていきますが、任意の箇所を通過するように、
IPv6のNext HeaderにIPv6アドレスのリストを入れるとそのリストを通過するようにルーティングを制御できる仕組みです。(自分の理解)

LinuxはSRv6はをサポートしているので、ip netns コマンドでNetwork Namespaceを作って動作確認をすることができます。
以下のBlogがとても参考になりました。記事中にあるスクリプトでNetwork Namespaceを作ってpingコマンドを実行すると挙動がわかります。

https://blog.bobuhiro11.net/2021/01-17-srv6linux.html

作成したもの

pingコマンドではSengment Routingを直接指定することはできませんが、SRv6ヘッダを付与してpingを送れたら便利かな?と思いコマンドを作成しました。

https://github.com/sat0ken/go-ping-srv6

解説

実装は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

ログインするとコメントできます