tcpdump はどこでパケットをtapしているのか?
tcpdumpは iptables の INPUT chain でドロップしているパケットも捉えることができる。
疑問。カーネルのパケット処理プロセスのうち、 tcpdump はどこでパケットを取得しているのか? iptables はどこでパケットフィルタリングを実行するのか?
resolution
概念的なフローは、Netfilter に関するこのダイアグラムが分かりやすい。
iptables - Wikipedia
Flow of network packets through Netfilter - drawn by Jan Engelhardt
Ingress
左から3番目のブロック alloc_skb から taps (=tcpdump) に向けて clone packet のラインが伸びている。
netdev の qdisc(キュー制御)や iptables の prerouting/input/forward といった要素は、パケットのクローン(キャプチャプロセスへのコピー)よりも後の処理だ。
Egress
右から2番目のブロック egress (qdisc) から AF_PACKET に向けて clone packet のラインが伸びている。
こちらは iptables の postrouting/output といった要素の後にキャプチャしている。Egressキューに詰めるところでクローンするということだろうか?
基本的に「Ingressのキャプチャは一番最初に、Egressのキャプチャは一番最後に」という方針であるのだろう。
digging
もう少しだけ深く調べた内容について。まだ確信できるほど理解できていないし、間違っているかもしれない。
パケットキャプチャの本質は「OSのIPスタックを通さず、生のパケットをユーザランドのプロセスまでコピーしてくる」という操作になる。
それをLinuxで実現するためには、概念的には以下のソケットをキャプチャプロセスで開けばいい。 このソケットからキャプチャポイントに流れ込んできたパケットを読み取ることができる。
int socket_type = SOCK_DGRAM; # Get IP raw packet int socket_type = SOCK_RAW; # ... or Get Ethernet raw packet int protocol = htons(ETH_P_ALL); # Capture all packets int packet_socket = socket(AF_PACKET, int socket_type, int protocol);
つまり protocol = htons(ETH_P_ALL) を設定したソケットバッファへパケットをコピーしている場所がキャプチャポイントになる。
Ingress/Egressのそれぞれに存在するはずだ。
プロトコルハンドラ
まずプロトコルハンドラについて。Linux のパケット処理は、処理すべきプロトコルのハンドラの構造体に定義された関数を呼び出すことで行われる。
packet_type がハンドラの構造体で、 識別子として type に htons(ether_type) が定義され、func に実際にパケットを処理する関数へのポインタがある。
# https://github.com/torvalds/linux/blob/ce36c8b149873b50f2a4b9818eb3dcdd74ddd5a3/include/linux/netdevice.h#L2685-L2702
struct packet_type {
__be16 type; /* This is really htons(ether_type). */
bool ignore_outgoing;
struct net_device *dev; /* NULL is wildcarded here */
netdevice_tracker dev_tracker;
int (*func) (struct sk_buff *,
struct net_device *,
struct packet_type *,
struct net_device *);
void (*list_func) (struct list_head *,
struct packet_type *,
struct net_device *);
bool (*id_match)(struct packet_type *ptype,
struct sock *sk);
struct net *af_packet_net;
void *af_packet_priv;
struct list_head list;
};
例えばIPv4パケットのハンドラなら type = htons(0x0806) が設定されるわけだが、キャプチャなどの全プロトコルに対して利用するハンドラには type = htons(ETH_P_ALL) が設定される。
パケットキャプチャなどを目的としたハンドラは ptype_all リストに登録される。このリストに登録されたハンドラは、任意のプロトコルのパケットが到着した際に対して呼び出される。
一方、特定のプロトコルに対する処理を定義したハンドラは type をキーとして ptype_base ハッシュテーブルに登録され、処理対象のプロトコルに応じたハンドラが呼び出される。
Egress
Egress に関しては以下のブログで解説されていた。
pktgenとパケットキャプチャとLinux kernel 4.4
パケットのコピーは net/core/dev.c 内で行われる。
dev_nit_active() 関数が各ネットワークデバイス(もしくは全ネットワークデバイス共通)の ptype_all リストの内容を確認し、空でなければ(キャプチャプロセスが存在すれば) true を返す。
/**
* dev_nit_active - return true if any network interface taps are in use
*
* @dev: network device to check for the presence of taps
*/
bool dev_nit_active(struct net_device *dev)
{
return !list_empty(&ptype_all) || !list_empty(&dev->ptype_all);
}
EXPORT_SYMBOL_GPL(dev_nit_active);
xmit_one() の中で dev_nit_active() を呼ぶ。キャプチャプロセスが存在すれば、dev_queue_xmit_nit() の中で packet_type.func 呼んでおり、最終的にソケットバッファにパケットがコピーされるはず (Read in progress)
static int xmit_one(struct sk_buff *skb, struct net_device *dev,
struct netdev_queue *txq, bool more)
{
unsigned int len;
int rc;
if (dev_nit_active(dev))
dev_queue_xmit_nit(skb, dev);
len = skb->len;
trace_net_dev_start_xmit(skb, dev);
rc = netdev_start_xmit(skb, dev, txq, more);
trace_net_dev_xmit(skb, rc, dev, len);
return rc;
}
Ingress
カーネルコードを読んでざっっくりと流れを追う。
デバイスにパケットが到着すると Hardware IRQ が発生し、カーネルはデバイスからパケットを取り出してカーネルのリングバッファに放り込む。
その後は SoftIRQ でキューからパケットを取り出して netif_receive_skb() を呼び、以下の関数を辿ってプロトコルハンドラへのディスパッチに辿り着く。
netif_receive_skb() → netif_receive_skb_internal() (RPS有効の場合、ここで他CPUのbacklogに詰めるなどして若干別ルートを辿る) → __netif_receive_skb() → __netif_receive_skb_one_core() → __netif_receive_skb_core()
__netif_receive_skb_core() では、以下のコードで &ptype_all のリストを確認し、もしプロトコルハンドラが登録されていれば deliver_skb() が packet_type.func を呼ぶ。
# https://github.com/torvalds/linux/blob/ce36c8b149873b50f2a4b9818eb3dcdd74ddd5a3/net/core/dev.c#L5365-L5376
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
list_for_each_entry_rcu(ptype, &skb->dev->ptype_all, list) {
if (pt_prev)
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
その後、以下のコードでパケットの type に合致するハンドラの packet_type.func を &ptype_base から呼び出す。マッチにマスクを指定することもできるらしい?
# https://github.com/torvalds/linux/blob/ce36c8b149873b50f2a4b9818eb3dcdd74ddd5a3/net/core/dev.c#L5472-L5480
/* deliver only exact match when indicated */
if (likely(!deliver_exact)) {
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&ptype_base[ntohs(type) &
PTYPE_HASH_MASK]);
}
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&orig_dev->ptype_specific);
iptables などの処理は、その後IPプロトコルハンドラを通してIP層のスタックのエントリポイント ip_rcv() に入った後の話になる。
Read in progress:
ETH_P_ALLのプロトコルハンドラ自体についてETH_P_ALLのソケットを開いた場合にプロトコルハンドラを登録する処理について
Reference:
- The Linux Foundation Wiki: networking > kernel_flow (kernel 2.6)
- https://wiki.linuxfoundation.org/networking/kernel_flow
古いがほぼ公式、とりあえずフローの概説を読んでおく
- https://wiki.linuxfoundation.org/networking/kernel_flow
- Linux JF Project > 11. ネットワーク
- https://linuxjf.osdn.jp/JFdocs/The-Linux-Kernel-11.html
パケット処理のフローと ptype_all などの配列についての概説がある
- https://linuxjf.osdn.jp/JFdocs/The-Linux-Kernel-11.html
- Linuxカーネル解読室
- Linux kernel v2.6.20 network stackを読んでみる - IP受信編
- https://qiita.com/IK_PE/items/4d868e8940885f46e0da
カーネルコードを実際に追っている
- https://qiita.com/IK_PE/items/4d868e8940885f46e0da
- ネットワークスタック内の謎の2バイトについて探る
- https://lv4.hateblo.jp/entry/2016/12/25/235121
直接の関係はないが面白いネタ
- https://lv4.hateblo.jp/entry/2016/12/25/235121
おまけ: ChatGPTの回答
以下は ChatGPT の「Linuxにおける受信パケットの処理フローを、主要な関数について触れながら解説してください」に対する回答
LinuxでNICに到着したパケットがip_rcv()関数に渡るまでの流れは、以下の主要な関数を介して行われます:
NICへの受信割り込み(Network Interrupt): パケットはハードウェア割り込みを発生させ、NICの割り込みハンドラが呼び出されます。これにより、受信パケットの処理が開始されます。
netif_receive_skb()関数: ネットワーク割り込みハンドラ内で、受信したパケットは通常netif_receive_skb()関数に渡されます。この関数は、受信したパケットをLinuxのネットワークスタックに送ります。イーサネットヘッダの解釈:
netif_receive_skb()関数内で、受信したパケットのイーサネットヘッダを解釈し、パケットの宛先MACアドレスを確認します。この情報は後続の処理で使用されます。eth_type_trans()関数: イーサネットヘッダの情報を基に、パケットのプロトコルを解釈するためにeth_type_trans()関数が呼び出されます。この関数は、イーサネットフレームのプロトコルタイプを認識し、適切なプロトコルの処理関数にパケットを渡します。IP プロトコルハンドラ(例:
ip_rcv()):eth_type_trans()関数によって選択されたIPパケットは、IPヘッダを解釈し、適切なトランスポート層プロトコル(TCP、UDP、ICMPなど)にパケットをデマルチプレクシングします。IPv4の場合、パケットは通常ip_rcv()関数に渡されます。トランスポート層 プロトコルハンドラ(例:
udp_rcv(),tcp_v4_rcv()): IPヘッダの解釈後、パケットは適切なトランスポート層プロトコルハンドラに渡されます。UDPパケットはudp_rcv()関数、TCPパケットはtcp_v4_rcv()関数などで処理されます。これらの関数はトランスポート層ヘッダを解釈し、適切なソケットにデータを配信します。ソケットバッファへの格納: パケットがトランスポート層の処理を通過した後、ソケットバッファにデータが格納されます。これにより、アプリケーションはソケットAPIを使用してデータを受信できるようになります。
このように、NICから受信したパケットは、複数の関数を通過してイーサネットヘッダ、IPヘッダ、トランスポート層ヘッダなどが解釈され、最終的にアプリケーションに届けられるまで処理されます。
Linuxカーネルのネットワーキングサブシステムでは、ユーザーがカスタムのパケット処理を実装できる仕組みとしてpacket_type構造体が提供されています。これは、特定のプロトコルを処理するためのコールバック関数を指定するために使用されます。
以下は、packet_type構造体の定義です:
struct packet_type {
void (*func)(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev);
struct packet_type *next;
int type;
struct net_device *dev;
struct sock *af_packet_priv;
};
func: パケットがこのタイプにマッチした場合に呼び出されるコールバック関数へのポインタです。この関数は、struct sk_buff(パケットデータを含むバッファ)、struct net_device(受信したデバイス)、struct packet_type(このパケットタイプを表す構造体)、struct net_device(元のデバイス、通常はNULL)を引数に取ります。next: タイプのリストを形成するための次のpacket_type構造体へのポインタです。type: このパケットタイプの識別子です。通常、プロトコル番号(Ethertypeなど)がここに指定されます。dev: このパケットタイプが関連付けられるネットワークデバイスへのポインタです。通常はNULLに設定され、すべてのデバイスでパケットをキャプチャするようになります。af_packet_priv: このパケットタイプのプライベートデータを指すsock構造体へのポインタです。通常はNULLに設定され、このフィールドは使用されません。
packet_type構造体は、特定のプロトコルのパケットを処理するカスタム処理を提供する際に使用されます。この機能を使用すると、Linuxカーネルのネットワーキングスタックを拡張し、特定のネットワーキングプロトコルに対するカスタム処理を実装することができます。
