世は大コンテナ時代!!!

解説  

手元から192.168.9.1に対してcurlをしてみると、確かに応答が返ってきていないことが確認出来ます。また、コンテナの中でtcpdumpしてもパケットが見えないことが分かります。VMに対してSSHは出来ているはずなのでL3の疎通性には問題が無いはずです。つまり、XDPのプログラムで処理されてからtcpdumpで触れる部分までの間で何かパケットが消えるような問題が起こっているだろうと問題を切り分けることが出来ます。

そのために、後輩がどのようなプログラムを動かしていたのかを読んでみましょう。/opt/forwarderにはXDPのプログラムの動作に必要なファイルがまとめられています。Dockerfileを見てみると、/opt/forwarderにあるファイルを全てコンテナの中に入れています。また、CMDとして/controller.pyを指定していることが分かります。

# syntax=docker/dockerfile:experimental
FROM fedora:30

RUN --mount=type=cache,target=/var/cache/dnf dnf install -y bcc bpftool bpftrace clang iproute kmod python3-pyroute2

COPY * /

CMD "/controller.py"

controller.pyでは、forwarder.cやconfig.jsonを読み出し、コンパイルやNICへのアタッチを行っています。このプログラムはNICにプログラムをアタッチするために用意しているだけのコードなので、編集不可という制限をつけています。このプログラムには動作上問題になるようなものは含まれていません。

def main():
    with open("./forwarder.c", "r") as f:
        text = f.read()

    with open("./config.json", "r") as f:
        conf = json.loads(f.read())

    # eBPFプログラムやプログラム内の関数のロード
    b = BPF(text=text)
    ext_fn = b.load_func("entry_external", BPF.XDP)
    int_fn = b.load_func("entry_internal", BPF.XDP)

    # XDP_REDIRECTするためのdevmapの準備
    ip = IPRoute()
    devmap = b.get_table("devmap")
    for link in ip.get_links():
        idx = link["index"]
        devmap[c_int(idx)] = c_int(idx)

    # コンテナへの通信やコンテナからの通信をNATする際に用いるデータの準備
    dnat_entries = b.get_table("dnat_entries")
    snat_entries = b.get_table("snat_entries")

    for entry in conf["entries"]:
        f, t = get_endpoint(entry["from"]), get_endpoint(entry["to"])
        dnat_entries[f] = t
        snat_entries[t] = f

    # externalに指定されたインターフェースにext_fn(DNAT用のプログラム)をアタッチする
    for interface in conf["interfaces"]["external"]:
        b.attach_xdp(interface, ext_fn, 0)

    # externalに指定されたインターフェースにint_fn(SNAT用のプログラム)をアタッチする
    for interface in conf["interfaces"]["internal"]:
        b.attach_xdp(interface, int_fn, 0)

    # XDP内でprintkしたログを表示する
    b.trace_print()

そのため、実際に転送処理を行っているforwarder.cを見ていくことにしましょう。コメントにもある通りforwarder.cはDNAT用のentrypointとSNAT用のentrypointがあります。このentrypointからprocess_ethhdr関数を呼んで処理が開始されます。

// DNAT用のentrypoint
int entry_external(struct xdp_md *ctx) {
	return process_ethhdr(ctx, DIR_INBOUND);
}

// SNAT用のentrypoint
int entry_internal(struct xdp_md *ctx) {
	return process_ethhdr(ctx, DIR_OUTBOUND);
}

process_ethhdr関数は次のヘッダがIPかどうかを調べてからprocess_iphdr関数を呼び出します。そして、process_iphdr関数でも次のヘッダやパラメータを調べてからprocess_tcphdr関数を呼び出します。その後、process_tcphdr関数ではlookup_endpoint関数を呼び出します。assert_lenマクロはXDPの制約上必要になっているコードなので…

static inline int process_tcphdr(
		struct xdp_md *ctx, 
		struct ethhdr *eth,
		struct iphdr *ip,
		enum direction_t dir)
{
	void *data_end = (void *)(long)ctx->data_end;

	struct tcphdr *tcp = (struct tcphdr *)(ip + 1);
	assert_len(tcp, data_end);

	return lookup_endpoint(ctx, eth, ip, tcp, dir);
}

static inline int process_iphdr(
		struct xdp_md *ctx, 
		struct ethhdr *eth,
		enum direction_t dir)
{
	void *data_end = (void *)(long)ctx->data_end;

