Интересный опыт устранения неполадок в сети Docker

задняя часть Docker

Некоторое время назад была проблема с сервисом упаковки Android компании, явление такое, что при загрузке сервера 360 для армирования, очень вероятно, что он застрянет на этапе загрузки, и повторная попытка будет неудачной для много времени. Я провел некоторое расследование и анализ этой ситуации, решил проблему и написал эту длинную статью для обзора опыта расследования, которая будет включать следующее содержание.

  • Сетевая модель режима моста Docker
  • Netfilter и принцип NAT
  • Использование Systemtap в тестах ядра

Симптом

Структура развертывания службы упаковки выглядит следующим образом: Среда упаковки Android упакована в виде образа докера и развернута на физическом компьютере. показано на следующем рисунке:

android-docker

Проблема заключается в шаге загрузки 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.

docker network 原理

Глубокое погружение в Netfilter и NAT

Netfilter — это инфраструктура ядра Linux, которая устанавливает несколько точек подключения в стеке протоколов ядра для перехвата, фильтрации или иной обработки пакетов. Его можно реализовать от простых брандмауэров до подробного анализа данных сетевого трафика и сложных фильтров пакетов, зависящих от состояния.

Docker использует функцию NAT (преобразование сетевых адресов) для преобразования исходного и конечного адресов в соответствии с определенными правилами. iptables — это инструмент пользовательского режима для управления этими сетевыми фильтрами.

Принцип структуры развертывания в этом сценарии показан на следующем рисунке.

docker network 2

После просмотра кода 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. , процесс показан на рисунке ниже.

docker network 3

Это также подтверждает нашу предыдущую логику кода, когда __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;
}

Наконец, эта статья завершается плавным скриншотом загрузки.

постскриптум

Посмотрите на код и подозревайте какие-то невозможные явления. Вышеупомянутое может быть неправильным, просто посмотрите на метод.