Некоторое время назад была проблема с сервисом упаковки Android компании, явление такое, что при загрузке сервера 360 для армирования, очень вероятно, что он застрянет на этапе загрузки, и повторная попытка будет неудачной для много времени. Я провел некоторое расследование и анализ этой ситуации, решил проблему и написал эту длинную статью для обзора опыта расследования, которая будет включать следующее содержание.
- Сетевая модель режима моста Docker
- Netfilter и принцип NAT
- Использование Systemtap в тестах ядра
Симптом
Структура развертывания службы упаковки выглядит следующим образом: Среда упаковки Android упакована в виде образа докера и развернута на физическом компьютере. показано на следующем рисунке:
Проблема заключается в шаге загрузки APK, и он зависает при загрузке части SDK 360 выдает аномалии, такие как тайм-аут, как показано на следующем рисунке.
Захватив пакеты в хосте и контейнере соответственно, мы обнаружили некоторые такие явления.
Перехват пакета хостом выглядит следующим образом: Пакет с порядковым номером 881 является задержанным ACK, и его значение ACK равно 530104. Пакет с порядковым номером 875, который больше, чем этот номер ACK, подтвержден (порядковый номер 532704). , а затем Хост отправляет пакет RST на удаленный сервер защиты 360.
Следующим шагом является продолжение повторных попыток отправки данных, и зависание загрузки соответствует этому этапу постоянных повторных попыток отправки данных, как показано на следующем рисунке.
Захватите пакет на стороне контейнера, и этот RST не появится, другие пакеты такие же, как показано на следующем рисунке.
Поскольку сторона контейнера не воспринимает аномалию соединения, служба в контейнере продолжает повторять попытку загрузки, и после многих попыток она все еще терпит неудачу.
Предварительное расследование и анализ
Первоначальное сомнение заключается в том, что это происходит из-за того, что получен отложенный ACK, поэтому получен ответ RST?
Этого не должно быть. В спецификации протокола TCP, если получен задержанный ACK, его можно игнорировать. Нет необходимости отвечать на ACK. Так почему же отправляется пакет RST?
Так этот пакет незаконен в первую очередь? После тщательного анализа информации этого пакета ничего аномального обнаружено не было. Из существующих знаний о принципах TCP невозможно сделать вывод об этом явлении.
У меня не так много навыков и идей.
Судя по коду ядра, функции отправки первых пакетов в основном следующие:
tcp_v4_send_reset@net/ipv4/tcp_ipv4.c
static void tcp_v4_send_reset(struct sock *sk, struct sk_buff *skb) {
}
tcp_send_active_reset@net/ipv4/tcp_output.c
void tcp_send_active_reset(struct sock *sk, gfp_t priority) {
}
Затем systemtap может внедрить эти две функции.
probe kernel.function("tcp_send_active_reset@net/ipv4/tcp_output.c").call {
printf ("\n%-25s %s<-%s\n", ctime(gettimeofday_s()) ,execname(), ppfunc());
if ($sk) {
src_addr = tcp_src_addr($sk);
src_port = tcp_src_port($sk);
dst_addr = tcp_dst_addr($sk);
dst_port = tcp_dst_port($sk);
if (src_port == 443 || dst_port == 443) {
printf (">>>>>>>>>[%s->%s] %s<-%s %d\n", str_addr(src_addr, src_port), str_addr(dst_addr, dst_port), execname(), ppfunc(), dst_port);
print_backtrace();
}
}
}
probe kernel.function("tcp_v4_send_reset@net/ipv4/tcp_ipv4.c").call {
printf ("\n%-25s %s<-%s\n", ctime(gettimeofday_s()) ,execname(), ppfunc());
if ($sk) {
src_addr = tcp_src_addr($sk);
src_port = tcp_src_port($sk);
dst_addr = tcp_dst_addr($sk);
dst_port = tcp_dst_port($sk);
if (src_port == 443 || dst_port == 443) {
printf (">>>>>>>>>[%s->%s] %s<-%s %d\n", str_addr(src_addr, src_port), str_addr(dst_addr, dst_port), execname(), ppfunc(), dst_port);
print_backtrace();
}
} else if ($skb) {
header = __get_skb_tcphdr($skb);
src_port = __tcp_skb_sport(header)
dst_port = __tcp_skb_dport(header)
if (src_port == 443 || dst_port == 443) {
try {
iphdr = __get_skb_iphdr($skb)
src_addr_str = format_ipaddr(__ip_skb_saddr(iphdr), @const("AF_INET"))
dst_addr_str = format_ipaddr(__ip_skb_daddr(iphdr), @const("AF_INET"))
tcphdr = __get_skb_tcphdr($skb)
urg = __tcp_skb_urg(tcphdr)
ack = __tcp_skb_ack(tcphdr)
psh = __tcp_skb_psh(tcphdr)
rst = __tcp_skb_rst(tcphdr)
syn = __tcp_skb_syn(tcphdr)
fin = __tcp_skb_fin(tcphdr)
printf ("skb [%s:%d->%s:%d] ack:%d, psh:%d, rst:%d, syn:%d fin:%d %s<-%s %d\n",
src_addr_str, src_port, dst_addr_str, dst_port, ack, psh, rst, syn, fin, execname(), ppfunc(), dst_port);
print_backtrace();
}
catch { }
}
} else {
printf ("tcp_v4_send_reset else\n");
print_backtrace();
}
}
Как только он запускается, обнаруживается, что при возникновении проблемы вводится функция tcp_v4_send_reset, и стек вызовов
Tue Jun 15 11:23:04 2021 swapper/6<-tcp_v4_send_reset
skb [36.110.213.207:443->10.21.17.99:39700] ack:1, psh:0, rst:0, syn:0 fin:0 swapper/6<-tcp_v4_send_reset 39700
0xffffffff99e5bc50 : tcp_v4_send_reset+0x0/0x460 [kernel]
0xffffffff99e5d756 : tcp_v4_rcv+0x596/0x9c0 [kernel]
0xffffffff99e3685d : ip_local_deliver_finish+0xbd/0x200 [kernel]
0xffffffff99e36b49 : ip_local_deliver+0x59/0xd0 [kernel]
0xffffffff99e364c0 : ip_rcv_finish+0x90/0x370 [kernel]
0xffffffff99e36e79 : ip_rcv+0x2b9/0x410 [kernel]
0xffffffff99df0b79 : __netif_receive_skb_core+0x729/0xa20 [kernel]
0xffffffff99df0e88 : __netif_receive_skb+0x18/0x60 [kernel]
0xffffffff99df0f10 : netif_receive_skb_internal+0x40/0xc0 [kernel]
...
Видно, что после получения пакета ACK RST отправил при вызове tcp_v4_rcv для его обработки, что это за строка?
Это требует использования мощного инструмента faddr2line для восстановления информации в стеке до количества строк, соответствующего исходному коду.
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/faddr2line
bash faddr2line /usr/lib/debug/lib/modules/`uname -r`/vmlinux tcp_v4_rcv+0x536/0x9c0
tcp_v4_rcv+0x596/0x9c0:
tcp_v4_rcv in net/ipv4/tcp_ipv4.c:1740
Видно, что функция tcp_v4_send_reset вызывается в строке 1740 файла tcp_ipv4.c,
int tcp_v4_rcv(struct sk_buff *skb)
{
struct sock *sk;
sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
if (!sk)
goto no_tcp_socket;
...
no_tcp_socket:
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
goto discard_it;
if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
csum_error:
TCP_INC_STATS_BH(net, TCP_MIB_CSUMERRORS);
bad_packet:
TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
} else {
tcp_v4_send_reset(NULL, skb); // 1739 行
}
}
Единственная логика, которая может быть вызвана, заключается в том, что информация о сокете, соответствующая этому пакету, не может быть найдена, sk равен NULL, и тогда можно перейти к метке no_tcp_socket, а затем перейти к процессу else.
Как это возможно? Соединение существует хорошо, как он может не найти сокет соединения при получении отложенного ack-пакета для обработки? Далее давайте рассмотрим базовую реализацию функции __inet_lookup_skb и, наконец, перейдем к функции __inet_lookup_installed.
struct sock *__inet_lookup_established(struct net *net,
struct inet_hashinfo *hashinfo,
const __be32 saddr, const __be16 sport,
const __be32 daddr, const u16 hnum,
const int dif)
Помимо существующего явления, существует очень похожий сценарий RST, который заключается в отправке пакетов службе, которая не прослушивает определенный порт. Для этого пакета нет соответствующего соединения, и ядро ответит RST, сообщив отправителю, что пакет не может быть обработан.
На данный момент расследование зашло в тупик. Почему не удается найти стек протоколов ядра, хотя соединение все еще существует?
Режим моста Docker режим потока сетевых пакетов
При запуске процесса Docker на хосте будет создан виртуальный мост с именем docker0, и контейнер docker на этом хосте будет подключен к этому виртуальному мосту.
После запуска контейнера Docker сгенерирует пару veth-интерфейсов (vethpair), которые по сути эквивалентны соединению Ethernet, реализованному программным обеспечением.Docker подключает eth0 в контейнере к мосту docker0 через veth. Внешние соединения могут быть обеспечены с помощью маскировки IP-адресов, которая представляет собой метод преобразования сетевых адресов (NAT), установленный с помощью правил IP-переадресации и iptables.
Глубокое погружение в Netfilter и NAT
Netfilter — это инфраструктура ядра Linux, которая устанавливает несколько точек подключения в стеке протоколов ядра для перехвата, фильтрации или иной обработки пакетов. Его можно реализовать от простых брандмауэров до подробного анализа данных сетевого трафика и сложных фильтров пакетов, зависящих от состояния.
Docker использует функцию NAT (преобразование сетевых адресов) для преобразования исходного и конечного адресов в соответствии с определенными правилами. iptables — это инструмент пользовательского режима для управления этими сетевыми фильтрами.
Принцип структуры развертывания в этом сценарии показан на следующем рисунке.
После просмотра кода netfilter обнаруживается, что он пометит пакет вне окна как состояние INVALID, см. исходный кодnet/netfilter/nf_conntrack_proto_tcp.c
:
/* Returns verdict for packet, or -1 for invalid. */
static int tcp_packet(struct nf_conn *ct,
const struct sk_buff *skb,
unsigned int dataoff,
enum ip_conntrack_info ctinfo,
u_int8_t pf,
unsigned int hooknum,
unsigned int *timeouts) {
// ...
if (!tcp_in_window(ct, &ct->proto.tcp, dir, index,
skb, dataoff, th, pf)) {
spin_unlock_bh(&ct->lock);
return -NF_ACCEPT;
}
}
Вышеизложенное является чисто теоретическим анализом.Как вы можете сказать, что это недопустимый пакет, вызванный ACK?
Мы можем распечатать недопустимые пакеты через правила iptables.
iptables -A INPUT -m conntrack --ctstate INVALID -m limit --limit 1/sec -j LOG --log-prefix "invalid: " --log-level 7
После добавления вышеуказанных правил снова запустите усиленный сценарий загрузки и одновременно начните захват пакетов, и явление повторится.
Затем проверьте соответствующий журнал в dmesg.
Взяв в качестве примера первую строку, ее LEN=40, что составляет 20 заголовков IP + 20 байтов заголовков TCP, и установлен бит ACK, указывающий, что это пакет ACK без какого-либо содержимого, соответствующий началу RST. на рисунке выше пакет ACK. Детали этого пакета показаны на рисунке ниже, также верно, что окно равно 187.
Если это пакет в состоянии INVALID, netfilter не будет выполнять NAT-трансляцию IP и порта на нем, поэтому, когда стек протоколов будет искать соединение пакета по ip + порту, он не сможет его найти. , и в это время он ответит RST. , процесс показан на рисунке ниже.
Это также подтверждает нашу предыдущую логику кода, когда __inet_lookup_skb имеет значение null, а затем отправляет RST.
Как изменить
Зная причину, внести изменения очень просто, их два. Первая модификация немного груба: используйте iptables, чтобы удалить недопустимый пакет и предотвратить создание RST.
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
После этой модификации проблема была решена моментально, после десятков тестов не было таймаута загрузки или сбоя.
С этой модификацией есть небольшая проблема, которая может случайно повредить FIN-пакеты и некоторые другие действительно недопустимые пакеты. Более элегантная модификация заключается в изменении параметров ядра.net.netfilter.nf_conntrack_tcp_be_liberal
Установите на 1:
sysctl -w "net.netfilter.nf_conntrack_tcp_be_liberal=1"
net.netfilter.nf_conntrack_tcp_be_liberal = 1
После установки значения этого параметра в 1 пакет вне окна не будет помечен как INVALID, см. исходный кодnet/netfilter/nf_conntrack_proto_tcp.c
:
static bool tcp_in_window(const struct nf_conn *ct,
struct ip_ct_tcp *state,
enum ip_conntrack_dir dir,
unsigned int index,
const struct sk_buff *skb,
unsigned int dataoff,
const struct tcphdr *tcph,
u_int8_t pf) {
...
res = false;
if (sender->flags & IP_CT_TCP_FLAG_BE_LIBERAL ||
tn->tcp_be_liberal)
res = true;
...
return res;
}
Наконец, эта статья завершается плавным скриншотом загрузки.
постскриптум
Посмотрите на код и подозревайте какие-то невозможные явления. Вышеупомянутое может быть неправильным, просто посмотрите на метод.