	struct iphdr *ip = (struct iphdr *)(eth + 1);
	assert_len(ip, data_end);

	if (ip->protocol != IPPROTO_TCP) return XDP_PASS;
	if (ip->version != 4) return XDP_DROP;
	if (ip->ihl != 5) return XDP_PASS;

	return process_tcphdr(ctx, eth, ip, dir);
}

static inline int process_ethhdr(
		struct xdp_md *ctx, 
		enum direction_t dir)
{
	void *data = (void *)(long)ctx->data;
	void *data_end = (void *)(long)ctx->data_end;

	struct ethhdr *eth = data;
	assert_len(eth, data_end);

	if (eth->h_proto != htons(ETH_P_IP)) return XDP_PASS;

	return process_iphdr(ctx, eth, dir);
}

lookup_endpoint関数では、dirの内容に合わせてdnat_entries, dnat_entriesをルックアップします。例えば、DIR_INBOUND(外部からVMにパケットが来た場合)はデスティネーションアドレスやポート番号を基にNATする先のアドレスやポート番号をルックアップします。その結果、値がない(val == NULL)である場合には、NATすることが出来ないため、XDP_PASSしてパケットの処理をカーネルに依頼します。NAT先がある場合、lookup_nexthop関数を呼び出します。

static inline int lookup_endpoint(
		struct xdp_md *ctx, 
		struct ethhdr *eth,
		struct iphdr *ip,
		struct tcphdr *tcp,
		enum direction_t dir)
{
	struct endpoint_t key = {}, *val = NULL;

	switch (dir) {
		case DIR_INBOUND:
			key.addr = ntohl(ip->daddr);
			key.port = ntohs(tcp->dest);
			val = dnat_entries.lookup(&key);
			break;
		case DIR_OUTBOUND:
			key.addr = ntohl(ip->saddr);
			key.port = ntohs(tcp->source);
			val = snat_entries.lookup(&key);
			break;
	}

	if (val == NULL) return XDP_PASS;
	return lookup_nexthop(ctx, eth, ip, tcp, val, dir);
}

lookup_nexthop関数ではstruct bpf_fib_lookup構造体に値を設定してからbpf_fib_lookup関数を呼び出します。この関数はカーネルの中で用意されているヘルパー関数でLinuxのFIBをルックアップすることが出来る関数です。もしその関数の返り値がBPF_FIB_LKUP_RET_NOT_FWDEDならパケットを転送する必要がない、つまりコンテナからパケットが転送されてきたけどVMで受け取るという状態なので、NATせずにパケットの処理をカーネルに依頼します。そうでないエラーコードの場合、カーネルに依頼しても意味がないのでこの段階でDropします。無事bpf_fib_lookup関数でパケットの転送先が分かったら、rewrite_packet関数を呼び出してパケットの書き換えを行います。

static inline int lookup_nexthop(
		struct xdp_md *ctx, 
		struct ethhdr *eth,
		struct iphdr *ip,
		struct tcphdr *tcp,
		struct endpoint_t *val,
		enum direction_t dir)
{
	struct bpf_fib_lookup params = {};
	params.family = AF_INET;
	params.ifindex = ctx->ingress_ifindex;

	switch (dir) {
		case DIR_INBOUND:
			params.ipv4_src = ip->saddr;
			params.ipv4_dst = htonl(val->addr);
			break;
		case DIR_OUTBOUND:
			params.ipv4_src = htonl(val->addr);
			params.ipv4_dst = ip->daddr;
			break;
		default:
			return XDP_DROP;
	}

	int ret = bpf_fib_lookup(ctx, &params, sizeof(params), 0);
	switch (ret) {
		case BPF_FIB_LKUP_RET_NOT_FWDED:
			return XDP_PASS;
		case BPF_FIB_LKUP_RET_FWD_DISABLED:
		case BPF_FIB_LKUP_RET_BLACKHOLE:
		case BPF_FIB_LKUP_RET_UNREACHABLE:
		case BPF_FIB_LKUP_RET_PROHIBIT:
		case BPF_FIB_LKUP_RET_FRAG_NEEDED:
		case BPF_FIB_LKUP_RET_UNSUPP_LWT:
			return XDP_DROP;
	}

	return rewrite_packet(ctx, eth, ip, tcp, val, &params, dir);
}

rewrite_packet関数では得られた情報を基にひたすらパケットを書き換えていきます。最後にdevmapをルックアップして、パケットをnexthopのあるI/Fにリダイレクトしています。

