日本語版
最新ニュース
科学&テクノロジー

EBPFミステリー:IPv4はいつIPv4ではありませんか? IPv6のふりをしているとき!

このアドベンチャーは、単一のプログラム(またはDockerコンテナ)のポート53でDNSリクエストを透過的にリダイレクトする簡単なEBPFプログラムから始まります。 これを行うために私は使用しました BPF_CGROUP_INET4_CONNECT に cgroup。これにより、いつトラフィックを検査してリダイレクトできます syscall.connect 内部から発生します cgroup。これが簡素化されたバージョンです int handle_connect_redirect(struct bpf_sock_addr *ctx, __be32 original_ip, bool is_connect4, struct redirect_result *result) { __be32 new_ip = original_ip; __be16 new_port = ctx->user_port; if (ctx->user_port == bpf_htons(53)) { new_ip = const_mitm_proxy_address; // Our…

EBPFミステリー:IPv4はいつIPv4ではありませんか? IPv6のふりをしているとき!

1746769119
2025-05-09 03:59:00

このアドベンチャーは、単一のプログラム(またはDockerコンテナ)のポート53でDNSリクエストを透過的にリダイレクトする簡単なEBPFプログラムから始まります。

これを行うために私は使用しました BPF_CGROUP_INET4_CONNECTcgroup。これにより、いつトラフィックを検査してリダイレクトできます syscall.connect 内部から発生します cgroup。これが簡素化されたバージョンです

int handle_connect_redirect(struct bpf_sock_addr *ctx, __be32 original_ip,
                            bool is_connect4, struct redirect_result *result) {
  __be32 new_ip = original_ip;
  __be16 new_port = ctx->user_port;

  if (ctx->user_port == bpf_htons(53)) {
    new_ip = const_mitm_proxy_address; // Our MITM DNS server we're using for intercept
    new_port = bpf_htons(const_dns_proxy_port);
  }

  result->is_redirected = did_redirect;
  result->ip = new_ip;
  result->port = new_port;
  return 1;
}

SEC("cgroup/connect4")
int connect4(struct bpf_sock_addr *ctx) {
  struct redirect_result r = {
      .ip = ctx->user_ip4,
      .port = ctx->user_port,
      .is_redirected = false,
  };
  handle_connect_redirect(ctx, ctx->user_ip4, true, &r);
  if (r.is_redirected) {
    // If we redirected the request then we need to update the socket
    // destination to the new IP and port
    ctx->user_ip4 = r.ip;
    ctx->user_port = r.port;
  }
  return 1;
}

プログラムを実行しているマシンにはIPv6サポートがないため、私の仮定は、ベースをカバーするということでした。

次に、Anを使用しました BPF_PROG_TYPE_CGROUP_SKB EBPFプログラム 直接IP呼び出しを行うことでリダイレクトを回避できないことを確認します。大まかにこれは👇のように見えました

SEC("cgroup_skb/egress")
int cgroup_skb_egress(struct __sk_buff *skb) {
  // Block IPv6 traffic. Currently not supported.
  if (skb->family == AF_INET6) {
    struct event info = {
        ...
        .eventType = PACKET_IPV6_TYPE,
    };

    bpf_ringbuf_output(&events, &info, sizeof(info), 0);

    return EGRESS_DENY_PACKET;
  }

  // .... then we'd check if outbound ip was allowed and deny/allow ...

注:私は使用しています bpf_ringbuf_output ここでは、EBPFプログラムからイベントを追跡し、ユーザースペースからそれらをログに記録します。これは、このバグを追跡するために非常に貴重でしたが、それらがなければ、ProgAMのEBPF部分内で何が起こっているかについて推論するのは非常に難しいでしょう。

すべてが非常にうまくいっていました、 ユーザーが試してみるまで dotnet cli。

彼らが走ったとき dotnet add package x それは無期限に吊るし、多くを生成します PACKET_IPV6_TYPE 介してメッセージをブロックしました ringbuf_output

OH DOTNETはIPv6を使用しています

明らかな結論は、マシンがどういうわけかIPv6リクエストを行っていたので、私はいくつかの掘削をしました

  • ❓私 connect4 EBPFプログラムは打撃を受けていませんでした、私は同様に付け加えました bpf_ringbuf_output イベントがストリーミングしてユーザースペースをログインできるようにすることができます
  • wiresharkを接続して、私はそれを確認しました dotnet IPv6をボックスから呼び出していなかったので、ボックスはIPv6リクエストをインターネットにリクエストすることができませんでした

今、私は困惑しています。

  • ネットワークトラフィックは、IPv4呼び出しが出ていることを示しています
  • egress EBPFプログラムはIPv6を示しています
  • connect4 EBPFプログラムは、IPv4がないことを示唆していません connect 通話が行われます

何!これらはお互いに矛盾しています!

続きを読むカーネルと dotnet ソースコード

この時点で、私は誤解していたものがなければならないことを知っていました。

私はカーネル、EBPF、および dotnet 私が作ったものを見つけることができるかどうかを見るために dotnet cli 他のツールは影響を受けなかったように特別なものです。

プログラムを変更し始めて詳細情報を入手しました。 connect6 EBPFプログラムとこれがヒットするフック syscall.connect IPv6で。うまくいけば、それはthasを確認するでしょう dotnet 実際にIPv6を使用していましたか?!

SEC("cgroup/connect6")
int connect6(struct bpf_sock_addr *ctx) {
  struct event info = {
      .eventType = IPV4_VIA_IPV6_REDIRECT_TYPE,
  };
  bpf_ringbuf_output(&events, &info, sizeof(info), 0);
  return 1;
}

reproステップを実行します dotnet add package x 上記のフックで、私はすぐに迎えられました connect6 WiresharkがVMを出るIPv4トラフィックを示したにもかかわらず、ヒットしています。

この時点で、私が考えることができる唯一の結論は、カーネルがIPv6を見ているが、トラフィックは実際にはIPv4であるということでした。

これにより、デュアルスタックネットワークに関するメモリがトリガーされました。掘る dotnet 私はこれを見つけました:

.NET 5以来、socketshttphandlerでデュアルモードソケットを使用しています。これにより、IPv6ソケットからのIPv4トラフィックを処理することができ、RFC 1933による好ましい実践と見なされます。
リンク
RFC1933

機能にはキルスイッチがありました!私はそれを試してみました DualMode sockets すべてを無効にしたすべてが期待どおりに機能しました、 connect4 ヒットしました egress リクエストはIPv6であるとは思わなかった。 🚀

なぜ質問はなぜでしたか?この機能はカバーの下で何をしていましたか。

IPv4-Compatible IPv6 Address または「IPv4が少しの間IPv6のふりをするとき」

この👇のライン dotnet DualMode ソケットドキュメントは、キーのように感じられました

これにより、IPv6ソケットからのIPv4トラフィックを処理できます

しかし、どのように!私が見つけたソースとカーネルをもっと掘り下げる:

これは、IPv4アドレスをエンコードする方法です IPv6アドレス内

使用すると、最後の32ビットが実際にIPv4アドレスであるIPv6アドレスを取得します

更新しました connect6 IPv6アドレスを私に出力するEBPF bpf_ringbuf_output イベントなので、私はそれを見ることができました。

低く、それがあったのを見よ IPv4 mapped address 🤯🎉

使用するとき DualMode sockets dotnet IPv6ソケットをリクエストします。 IPV6以外のリクエストでも、およびを設定します user_ip6 aのアドレスフィールド IPv4マップ 住所。

IPv4マッピングアドレスはどのように見えますか?これらは次のように見えます ::ffff:1.1.1.1 IPv6アドレスの最後にあるIPv4アドレスをエンコードします。

私はこれを間違っているに違いないと思った、きっとあなたはIPv6フィールドでIPv4アドレスを粉砕することはできません、そして魔法は起こりますか?!いいえ、それが間違っていませんでした、それが起こることです。 Linuxはこれをサポートし、IPv4としてリクエストをルーティングします。

私のWiresharkトレースは、ネットワークコールを行うときにカーネルがIPv4に戻すため、IPv6トラフィックが表示されなかったため、この暫定状態はにのみ表示されます。 eBPF プログラム/カーネル。

eBPFを固定して処理します IPv4-mapped IPv6 addresses

私のオリジナルを作るために syscall.connect INTERCEPT WORK IPv4バージョンとIPv6バージョンの両方をフックする必要があります。この場合、私は更新しました connect6 以前から、IPv6アドレスからIPv4アドレスを解析します。

SEC("cgroup/connect6")
int connect6(struct bpf_sock_addr *ctx) {
  // Check if we have an IPv4-mapped IPv6 address (::ffff:x.x.x.x)
  // The first 10 bytes should be zeros, followed by 2 bytes of 0xffff
  // user_ip6[0] and user_ip6[1] should be 0 (first 64 bits)
  // user_ip6[2] should be 0x0000ffff (next 32 bits with pattern 0000...1111)
  if (ctx->user_ip6[0] != 0 || ctx->user_ip6[1] != 0 ||
      ctx->user_ip6[2] != bpf_htonl(0x0000ffff)) {
    return 1;
  }

  // See: https://en.m.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses As
  // bpf_sock_addr stores `user_ip6` as IPv6 is 4x32 bits to get the IPv4
  // address we ignore the first 96 bits and take the last 32 bits which is the
  // __u32 at index 3 of the user_ip6 array
  __be32 ipv4_address = ctx->user_ip6[3];

  struct event info = {
      .ip = bpf_ntohl(ipv4_address),
      .eventType = IPV4_VIA_IPV6_REDIRECT_TYPE,
  };
  bpf_ringbuf_output(&events, &info, sizeof(info), 0);

  struct redirect_result r = {
      .ip = ipv4_address,
      .port = ctx->user_port,
      .is_redirected = false,
  };
  handle_connect_redirect(ctx, ipv4_address, false, &r);
  if (r.is_redirected) {
    // If we redirected the request then we need to update the socket
    // destination to the new IP and port
    ctx->user_ip6[3] = r.ip;
    ctx->user_port = r.port;
  }
  return 1;
}

元のIPv4アドレスを引き出してから、既存のものを呼び出します handle_connect_redirect 方法。やった?いいえ。

これは私のようには十分ではありませんでした egress EBPFプログラムは、これらの要求をIPv6としてブロックします。

これをこのチェックインまで追跡しました egress プログラム:

if (skb->family == AF_INET6) {
    // end up here
    return 0
}

マッピングされたIPv4ソケットは、その家族をIPv6として識別していましたが、これを回避するにはどうすればよいですか? IPv6ソケットを介してマッピングされたIPv4をブロックしたいのです。

たくさんプレイした後、私はこのアプローチを見つけました:

if (skb->protocol == bpf_htons(ETH_P_IPV6)) {
    // IPv6 hits this but IPv4 Mapped doesn't
    return 0
}

プロトコルを見ることにより skb IPv6とIPv4にマッピングされたIPv4を区別することができました(TODO:この最後のビットでより多くのテストが必要です)。

それでおしまい

IPv4はいつIPv4ではありませんか?

それが使用しているとき IPv4-Compatible IPv6 Address IPv6ソケットを介してIPv4を送信します

#EBPFミステリーIPv4はいつIPv4ではありませんか #IPv6のふりをしているとき

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。