The simulation describes the path from the kernel source. Here is the same path recorded on a running machine: every kernel function that touched the packets of one curl request, with the matching step of the simulation next to it.
Симуляція описує шлях за кодом ядра. Тут той самий шлях, записаний на живій машині: кожна функція ядра, через яку пройшли пакети одного запиту curl, і відповідний крок симуляції поруч.
nic0 (Intel, driver e1000e) is a port of the Linux bridge vmbr0, and the host’s IP address lives on the bridge. This is the usual setup on a virtualization host, so every packet also crosses the bridge, once in each direction.Мережева карта nic0 (Intel, драйвер e1000e) — порт Linux-бриджа vmbr0, а IP-адреса хоста висить на бриджі. Це звичайне налаштування для хоста віртуалізації, тож кожен пакет ще й проходить бридж, по разу в кожен бік.sk_buff. It prints each call for packets that match a filter.Інструмент — pwru v1.0.12 («packet, where are you?»): eBPF-програма на кожній функції ядра, що приймає sk_buff. Вона друкує кожен виклик для пакетів, які підходять під фільтр.10.0.0.2, the router 10.0.0.1, MAC addresses are 02:00:00:….Домашні адреси замінено: хост — 10.0.0.2, роутер — 10.0.0.1, MAC-адреси — 02:00:00:….# terminal 1 pwru --output-caller --timestamp relative "host 1.1.1.1 and tcp port 80" # terminal 2 curl http://1.1.1.1/
How to read the excerpts: the first column is the device (name:ifindex, 0 when skb->dev is not set), then skb->len, then the function and who called it. Small helpers (checksum, feature checks, freeing memory) are cut out; nothing is reordered.Як читати витяги: перша колонка — пристрій (ім’я:ifindex, 0, коли skb->dev не задано), далі skb->len, далі функція і хто її викликав. Дрібні допоміжні функції (контрольні суми, перевірки можливостей, звільнення пам’яті) прибрано; порядок не змінено.
One curl request is a handful of packets between this host and 1.1.1.1. Sections 1–6 follow them in order, each looked at from inside the kernel. Click a row to jump to it.Один запит curl — це кілька пакетів між цим хостом і 1.1.1.1. Розділи 1–6 йдуть за ними по порядку, кожен — зсередини ядра. Натисни рядок, щоб перейти до розділу.
connect(): who creates the first packetconnect(): хто створює перший пакет
2SYNthe same SYN on its way down: IP, bridge, network cardтой самий SYN по дорозі вниз: IP, бридж, мережева карта
3SYN-ACKthe reply: from the interrupt up to the socketвідповідь: від переривання вгору до сокета
4ACKthe handshake is done; the kernel answers on its ownрукостискання завершено; ядро відповідає саме
5GET / · datathe HTTP request goes out, the response comes inHTTP-запит виходить, відповідь приходить
6closecurl exits and the socket is destroyedcurl завершується, сокет знищується
Section 7 is not part of this request: it shows ordinary tools (ss, tc, tcpdump…) run on the same host during a longer download. Section 8 shows how to repeat all of this on your own machine.Розділ 7 — не частина цього запиту: там звичайні інструменти (ss, tc, tcpdump…), запущені на тому ж хості під час довшого завантаження. Розділ 8 — як повторити все це на своїй машині.
ip_local_out ip_queue_xmit __tcp_transmit_skb tcp_connect tcp_v4_connect __inet_stream_connect inet_stream_connect __sys_connect __x64_sys_connect do_syscall_64 entry_SYSCALL_64_after_hwframe
Read from the bottom: curl calls connect(), TCP builds the SYN in tcp_connect() and sends it through the same __tcp_transmit_skb() → ip_queue_xmit() → ip_local_out() as any data segment. Only the entry point differs from the Send scenario, which starts at write().Читати знизу: curl викликає connect(), TCP будує SYN у tcp_connect() і відправляє його тим самим шляхом __tcp_transmit_skb() → ip_queue_xmit() → ip_local_out(), що й будь-який сегмент з даними. Від сценарію «Відправка», який починається з write(), відрізняється лише точка входу.
In the simulation:У симуляції: Send 5–7Відправка 5–7
0 60 ip_local_out ← __ip_queue_xmit
0 60 __ip_local_out ← ip_local_out
0 60 nf_hook_slow ← __ip_local_out
0 60 apparmor_ip_localout ← nf_hook_slow
0 60 ip_output ← ip_local_out
vmbr0:4 60 nf_hook_slow ← ip_output
vmbr0:4 60 apparmor_ip_postroute ← nf_hook_slow
vmbr0:4 60 ip_finish_output ← ip_output
vmbr0:4 60 __ip_finish_output ← ip_finish_output
vmbr0:4 60 ip_finish_output2 ← __ip_finish_output
vmbr0:4 74 __dev_queue_xmit ← ip_finish_output2
vmbr0:4 74 validate_xmit_skb ← __dev_queue_xmit
vmbr0:4 74 dev_hard_start_xmit ← __dev_queue_xmit
vmbr0:4 74 dev_queue_xmit_nit ← dev_hard_start_xmit
vmbr0:4 74 skb_clone ← dev_queue_xmit_nit
vmbr0:4 74 packet_rcv ← dev_queue_xmit_nit
vmbr0:4 74 sk_skb_reason_drop(SKB_CONSUMED) ← packet_rcv
vmbr0:4 74 br_dev_xmit ← dev_hard_start_xmit
vmbr0:4 74 br_nf_dev_xmit ← br_dev_xmit
vmbr0:4 60 br_allowed_ingress ← br_dev_xmit
vmbr0:4 60 br_forward ← br_dev_xmit
vmbr0:4 60 __br_forward ← br_forward
nic0:2 60 nf_hook_slow ← __br_forward
nic0:2 60 br_forward_finish ← __br_forward
nic0:2 60 nf_hook_slow ← br_forward_finish
nic0:2 60 br_nf_post_routing ← nf_hook_slow
nic0:2 60 br_dev_queue_push_xmit ← br_forward_finish
nic0:2 74 __dev_queue_xmit ← br_dev_queue_push_xmit
nic0:2 74 sch_direct_xmit ← __dev_queue_xmit
nic0:2 74 dev_hard_start_xmit ← sch_direct_xmit
nic0:2 74 skb_clone_tx_timestamp ← e1000_xmit_frame[e1000e]
nic0:2 74 dev_kfree_skb_any_reason ← e1000_put_txbuf[e1000e]
nic0:2 74 sk_skb_reason_drop(SKB_CONSUMED) ← dev_kfree_skb_any_reason
nf_hook_slow() is how a hook with registered callbacks runs: first NF_INET_LOCAL_OUT in __ip_local_out(), then NF_INET_POST_ROUTING in ip_output(). The callbacks seen here are AppArmor’s: it is enabled on this host.Netfilter. nf_hook_slow() — так виконується хук, на якому є зареєстровані обробники: спершу NF_INET_LOCAL_OUT у __ip_local_out(), потім NF_INET_POST_ROUTING в ip_output(). Тут видно обробники AppArmor: він увімкнений на цьому хості. Send 7–8Відправка 7–8ip_finish_output2() the neighbour entry of the router prepends the 14-byte Ethernet header: skb->len grows by exactly that much.60 → 74. В ip_finish_output2() запис сусіда (роутера) додає спереду 14-байтний Ethernet-заголовок: skb->len росте рівно на стільки. Send 10Відправка 10vmbr0 has no qdisc (noqueue), so __dev_queue_xmit() calls dev_hard_start_xmit() right away. dev_queue_xmit_nit() hands a clone to an AF_PACKET socket open on the host; its filter did not want this frame, so the clone is freed with SKB_CONSUMED, which is not a drop.Перший пристрій — бридж. На vmbr0 немає qdisc (noqueue), тож __dev_queue_xmit() одразу викликає dev_hard_start_xmit(). dev_queue_xmit_nit() віддає клон сокету AF_PACKET, відкритому на хості; його фільтру цей кадр не потрібен, тож клон звільняється з SKB_CONSUMED, а це не втрата. Send 11Відправка 11br_dev_xmit() is the bridge’s transmit function. It pulls the Ethernet header off (74 → 60), looks up the destination MAC in the forwarding database and sends the frame to the port nic0. The br_nf_* functions are br_netfilter, which passes bridged frames to the IP-level netfilter hooks. It is active on this host, but net.bridge.bridge-nf-call-iptables is 0, so they let the frame through without running IP rules. br_dev_queue_push_xmit() pushes the header back (60 → 74) and calls dev_queue_xmit() a second time, now for nic0.Через бридж. br_dev_xmit() — функція відправки бриджа. Вона знімає Ethernet-заголовок (74 → 60), шукає MAC отримувача в таблиці пересилання і відправляє кадр на порт nic0. Функції br_nf_* — це br_netfilter, який передає кадри з бриджа в IP-хуки netfilter. На цьому хості він активний, але net.bridge.bridge-nf-call-iptables дорівнює 0, тож вони пропускають кадр, не виконуючи IP-правил. br_dev_queue_push_xmit() повертає заголовок (60 → 74) і вдруге викликає dev_queue_xmit(), тепер для nic0. Bridge 7–8Бридж 7–8nic0 has fq_codel. Its queue was empty and fq_codel allows bypass (TCQ_F_CAN_BYPASS), so there is no enqueue: sch_direct_xmit() hands the skb straight to the driver, e1000_xmit_frame().Обхід qdisc. На nic0 стоїть fq_codel. Черга була порожня, а fq_codel дозволяє обхід (TCQ_F_CAN_BYPASS), тож постановки в чергу немає: sch_direct_xmit() віддає skb одразу драйверу, e1000_xmit_frame(). Send 12Відправка 12e1000_put_txbuf(), again with SKB_CONSUMED.Кінець відправленого пакета. Коли карта відправила кадр, драйвер звільняє skb в e1000_put_txbuf(), знову з SKB_CONSUMED. Send 13Відправка 13tcp_v4_rcv ip_local_deliver_finish ip_local_deliver ip_rcv __netif_receive_skb_one_core __netif_receive_skb netif_receive_skb br_pass_frame_up br_handle_frame_finish br_handle_frame __netif_receive_skb_core __netif_receive_skb_list_core netif_receive_skb_list_internal napi_complete_done e1000e_poll __napi_poll net_rx_action handle_softirqs __irq_exit_rcu irq_exit_rcu common_interrupt
Again from the bottom. The card raised an interrupt; on the way out of it the kernel runs the network softirq, net_rx_action(), which calls the driver’s NAPI poll, e1000e_poll(). When the poll is done, GRO hands over its batch, and __netif_receive_skb_core() processes the frame on nic0. nic0 is a bridge port, so its rx_handler is br_handle_frame(). The frame is addressed to the bridge itself, so br_pass_frame_up() passes it up: netif_receive_skb() runs a second time, now on vmbr0, and from there it is the usual ip_rcv() → ip_local_deliver() → tcp_v4_rcv(). All of it in one softirq, with no process involved.Знову знизу. Карта підняла переривання; на виході з нього ядро запускає мережевий softirq, net_rx_action(), а той — NAPI-опитування драйвера, e1000e_poll(). Коли опитування закінчено, GRO віддає свою пачку, і __netif_receive_skb_core() обробляє кадр на nic0. nic0 — порт бриджа, тож його rx_handler — br_handle_frame(). Кадр адресовано самому бриджу, тому br_pass_frame_up() передає його вгору: netif_receive_skb() виконується вдруге, тепер на vmbr0, а далі звичний ip_rcv() → ip_local_deliver() → tcp_v4_rcv(). Усе в одному softirq, без жодного процесу.
In the simulation:У симуляції: Receive 3–8Прийом 3–8 · Bridge 5–6 (the rx_handler branch)Бридж 5–6 (гілка rx_handler)
nic0:2 60 br_nf_pre_routing ← br_handle_frame
nic0:2 60 br_handle_frame_finish ← br_handle_frame
nic0:2 60 br_pass_frame_up ← br_handle_frame_finish
vmbr0:4 60 nf_hook_slow ← br_pass_frame_up
vmbr0:4 60 br_nf_local_in ← nf_hook_slow
vmbr0:4 60 netif_receive_skb ← br_pass_frame_up
vmbr0:4 60 __netif_receive_skb ← netif_receive_skb
vmbr0:4 60 deliver_skb ← __netif_receive_skb_core
vmbr0:4 60 packet_rcv ← deliver_skb
vmbr0:4 60 ip_rcv ← __netif_receive_skb_one_core
vmbr0:4 60 ip_rcv_core ← ip_rcv
vmbr0:4 60 nf_hook_slow ← ip_rcv
vmbr0:4 60 ip_sabotage_in ← nf_hook_slow
vmbr0:4 60 ip_rcv_finish_core ← ip_rcv
vmbr0:4 60 tcp_v4_early_demux ← ip_rcv_finish_core
vmbr0:4 60 ip_route_input_noref ← ip_rcv_finish_core
vmbr0:4 60 ip_route_input_slow ← ip_route_input_noref
vmbr0:4 60 ip_local_deliver ← ip_rcv
vmbr0:4 60 nf_hook_slow ← ip_local_deliver
vmbr0:4 60 ip_local_deliver_finish ← ip_local_deliver
vmbr0:4 40 ip_protocol_deliver_rcu ← ip_local_deliver_finish
vmbr0:4 40 raw_local_deliver ← ip_protocol_deliver_rcu
vmbr0:4 40 tcp_v4_rcv ← ip_protocol_deliver_rcu
0 40 tcp_v4_do_rcv ← tcp_v4_rcv
0 40 tcp_rcv_state_process ← tcp_v4_do_rcv
0 40 tcp_finish_connect ← tcp_rcv_state_process
0 40 tcp_init_transfer ← tcp_finish_connect
nic0 to vmbr0 in br_pass_frame_up(). packet_rcv() appears again because vmbr0 has an AF_PACKET listener, as on the way out.Один кадр, два пристрої. Колонка пристрою змінюється з nic0 на vmbr0 у br_pass_frame_up(). packet_rcv() знову видно, бо на vmbr0 є слухач AF_PACKET, як і на виході.ip_sabotage_in() is br_netfilter’s first NF_INET_PRE_ROUTING callback. When the bridge has already run the IPv4 PRE_ROUTING hooks for a frame (with bridge-nf-call-iptables on), it stops them from running a second time. Here that setting is off, so it just accepts the packet.ip_sabotage_in() — перший обробник NF_INET_PRE_ROUTING від br_netfilter. Коли бридж уже виконав для кадру хуки IPv4 PRE_ROUTING (з увімкненим bridge-nf-call-iptables), він не дає їм виконатися вдруге. Тут це налаштування вимкнене, тож він просто приймає пакет.tcp_v4_early_demux() finds the socket already in ip_rcv_finish_core(), but the socket has no cached input route yet (sk_rx_dst), so the kernel does a full lookup in ip_route_input_slow(). tcp_finish_connect() then stores the route in the socket. Later packets of this connection skip the lookup entirely, see section 5.Пошук маршруту лише для першого пакета. tcp_v4_early_demux() знаходить сокет ще в ip_rcv_finish_core(), але в сокета ще немає збереженого вхідного маршруту (sk_rx_dst), тож ядро робить повний пошук в ip_route_input_slow(). Потім tcp_finish_connect() зберігає маршрут у сокеті. Наступні пакети цього з’єднання пошук пропускають повністю, див. розділ 5. Receive 6Прийом 6ip_local_deliver_finish() pulls the 20-byte IP header off before handing the packet to TCP. From tcp_v4_do_rcv() on the device column shows 0: tcp_v4_rcv() sets skb->dev = NULL before the socket processes the packet.60 → 40. ip_local_deliver_finish() знімає 20-байтний IP-заголовок перед тим, як віддати пакет TCP. Починаючи з tcp_v4_do_rcv() колонка пристрою показує 0: tcp_v4_rcv() ставить skb->dev = NULL до того, як сокет обробить пакет. Receive 7–8Прийом 7–8tcp_rcv_state_process() rather than the established fast path, and tcp_finish_connect() completes the handshake.SYN_SENT → ESTABLISHED. Сокет ще з’єднується, тож пакет іде в tcp_rcv_state_process(), а не у швидкий шлях встановленого з’єднання, і tcp_finish_connect() завершує рукостискання. Receive 9Прийом 9ip_local_out ip_queue_xmit __tcp_transmit_skb __tcp_send_ack tcp_rcv_state_process tcp_v4_do_rcv tcp_v4_rcv … the same receive stack as in section 3, down to common_interruptтой самий стек прийому, що в розділі 3, до common_interrupt
The third packet of the handshake has no system call behind it. It is sent while the kernel is still processing the SYN-ACK, inside the same softirq: tcp_rcv_state_process() calls __tcp_send_ack(), and the ACK takes the usual send path from __tcp_transmit_skb() down. Most ACKs during a download are sent like this, from the receive path.За третім пакетом рукостискання немає системного виклику. Він відправляється, поки ядро ще обробляє SYN-ACK, у тому самому softirq: tcp_rcv_state_process() викликає __tcp_send_ack(), і ACK іде звичним шляхом відправки від __tcp_transmit_skb() вниз. Більшість ACK під час завантаження відправляються саме так, зі шляху прийому.
ip_local_out ip_queue_xmit __tcp_transmit_skb tcp_write_xmit __tcp_push_pending_frames tcp_push tcp_sendmsg_locked tcp_sendmsg inet_sendmsg __sys_sendto __x64_sys_sendto do_syscall_64 entry_SYSCALL_64_after_hwframe
The HTTP request. curl uses send() (on x86-64 it is the sendto system call) rather than write(), but from inet_sendmsg() on it is the Send scenario as written: the data is copied into the socket write queue in tcp_sendmsg_locked(), and tcp_push() → tcp_write_xmit() sends it.HTTP-запит. curl використовує send() (на x86-64 це системний виклик sendto), а не write(), але від inet_sendmsg() далі це сценарій «Відправка» як він є: дані копіюються в чергу запису сокета в tcp_sendmsg_locked(), а tcp_push() → tcp_write_xmit() їх відправляє.
In the simulation:У симуляції: Send 2–5Відправка 2–5
vmbr0:4 438 ip_rcv_finish_core ← ip_rcv
vmbr0:4 438 tcp_v4_early_demux ← ip_rcv_finish_core
vmbr0:4 438 ip_local_deliver ← ip_rcv
vmbr0:4 438 ip_local_deliver_finish ← ip_local_deliver
vmbr0:4 418 ip_protocol_deliver_rcu ← ip_local_deliver_finish
vmbr0:4 418 tcp_v4_rcv ← ip_protocol_deliver_rcu
0 418 tcp_v4_do_rcv ← tcp_v4_rcv
0 418 tcp_rcv_established ← tcp_v4_do_rcv
0 418 tcp_data_queue ← tcp_rcv_established
0 386 tcp_queue_rcv ← tcp_data_queue
0 386 tcp_event_data_recv ← tcp_data_queue
A packet of the response (the part before ip_rcv_finish_core() is the same as in section 3). Compare with the SYN-ACK: after tcp_v4_early_demux() comes ip_local_deliver() directly. There is no ip_route_input_*, because the route cached in the socket was used. The connection is established, so TCP goes through tcp_rcv_established(). tcp_data_queue() pulls the 32-byte TCP header (20 bytes plus the timestamp option; 418 → 386) and tcp_queue_rcv() puts the payload on the socket receive queue, where it waits for curl to read it.Пакет відповіді (до ip_rcv_finish_core() усе як у розділі 3). Порівняйте з SYN-ACK: після tcp_v4_early_demux() одразу йде ip_local_deliver(). ip_route_input_* немає, бо використано маршрут, збережений у сокеті. З’єднання встановлене, тож TCP іде через tcp_rcv_established(). tcp_data_queue() знімає 32-байтний TCP-заголовок (20 байтів плюс опція timestamp; 418 → 386), а tcp_queue_rcv() кладе дані в чергу прийому сокета, де вони чекають, поки curl їх прочитає.
In the simulation:У симуляції: Receive 9–10Прийом 9–10
0 0 sk_skb_reason_drop(SKB_DROP_REASON_QUEUE_PURGE) ← sk_stream_kill_queues
0 0 sock_rfree ← skb_release_head_state
The last line of the trace. When the socket is destroyed, sk_stream_kill_queues() empties its receive queue, and every skb still there is freed with the drop reason SKB_DROP_REASON_QUEUE_PURGE. This one carried no data (len 0). sock_rfree() is the destructor of received skbs: it returns the memory to the socket’s accounting. A drop reason in a trace is therefore not always a lost packet; here it is an ordinary close.Останній рядок трасування. Коли сокет знищується, sk_stream_kill_queues() спорожнює його чергу прийому, і кожен skb, що там лишився, звільняється з причиною SKB_DROP_REASON_QUEUE_PURGE. Цей не ніс даних (len 0). sock_rfree() — деструктор прийнятих skb: він повертає пам’ять в облік сокета. Тож причина «drop» у трасуванні не завжди означає втрачений пакет; тут це звичайне закриття.
These are the commands the simulation suggests at each step, run on the same host while it downloaded a file over HTTPS (the server is 172.66.0.218). Output is trimmed to the relevant lines.Це команди, які симуляція пропонує на кожному кроці, запущені на тому самому хості під час завантаження файлу по HTTPS (сервер — 172.66.0.218). Вивід скорочено до потрібних рядків.
$ ip route get 172.66.0.218 172.66.0.218 via 10.0.0.1 dev vmbr0 src 10.0.0.2 uid 0 $ ip neigh show 10.0.0.1 10.0.0.1 dev vmbr0 lladdr 02:00:00:00:00:01 DELAY
The route goes through the router on vmbr0, not nic0: the bridge holds the IP address. The router’s neighbour entry is in DELAY: it had gone STALE, a packet was just sent through it, and the kernel now waits up to delay_first_probe_time (5 seconds here) for a confirmation that the router is reachable before it sends ARP probes.Маршрут іде через роутер на vmbr0, а не nic0: IP-адреса на бриджі. Запис сусіда-роутера в стані DELAY: він був STALE, через нього щойно відправили пакет, і ядро тепер до delay_first_probe_time (тут 5 секунд) чекає підтвердження, що роутер досяжний, перш ніж слати ARP-запити.
State Recv-Q Send-Q Local Address:Port Peer Address:Port ESTAB 67708 0 10.0.0.2:49120 172.66.0.218:https skmem:(r94848,rb131072,t0,tb46080,f3456,w0,o0,bl0,d2) cubic wscale:13,10 rtt:14.523/9.121 mss:1388 pmtu:1500 rcvmss:1440 advmss:1448 cwnd:10 … rcv_wnd:23552
Recv-Q is data in the socket receive queue that curl has not read yet: 67 KB delivered by tcp_queue_rcv() and waiting for read(). skmem shows the memory behind it: r is the receive memory in use, rb the limit (sk_rcvbuf), d the packets the socket dropped.Recv-Q — дані в черзі прийому сокета, які curl ще не прочитав: 67 КБ, які поклав tcp_queue_rcv() і які чекають на read(). skmem показує пам’ять за ними: r — зайнята пам’ять прийому, rb — ліміт (sk_rcvbuf), d — пакети, які сокет відкинув.
qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024 quantum 1514 target 5ms interval 100ms memory_limit 32Mb ecn drop_batch 64 Sent 261828748172 bytes 303231108 pkt (dropped 0, overlimits 0 requeues 13109) backlog 0b 0p requeues 13109
The qdisc that the SYN bypassed. backlog 0b 0p: nothing waits in it, which is why bypass is the common case on a lightly loaded uplink. requeues counts the times the driver could not take a packet and it went back to the queue.Та сама qdisc, яку SYN обійшов. backlog 0b 0p: у ній нічого не чекає, тому на мало завантаженому каналі обхід — звичайна справа. requeues — скільки разів драйвер не зміг узяти пакет і той повертався в чергу.
02:00:00:00:00:01 > 02:00:00:00:00:02, ethertype IPv4 (0x0800), length 1506: 172.66.0.218.443 > 10.0.0.2.49120: Flags [.], seq 502524510:502525950, ack 3776738090, win 16, options [nop,nop,TS val 3954928449 ecr 2048667830], length 1440 02:00:00:00:00:02 > 02:00:00:00:00:01, ethertype IPv4 (0x0800), length 66: 10.0.0.2.49120 > 172.66.0.218.443: Flags [.], ack 1440, win 2, options [nop,nop,TS val 2048667871 ecr 3954928420], length 0
One data frame in and one ACK out, captured on nic0. 1506 = 14 (Ethernet) + 20 (IP) + 32 (TCP with timestamps) + 1440 bytes of data; the ACK is 66 = 14 + 20 + 32. The capture itself is the AF_PACKET tap from sections 2 and 3: tcpdump gets its copies through dev_queue_xmit_nit() and deliver_skb().Один кадр з даними вхідний і один ACK вихідний, знято на nic0. 1506 = 14 (Ethernet) + 20 (IP) + 32 (TCP з timestamps) + 1440 байтів даних; ACK — 66 = 14 + 20 + 32. Сам запис — це той самий відвід AF_PACKET з розділів 2 і 3: tcpdump отримує копії через dev_queue_xmit_nit() і deliver_skb().
$ grep nic0 /proc/interrupts 124: 354817914 0 0 0 0 0 IR-PCI-MSI 0-edge nic0 $ head -2 /proc/net/softnet_stat 30533ca3 00000000 00000007 00000000 … 0daa289e 00000000 00000001 00000000 …
The card has one interrupt line, and all of its 354 million interrupts landed on CPU 0. Each starts the NAPI poll seen in section 3. In softnet_stat each line is a CPU and the values are hex: packets processed, packets dropped because the backlog queue was full (0 here), and “time squeeze”: the times net_rx_action() stopped because it ran out of its packet budget or time (7 on CPU 0).У карти одна лінія переривань, і всі її 354 мільйони переривань прийшли на CPU 0. Кожне запускає NAPI-опитування з розділу 3. У softnet_stat кожен рядок — це CPU, значення шістнадцяткові: оброблені пакети, пакети, відкинуті через переповнену чергу backlog (тут 0), і «time squeeze»: скільки разів net_rx_action() зупинявся, бо вичерпав бюджет пакетів або час (7 на CPU 0).
pwru answers one question: which functions did this packet pass through. It does not show everything:pwru відповідає на одне питання: через які функції пройшов цей пакет. Але не все:
ip_rcv_finish(), dst_input(), neigh_output()) run but never appear as separate calls.Дрібні функції, які компілятор вбудував у тих, хто їх викликає (ip_rcv_finish(), dst_input(), neigh_output()), виконуються, але окремими викликами не видно.sk_buff, such as tcp_sendmsg(), are visible only in a call stack (--output-stack).Функції без аргументу sk_buff, як-от tcp_sendmsg(), видно лише в стеку викликів (--output-stack).Needs root and a kernel 5.3 or newer built with BTF: check that /sys/kernel/btf/vmlinux exists. pwru loads eBPF programs on hundreds of kernel functions, so use a test machine and a narrow filter.Потрібні root і ядро 5.3 або новіше, зібране з BTF: перевір, що існує /sys/kernel/btf/vmlinux. pwru ставить eBPF-програми на сотні функцій ядра, тож краще тестова машина і вузький фільтр.
ls /sys/kernel/btf/vmlinux # x86-64; other builds are on https://github.com/cilium/pwru/releases curl -LO https://github.com/cilium/pwru/releases/download/v1.0.12/pwru-linux-amd64.tar.gz curl -LO https://github.com/cilium/pwru/releases/download/v1.0.12/pwru-linux-amd64.tar.gz.sha256sum sha256sum -c pwru-linux-amd64.tar.gz.sha256sum tar xzf pwru-linux-amd64.tar.gz # terminal 1: sections 2, 3, 5, 6 sudo ./pwru --output-caller "host 1.1.1.1 and tcp port 80" # terminal 1, another run: the call stacks of sections 1, 3, 4, 5 sudo ./pwru --output-stack --filter-func '^(ip_local_out|tcp_v4_rcv)$' "host 1.1.1.1 and tcp port 80" # terminal 2 curl http://1.1.1.1/
$ strace -f -e trace=connect,sendto,recvfrom curl -s -o /dev/null http://1.1.1.1/
connect(4, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("1.1.1.1")}, 16) = -1 EINPROGRESS
sendto(4, "GET / HTTP/1.1\r\nHost: 1.1.1.1\r\nU"..., 71, MSG_NOSIGNAL, NULL, 0) = 71
recvfrom(4, 0x5d12589ebae0, 102400, 0, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
recvfrom(4, "HTTP/1.1 301 Moved Permanently\r\n"..., 102400, 0, NULL, NULL) = 386
Only three kinds of calls, and they line up with the trace. connect() returns at once with EINPROGRESS: curl uses a non-blocking socket, so the SYN leaves (section 2) and the SYN-ACK and ACK (sections 3–4) are handled by the kernel while curl does something else. sendto() hands over the 71-byte request (section 5). The first recvfrom() finds the receive queue empty (EAGAIN); the second takes 386 bytes, the size of the response payload that tcp_queue_rcv() put in the queue in section 5. The server returns the same short redirect every time.Лише три види викликів, і вони збігаються з трасуванням. connect() одразу повертає EINPROGRESS: curl використовує неблокувальний сокет, тож SYN іде (розділ 2), а SYN-ACK і ACK (розділи 3–4) ядро обробляє, поки curl робить щось інше. sendto() віддає 71-байтний запит (розділ 5). Перший recvfrom() бачить порожню чергу прийому (EAGAIN); другий забирає 386 байтів — розмір даних відповіді, які tcp_queue_rcv() поклав у чергу в розділі 5. Сервер щоразу повертає той самий короткий редирект.
Replace eth0 with your interface. Every step of the simulation lists more commands like these under “Try it on a live system”.Підстав свій інтерфейс замість eth0. На кожному кроці симуляції є більше таких команд у блоці «Подивитися на живій системі».
| LayerРівень | CommandКоманда | What it showsЩо показує | StepКрок |
|---|---|---|---|
| AppЗастосунок | strace -f -e trace=connect,sendto,recvfrom curl -s -o /dev/null http://1.1.1.1/ | the system calls and how many bytes each one movedсистемні виклики і скільки байтів кожен передав | Send 1Відправка 1 |
| SocketСокет | ss -tmi dst 1.1.1.1 | bytes waiting in the socket queues, socket memory, RTT, cwndбайти в чергах сокета, пам’ять сокета, RTT, cwnd | Receive 10Прийом 10 |
| TCPTCP | nstat -az TcpOutSegs TcpRetransSegs | segments sent and retransmitted, for the whole hostвідправлені й повторно відправлені сегменти, по всьому хосту | Send 4Відправка 4 |
| IPIP | ip route get 1.1.1.1 | the route, gateway, device and source address chosenобраний маршрут, шлюз, пристрій і адреса відправника | Send 6Відправка 6 |
| netfilternetfilter | nft list ruleset | the rules that run in the hooksправила, що виконуються в хуках | Send 7Відправка 7 |
| NeighbourСусіди | ip neigh show | MAC addresses of next hops and the state of each entryMAC-адреси наступних вузлів і стан кожного запису | Send 10Відправка 10 |
| qdiscqdisc | tc -s qdisc show dev eth0 | the queue discipline, its backlog, drops and requeuesдисципліна черги, її backlog, втрати і requeues | Send 11Відправка 11 |
| Driver / NICДрайвер / карта | ethtool -S eth0 | the driver’s own counters, per queueвласні лічильники драйвера, по чергах | Send 13Відправка 13 |
| InterruptsПереривання | grep eth0 /proc/interrupts | which CPUs take the card’s interruptsякі CPU приймають переривання карти | Receive 2Прийом 2 |
| NAPINAPI | cat /proc/net/softnet_stat | per CPU: packets processed, backlog drops, time squeezeпо кожному CPU: оброблені пакети, втрати backlog, time squeeze | Receive 3Прийом 3 |
| WireДріт | tcpdump -ni eth0 -e host 1.1.1.1 | the frames themselves, as the device sees themсамі кадри, як їх бачить пристрій | Send 14Відправка 14 |
| DropsВтрати | perf trace -e skb:kfree_skb | every freed packet with the place and the reasonкожен звільнений пакет із місцем і причиною | Receive 5Прийом 5 |