static inline int rewrite_packet(
		struct xdp_md *ctx, 
		struct ethhdr *eth,
		struct iphdr *ip,
		struct tcphdr *tcp,
		struct endpoint_t *val,
		struct bpf_fib_lookup *params,
		enum direction_t dir)
{
	__be64 ip_check, tcp_check, l3_diff, l4_diff;

	switch (dir) {
		case DIR_INBOUND:
			l3_diff = (~ip->daddr) & 0xffffffff;
			l4_diff = (~tcp->dest) & 0xffff;
			ip->daddr = htonl(val->addr);
			tcp->dest = htons(val->port);
			break;
		case DIR_OUTBOUND:
			l3_diff = (~ip->saddr) & 0xffffffff;
			l4_diff = (~tcp->source) & 0xffff;
			ip->saddr = htonl(val->addr);
			tcp->source = htons(val->port);
			break;
		default:
			return XDP_DROP;
	}

	l3_diff += htonl(val->addr);
	l4_diff += htons(val->port);

	l4_diff += l3_diff;

	ip_check = (~ip->check & 0xffff) + l3_diff;
	ip->check = ~fold_csum(ip_check) & 0xffff;

	tcp_check = (~tcp->check & 0xffff) + l4_diff;
	tcp->check = ~fold_csum(tcp_check) & 0xffff;

	for (int i = 0; i < 6; i++) {
		eth->h_source[i] = params->smac[i];
		eth->h_dest[i] = params->dmac[i];
	}

	return devmap.redirect_map(params->ifindex, 0);
}

一見すると正しく動いてくれそうに見えます。そのため、forwarder.cを書き換えてパケットの中身を調べてみましょう。

    bpf_trace_rintk("===\n");
    bpf_trace_printk("ip->saddr: %x\n, ip->saddr);
    bpf_trace_printk("ip->daddr: %x\n, ip->daddr);
    bpf_trace_printk("tcp->source: %x\n, tcp->source);
    bpf_trace_printk("tcp->dest: %x\n, tcp->dest);
    bpf_trace_printk("params->ifindex: %d\n, params->ifindex);
    
	return devmap.redirect_map(params->ifindex, 0);

再度コンテナをスタートし、ログを見てみると次のような表示が得られます。ネットワークパケットはビッグエンディアンであることに注意しながらこのログを見ていくと、最終的に192.168.9.254:55802から10.123.1.1:80へのパケットだということが分かります。また、params->ifindex(転送先のI/Fのインデックス)は4になっています。

$ sudo docker logs -f forwarder
b'          <idle>-0     [000] ..s.   324.937829: 0: ==='
b'          <idle>-0     [000] .Ns.   324.938507: 0: ip->saddr: fe09a8c0'
b'          <idle>-0     [000] .Ns.   324.938515: 0: ip->daddr: 1017b0a'
b'          <idle>-0     [000] .Ns.   324.938516: 0: tcp->source: fa9d'
b'          <idle>-0     [000] .Ns.   324.938516: 0: tcp->dest: 5000'
b'          <idle>-0     [000] .Ns.   324.938517: 0: params->ifindex: 4'
b'          <idle>-0     [000] ..s.   325.946305: 0: ==='
b'          <idle>-0     [000] .Ns.   325.946384: 0: ip->saddr: fe09a8c0'
b'          <idle>-0     [000] .Ns.   325.946385: 0: ip->daddr: 1017b0a'
b'          <idle>-0     [000] .Ns.   325.946385: 0: tcp->source: fa9d'
b'          <idle>-0     [000] .Ns.   325.946386: 0: tcp->dest: 5000'
b'          <idle>-0     [000] .Ns.   325.946387: 0: params->ifindex: 4'
b'          <idle>-0     [000] ..s.   327.962426: 0: ==='
b'          <idle>-0     [000] .Ns.   327.962508: 0: ip->saddr: fe09a8c0'
b'          <idle>-0     [000] .Ns.   327.962509: 0: ip->daddr: 1017b0a'
b'          <idle>-0     [000] .Ns.   327.962510: 0: tcp->source: fa9d'
b'          <idle>-0     [000] .Ns.   327.962510: 0: tcp->dest: 5000'
b'          <idle>-0     [000] .Ns.   327.962511: 0: params->ifindex: 4'
b'          <idle>-0     [000] ..s.   332.186120: 0: ==='
b'          <idle>-0     [000] .Ns.   332.186190: 0: ip->saddr: fe09a8c0'
b'          <idle>-0     [000] .Ns.   332.186191: 0: ip->daddr: 1017b0a'
b'          <idle>-0     [000] .Ns.   332.186192: 0: tcp->source: fa9d'
b'          <idle>-0     [000] .Ns.   332.186192: 0: tcp->dest: 5000'
b'          <idle>-0     [000] .Ns.   332.186193: 0: params->ifindex: 4'
^C

ipコマンドを使ってifindexが4のデバイスを調べてみるとve-m1だということが分かります。宛先が10.123.1.1:80のパケットがve-m1から送出されようとしているのは普通に考えると正しい挙動になっていそうだということが分かります。実際、forwarder.cは動作上問題になるようなものは含まれていません。

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:12 qdisc mq state UP group default qlen 1000
    link/ether 52:54:9a:e6:3d:83 brd ff:ff:ff:ff:ff:ff
    inet 192.168.9.1/24 brd 192.168.9.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:9aff:fee6:3d83/64 scope link 
       valid_lft forever preferred_lft forever
3: ve-m2@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:13 qdisc noqueue state UP group default qlen 1000
    link/ether e2:00:a0:72:c7:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 169.254.248.252/16 brd 169.254.255.255 scope link ve-m2
       valid_lft forever preferred_lft forever
    inet 10.123.2.254/24 scope global ve-m2
       valid_lft forever preferred_lft forever
4: ve-m1@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:13 qdisc noqueue state UP group default qlen 1000
    link/ether a2:5e:3c:30:e4:23 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 169.254.87.30/16 brd 169.254.255.255 scope link ve-m1
       valid_lft forever preferred_lft forever
    inet 10.123.1.254/24 scope global ve-m1
       valid_lft forever preferred_lft forever
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:58:3a:60:6f brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever

では、正しくXDPのプログラムでの処理が行われたとすると、パケットはXDPのプログラムを離れてからtcpdumpで見えるまで、つまりソケットのレイヤーに到達する前のどこかで消えているだろうと問題の切り分けができます。

では、XDPのプログラムを離れる地点はどこで、その後パケットはどのような流れを追ってtcpdumpが行われている地点まで流れていくのでしょうか?それを追ってみてきましょう。eth0はvirtioドライバでLinuxカーネルに認識されているので/drivers/net/virtio_net.cを見ていきます。

Linuxではパケットの受信処理の際にはNAPIと呼ばれる仕組みを用いてパケットの受信処理を行っています。この仕組みのおかげで輻輳している状態でもスラッシングが起こらないなどの利点があります。このNAPIは大抵のNICのドライバーで利用されています。このNAPIを有効にするためにドライバーではnetif_napi_add関数を呼ぶ必要があります。virtio_netではこの部分でnetif_napi_add関数を呼んでいます。NAPIのポーリング用の関数としてvirtnet_poll関数が指定されているので、それを起点に読み進めていきます。

	INIT_DELAYED_WORK(&vi->refill, refill_work);
	for (i = 0; i < vi->max_queue_pairs; i++) {
		vi->rq[i].pages = NULL;
		netif_napi_add(vi->dev, &vi->rq[i].napi, virtnet_poll,
			       napi_weight);
		netif_tx_napi_add(vi->dev, &vi->sq[i].napi, virtnet_poll_tx,
				  napi_tx ? napi_weight : 0);

		sg_init_table(vi->rq[i].sg, ARRAY_SIZE(vi->rq[i].sg));
		ewma_pkt_len_init(&vi->rq[i].mrg_avg_pkt_len);
		sg_init_table(vi->sq[i].sg, ARRAY_SIZE(vi->sq[i].sg));

		u64_stats_init(&vi->rq[i].stats.syncp);
		u64_stats_init(&vi->sq[i].stats.syncp);
	}

virtnet_poll関数からvirtnet_receive関数, receive_buf関数, receive_small関数と進んでいくと、xdp_progという変数が見えてきます。この関数の中でbpf_prog_run_xdp関数が呼ばれXDPのプログラムが実行されます。

		xdp.data_hard_start = buf + VIRTNET_RX_PAD + vi->hdr_len;
		xdp.data = xdp.data_hard_start + xdp_headroom;
		xdp_set_data_meta_invalid(&xdp);
		xdp.data_end = xdp.data + len;
		xdp.rxq = &rq->xdp_rxq;
		orig_data = xdp.data;
		act = bpf_prog_run_xdp(xdp_prog, &xdp);
		stats->xdp_packets++;

その後、actに従ってパケットの処理の先が変わります。今回はXDP_REDIRECTしているはずなので、その部分の処理を見ていきます。xdp_do_redirect関数が呼ばれていることが分かるのでその先を見ていきます。

		case XDP_REDIRECT:
			stats->xdp_redirects++;
			err = xdp_do_redirect(dev, &xdp, xdp_prog);
			if (err)
				goto err_xdp;
			*xdp_xmit |= VIRTIO_XDP_REDIR;
			rcu_read_unlock();
			goto xdp_xmit;

xdp_do_redirect関数からxdp_do_redirect_map関数, __bpf_tx_xdp_map関数, dev_map_enqueue関数, bq_enqueue関数へと処理が進んでいきます。この関数内のlist_add関数によってXDPのパケットはリストに繋がれてパケットがflushされるのを待ちます。

このflush_listで検索をかけてみると、__dev_map_flush関数という関数が見つけられます。この関数から呼ばれているbq_xmit_all関数を見てみるとこの行で転送先デバイスに紐付けられたnetdev_opsのndo_xdp_xmit関数を呼び出していることが分かります。

	sent = dev->netdev_ops->ndo_xdp_xmit(dev, bq->count, bq->q, flags);

今はvethに対してリダイレクトしているので、vethのnet_device_opsを見てみましょう。

static const struct net_device_ops veth_netdev_ops = {
	.ndo_init            = veth_dev_init,
	.ndo_open            = veth_open,
	.ndo_stop            = veth_close,
	.ndo_start_xmit      = veth_xmit,
	.ndo_get_stats64     = veth_get_stats64,
	.ndo_set_rx_mode     = veth_set_multicast_list,
	.ndo_set_mac_address = eth_mac_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
	.ndo_poll_controller	= veth_poll_controller,
#endif
	.ndo_get_iflink		= veth_get_iflink,
	.ndo_fix_features	= veth_fix_features,
	.ndo_features_check	= passthru_features_check,
	.ndo_set_rx_headroom	= veth_set_rx_headroom,
	.ndo_bpf		= veth_xdp,
	.ndo_xdp_xmit		= veth_xdp_xmit,
};

すると、veth_xdp_xmitと呼ばれる関数が紐付いていることが分かります。この関数の内部で次のようなコードがあります。

	/* Non-NULL xdp_prog ensures that xdp_ring is initialized on receive
	 * side. This means an XDP program is loaded on the peer and the peer
	 * device is up.
	 */
	if (!rcu_access_pointer(rq->xdp_prog)) {
		ret = -ENXIO;
		goto drop;
	}

xdp_progはそのデバイスに紐付いているXDPのプログラムへのポインタです。それがNULLの時には-ENXIO(-6)を返してDropしている処理が入っています。実際に、bpftraceと呼ばれるツールを用いてこの関数からどんな値が返ってきているのかを調べてみると、-6が返ってきていることが分かります。

# bpftrace -e 'kretprobe:veth_xdp_xmit { printf("retval = %d\n", retval); }'
Attaching 1 probe...
retval = -6
retval = -6
^C

では、受信するvethのデバイスにXDPのプログラムがアタッチされていれば良さそうなので、プログラムを用意します。ここではXDP_PASS(2)を返すだけのプログラムを用意し、それをコンテナの中のNICにアタッチします。

$ cat main.c
int f() {
    return 2;
}
EOF
$ clang --target=bpf -c main.c

コンテナはただ単に様々なnamespaceを分離してプロセスのisolationをしているだけなので、コンテナ外から見ると普通にプロセスツリーの中にsystemdがあることが分かります。また、nsenterコマンドを用いることで、あるPIDのプロセスのnet namespaceに入るというような処理が可能です。そのため、コンテナの中にプログラムをattachするには次のようにすればよいです。

$ for pid in $(pgrep systemd-nspawn | xargs -n 1 pgrep -P); do
    sudo nsenter --net --target $pid ip link set xdp object main.o section .text dev host0
done

この状態でコンテナの中でtcpdumpしてみると次のような表示が得られます。

# tcpdump -vni host0
tcpdump: listening on host0, link-type EN10MB (Ethernet), capture size 262144 bytes
17:59:42.029623 IP (tos 0x0, ttl 64, id 38097, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.9.254.40454 > 10.123.1.1.80: Flags [S], cksum 0x2338 (correct), seq 1532198029, win 29200, options [mss 1460,sackOK,TS val 1575487606 ecr 0,nop,wscale 7], length 0
17:59:42.029745 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.123.1.1.80 > 192.168.9.254.40454: Flags [S.], cksum 0xd650 (incorrect -> 0xc332), seq 3121682320, ack 1532198030, win 65160, options [mss 1460,sackOK,TS val 1668587109 ecr 1575487606,nop,wscale 7], length 0
17:59:43.035987 IP (tos 0x0, ttl 64, id 38098, offset 0, flags [DF], proto TCP (6), length 60)
    192.168.9.254.40454 > 10.123.1.1.80: Flags [S], cksum 0x1f4a (correct), seq 1532198029, win 29200, options [mss 1460,sackOK,TS val 1575488612 ecr 0,nop,wscale 7], length 0
17:59:43.036068 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.123.1.1.80 > 192.168.9.254.40454: Flags [S.], cksum 0xd650 (incorrect -> 0xbf43), seq 3121682320, ack 1532198030, win 65160, options [mss 1460,sackOK,TS val 1668588116 ecr 1575487606,nop,wscale 7], length 0
17:59:43.325308 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.123.1.1.80 > 192.168.9.254.40450: Flags [S.], cksum 0xd650 (incorrect -> 0x92fa), seq 3503547284, ack 4128794799, win 65160, options [mss 1460,sackOK,TS val 1668588405 ecr 1575471605,nop,wscale 7], length 0
17:59:43.581414 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    10.123.1.1.80 > 192.168.9.254.40452: Flags [S.], cksum 0xd650 (incorrect -> 0x47c2), seq 2126030842, ack 2787864494, win 65160, options [mss 1460,sackOK,TS val 1668588661 ecr 1575480015,nop,wscale 7], length 0
^C

注目すべき点はこの部分です。送信しているパケットのchecksumが合っていないといわれています。tx checksum offloadingが有効になっているとLinuxカーネルでは計算を行わずにハードウェアや先のデバイスでchecksumの計算を行うような仕組みになっています。そのため、送信時にchecksumがあっていないと表示されます。ですが、XDPのプログラムでは差分アップデートのみを行っているためchecksumが合わずパケットが経路のどこかでDropしてしまいます。

    10.123.1.1.80 > 192.168.9.254.40454: Flags [S.], cksum 0xd650 (incorrect -> 0xc332), seq 3121682320, ack 1532198030, win 65160, options [mss 1460,sackOK,TS val 1668587109 ecr 1575487606,nop,wscale 7], length 0

これに対応するために、vethのtx checksum offloadingを無効にします。

$ for pid in $(pgrep systemd-nspawn | xargs -n 1 pgrep -P); do
    sudo nsenter --net --target $pid ethtool -K host0 tx off
done

これでパケットが無事返ってくることが確認できます。

採点基準  

  • XDPとvethに起因する問題(70%)
    • setup-veth.serviceをrestartしてアドレスを振る(10%)
    • 通信が行えていない理由についてなんらかの言及をする(20%)
      • XDPのプログラムでは適切に処理が行われていることに言及する(10%)
        • devmap.redirect_mapでは適切なI/FにXDP_REDIRECTしていることに言及する
        • bpf_fib_lookupでは適切なルーティング結果が返ってきている
      • netns内ではパケットが受信できていないことに言及する(10%)
        • tcpdumpをしてみてもパケットが流れていない
    • vethのpeerにXDPのプログラムがattachされていないことに言及する(30%)
    • vethのpeerに通信に影響のないXDPのプログラムをattachできる(10%)
  • vethのtx checksum offloadingに起因する問題(30%)
    • netns内のvethのchecksumがおかしいことに言及する(10%)
    • netns内のvethのtx checksum offloadを無効化する(20%)
      • ソースコードを書き換えてchecksumを計算しても良い

作問者からのコメント  

この問題はいわゆる”全完絶対阻止する問”としてインターン期間中に遭遇した問題をアレンジした問題です。XDPのプログラムでは問題がなさそうという部分まで言及できているチームは1チームだけありましたが、peerのvethにXDPのプログラムがattachされていないことが原因だとは気づいていないと思います。Linuxカーネル内部のネットワーク処理の流れをある程度知っていないと解けない問題ですが、Linuxカーネルはすごく面白いのでぜひ興味を持ってほしいなと思います。