Русский
Русский
English
Статистика
Реклама

Strace

Перевод Почему в Docker не работает Strace

02.07.2020 10:04:35 | Автор: admin
Когда я редактировала страницу о возможностях контейнеров для журнала How Containers Work, мне потребовалось объяснить, почему в Docker не работает strace. Вот что случалось при запуске strace в Docker-контейнере на моем ноутбуке:

$ docker run  -it ubuntu:18.04 /bin/bash$ # ... install strace ...root@e27f594da870:/# strace lsstrace: ptrace(PTRACE_TRACEME, ...): Operation not permitted

strace работает через системный вызов ptrace, поэтому без разрешения для ptrace ничего не заработает! Но это легко исправить, и на моем ноутбуке я все сделала вот так:

docker run --cap-add=SYS_PTRACE  -it ubuntu:18.04 /bin/bash

Но мне было интересно не решить проблему, а разобраться, почему эта ситуация вообще возникает. Так почему же strace не работает, а --cap-add=SYS_PTRACE все исправляет?

Гипотеза 1: У процессов контейнеров нет собственной привилегии CAP_SYS_PTRACE


Так как проблема стабильно решается через --cap-add=SYS_PTRACE, мне всегда казалось, что у процессов контейнеров Docker по определению нет собственной привилегии CAP_SYS_PTRACE, но по двум причинам тут кое-что не сходится.

Причина 1: В виде эксперимента я, будучи залогиненной под обычным пользователем, без труда могла запустить strace к любому процессу, однако проверка наличия у моего текущего процесса привилегии CAP_SYS_PTRACE ничего не находила:

$ getpcaps $$Capabilities for `11589': =

Причина 2: в man capabilities о привилегии CAP_SYS_PTRACE сказано следующее:

CAP_SYS_PTRACE       * Trace arbitrary processes using ptrace(2);

Вся суть CAP_SYS_PTRACE заключается в том, чтобы мы, по аналогии с root, могли перехватывать контроль над произвольным процессом любого пользователя. Для ptrace обычного процесса вашего пользователя такая привилегия не нужна.

Кроме того, я провела еще одну проверку: я запустила Docker контейнер через docker run --cap-add=SYS_PTRACE -it ubuntu:18.04 /bin/bash, затем отменила привилегию CAP_SYS_PTRACE и strace продолжил корректно работать даже без привилегии. Почему?!

Гипотеза 2: Дело в пользовательском пространстве имен?


Моя следующая (и намного хуже обоснованная) гипотеза звучала в духе хмм, возможно процесс находится в другом пользовательском пространстве имен и strace не работает просто потому что? Выглядит как набор не очень связных высказываний, но я все же попробовала посмотреть на проблему и с этой стороны.

Итак, находится ли процесс в другом пользовательском пространстве имен? Так он выглядит в контейнере:

root@e27f594da870:/# ls /proc/$$/ns/user -l... /proc/1/ns/user -> 'user:[4026531837]'

А так он выглядит на хосте:

bork@kiwi:~$ ls /proc/$$/ns/user -l... /proc/12177/ns/user -> 'user:[4026531837]'

root в контейнере это тот же самый пользователь, что и root на хосте, поскольку у них общий идентификатор в пользовательском пространстве имен (4026531837), так что с этой стороны не должно быть никаких мешающих работе strace причин. Как можно видеть, гипотеза оказалась так себе, но тогда я еще не осознавала, что пользователи в контейнере и на хосте совпадают, и этот поход казался мне интересным.

Гипотеза 3: Системный вызов ptrace блокируется правилом seccomp-bpf


Я уже знала, что для ограничения запуска большого числа системных вызовов процессорами контейнеров в Docker есть правило seccomp-bpf, и оказалось, что в его списке блокируемых по определению вызовов есть и ptrace! (На самом деле список вызовов это лист исключений и ptrace попросту в него не попадает, но результат от этого не меняется.)

Теперь понятно, почему в Docker контейнере не работает strace, ведь очевидно, что полностью заблокированный ptrace вызвать не получится.

Давайте проверим эту гипотезу и посмотрим, сможем ли мы воспользоваться strace в Docker контейнере, если отключим все правила seccomp:

$ docker run --security-opt seccomp=unconfined -it ubuntu:18.04  /bin/bash$ strace lsexecve("/bin/ls", ["ls"], 0x7ffc69a65580 /* 8 vars */) = 0... it works fine ...

Отлично! Все работает, и секрет раскрыт! Вот только

Почему --cap-add=SYS_PTRACE решает проблему?


Мы все еще не объяснили почему --cap-add=SYS_PTRACE решает возникающую проблему с вызовами. Главная страница docker run следующим образом объясняет работу аргумента --cap-add:

--cap-add=[]   Add Linux capabilities

Все это не имеет никакого отношения к правилам seccomp! В чем же дело?

Давайте посмотрим на исходный код Docker.


Если уже и документация не помогает, все что нам остается это погрузиться в исходники.
В Go есть одна приятная особенность: благодаря вендорингу зависимостей в репозитории Go вы через grep можете пройтись по всему репозиторию и найти интересующий вас код. Так что я склонировала github.com/moby/moby и прошерстила его в поисках выражений вида rg CAP_SYS_PTRACE.

На мой взгляд, тут происходит вот что: в имплементации seccomp в контейнере, в разделе contrib/seccomp/seccomp_default.go полно кода, который через правило seccomp проверяет, есть ли у процесса с привилегиями разрешение на использование системных вызовов в соответствии с этой привилегией.

case "CAP_SYS_PTRACE":s.Syscalls = append(s.Syscalls, specs.LinuxSyscall{Names: []string{"kcmp","process_vm_readv","process_vm_writev","ptrace",},Action: specs.ActAllow,Args:   []specs.LinuxSeccompArg{},})


Там еще есть код, который в moby и для profiles/seccomp/seccomp.go, и для seccomp профиля по определению проводит похожие операции, так что, наверное, мы нашли наш ответ!

В Docker --cap-add способен на большее, чем сказано


В итоге, похоже, --cap-add делает не совсем то, что написано на главной странице, и скорее должен выглядеть как --cap-add-and-also-whitelist-some-extra-system-calls-if-required. И это похоже на правду: если у вас есть привилегия в духе CAP_SYS_PTRACE, которая разрешает вам пользоваться системным вызовом process_vm_readv, но этот вызов блокируется профилем seccomp, вам это мало чем поможет, так что разрешение на использование системных вызовов process_vm_readv и ptrace через CAP_SYS_PTRACE выглядит разумно.

Оказывается, strace работает в последних версиях Docker


Для версий ядра 4.8 и выше благодаря этому коммиту в Docker 19.03 наконец-то разрешены системные вызовы ptrace. Вот только на моем ноутбуке Docker все еще версии 18.09.7, и этот коммит, очевидно, отсутствует.

Вот и все!


Разбираться с этой проблемой оказалось интересно, и я думаю, что это хороший пример нетривиально взаимодействующей между собой подвижной начинки контейнеров.

Если вам понравился этот пост, вам может понравиться и мой журнал How Containers Work, на его 24 страницах объясняются особенности ядра Linux по организации работы контейнеров. Там же вы можете ознакомиться с привилегиями и seccomp-bpf.
Подробнее..

Эта маленькая правка не может убить сервер

01.09.2020 14:23:02 | Автор: admin
Вначале был сервер. И на этом сервере жил сайт, допустим, example.com.
Шло время, сайт рос, одного сервера ему стало мало, да ещё и ребрендинга захотелось. И оставил сисадмин его бакендом, и создал для него фронтенд, допустим, example2.com.

Так и появилась типичная схема на базе веб-сервера Nginx с фронтендом example2.com и бакендом example.com, и в /etc/hostname на этих серверах были указаны именно эти данные. Для example.com также осталась прежняя ресурсная А-запись в настройках ДНС.

Сервера стабильно работали, uptime рос, а сайт всё дальше развивался и владельцы решили провести ещё один реберендинг и сменить имя (например) на example2.wtf. Готовясь к изменениям, зашел сисадмин на фронтенд-сервер и сменил в /etc/hostname строку example2.com на example2.wtf, но, правда, дело дальше не пошло и зону сайта решили оставить прежней, соответственно, конфиги Nginx не менять. Но и в /etc/hostname прежнюю запись обратно не вернули.

Ну не может же эта незначительная правка убить Nginx!
Или всё таки может?


Здесь и далее:

$ head -1 /etc/*releasePRETTY_NAME="Debian GNU/Linux 10 (buster)"

$ nginx -vnginx version: nginx/1.19.2


Uptime рос дальше, сервера работали всё также безотказно, но случился день когда то ли понадобилось перезагрузить сервер, то ли пропало питание, в общем не важно. Суть в том, что конфиги Nginx ни до этого, ни во время никто не менял, однако:

$ nginx -tnginx: [emerg] host not found in upstream "example" in /nginx/vhosts/example2.com:9nginx: configuration file /etc/nginx/nginx.conf test failed


Рабочие конфиги Nginx приведены ниже:

$ cat /etc/nginx/nginx.confuser www-data;worker_processes  1;error_log  /var/log/nginx/error.log;events {}http {  log_format domainlog '$scheme\t$server_name\t$host\t$upstream_addr\t$status\t$upstream_status';  include       mime.types;  default_type  application/octet-stream;  include /nginx/upstream/*;  include /nginx/vhosts/*;}

$ cat /nginx/vhosts/example2.comserver {  listen 80;  server_name example2.com;  access_log /var/log/nginx/access/example2.com domainlog;  location / {    proxy_pass http://example;    proxy_set_header Host $host;  }}


$ cat /nginx/upstreams/example2.comupstream example {  server 93.184.216.34:80;}


Я намеренно взял слово рабочие в кавычки. Эти конфиги содержат ошибку, заключающуюся всего лишь в одной букве s:

image

Одна из причин приведших к этой проблеме заключается в том, что Nginx не считает ошибкой некорректные пути, если они содержат звездочку, хотя она и находится вне однозначно определённого каталога, который задан неверно. И, как можно заметить, Nginx знает о проблеме:

image

$ strace nginx -t 2>&1 | grep -F "ENOENT"access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)openat(AT_FDCWD, "/nginx/upstream", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = -1 ENOENT (No such file or directory)connect(6, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)connect(5, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)


Так как дальше будет много выкладок strace, мы максимально облегчим Nginx, собрав его таким образом:

$ nginx -Vnginx version: nginx/1.19.2built by gcc 8.3.0 (Debian 8.3.0-6)configure arguments: --prefix=/usr/share/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/run/nginx.pid --without-http_charset_module --without-http_gzip_module --without-http_ssi_module --without-http_userid_module --without-http_access_module --without-http_auth_basic_module --without-http_mirror_module --without-http_autoindex_module --without-http_geo_module --without-http_map_module --without-http_split_clients_module --without-http_referer_module --without-http_rewrite_module --without-http_fastcgi_module --without-http_uwsgi_module --without-http_scgi_module --without-http_grpc_module --without-http_memcached_module --without-http_limit_conn_module --without-http_limit_req_module --without-http_empty_gif_module --without-http_browser_module --without-http_upstream_hash_module --without-http_upstream_ip_hash_module --without-http_upstream_least_conn_module --without-http_upstream_keepalive_module --without-http_upstream_zone_module --without-poll_module


Также добавим пару строк в исходники, для вывода имени proxy_pass и значения определенного Nginx'ом в качестве upstream-сервера.

$ diff /scripts/nginx/nginx-1.19.2/src/http/ngx_http_upstream_round_robin.c /scripts/nginx/nginx-1.19.2.0/src/http/ngx_http_upstream_round_robin.c232a233>         ngx_log_stderr(0, "%V\n",&peer[i].name);238a240>     ngx_log_stderr(0, "upstream \"%V\" in %s:%ui", &us->host, us->file_name, us->line);


Здесь ещё важны настройки ресолвинга, предположим, что они имели такой вид:

$ cat /etc/resolv.confnameserver 8.8.8.8


Теперь с нашими правками, корректная проверка конфига будет выглядеть следующим образом:

$ hostname example2.com$ nginx -tnginx: 93.184.216.34:80nginx: [2606:2800:220:1:248:1893:25c8:1946]:80nginx: upstream "example" in /nginx/vhosts/example2.com:9nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful


И эта проверка по прежнему проходит с ошибочным конфигом Nginx и запросы при этом корректно проксируются на целевой бакенд-сервер 93.184.216.34:80 указанный в upstream.

Как же это происходит:

$ strace nginx -t 2>&1 | grep -vE "(mmap|munmap|mprotect|poll|stat|ioctl|write|close|getsockname)" | grep -vE "^\)" | grep . | grep -FA 24 "/etc/resolv.conf"openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "nameserver 8.8.8.8\n", 4096)   = 19read(5, "", 4096)                       = 0uname({sysname="Linux", nodename="example2.com", ...}) = 0openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5lseek(5, 0, SEEK_CUR)                   = 0read(5, "127.0.0.1\tlocalhost\n::1\t\tlocalho"..., 4096) = 129lseek(5, 0, SEEK_CUR)                   = 129read(5, "", 4096)                       = 0openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\21\0\0\0\0\0\0"..., 832) = 832openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240C\0\0\0\0\0\0"..., 832) = 832socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\20\353\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\1\0\1", iov_len=29}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=29}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\330\370\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\34\0\1", iov_len=29}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=29}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "\20\353\201\200\0\1\0\1\0\0\0\0\7example\3com\0\0\1\0\1\300\f\0"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 45recvfrom(5, "\330\370\201\200\0\1\0\1\0\0\0\0\7example\3com\0\0\34\0\1\300\f\0"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 57openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "# Configuration for getaddrinfo("..., 4096) = 2584read(5, "", 4096)                       = 0futex(0x7efdc41f2044, FUTEX_WAKE_PRIVATE, 2147483647) = 0socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("93.184.216.34")}, 16) = 0


Ссылка на картинку с описанными шагами:
hsto.org/webt/fp/fe/jp/fpfejpnv6i3kum27g4viqsa3d4m.png

1) Из /etc/resolv.conf получаются данные о сервере;
2) В нашем случае это 8.8.8.8;
3) Узнаем hostname сервера example2.com;
4,5) Пытаемся отресолвить имя example.com на 8.8.8.8;
6) Получаем в ответ 93.184.216.34 как имя example.com

Однако, глядя на доменные имена в шаге 3 и 5 по прежнему ничего не понятно. Если наш hostname именуется example2.com почему Nginx ресолвит некий example.com?

Вернёмся к моменту, когда мы убили сервер изменив его hostname:

$ hostname example2.wtf$ strace nginx -t 2>&1 | grep -vE "(mmap|munmap|mprotect|poll|stat|ioctl|write|close|getsockname)" | grep -vE "^\)" | grep . | grep -FA 24 "/etc/resolv.conf"openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "nameserver 8.8.8.8\n", 4096)   = 19read(5, "", 4096)                       = 0uname({sysname="Linux", nodename="example2.wtf", ...}) = 0openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5lseek(5, 0, SEEK_CUR)                   = 0read(5, "127.0.0.1\tlocalhost\n::1\t\tlocalho"..., 4096) = 129lseek(5, 0, SEEK_CUR)                   = 129read(5, "", 4096)                       = 0openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\21\0\0\0\0\0\0"..., 832) = 832openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240C\0\0\0\0\0\0"..., 832) = 832socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="F\23\1\0\0\1\0\0\0\0\0\0\7example\3wtf\0\0\1\0\1", iov_len=29}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=29}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="{#\1\0\0\1\0\0\0\0\0\0\7example\3wtf\0\0\34\0\1", iov_len=29}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=29}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "F\23\201\203\0\1\0\0\0\1\0\0\7example\3wtf\0\0\1\0\1\300\24\0"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 111recvfrom(5, "{#\201\203\0\1\0\0\0\1\0\0\7example\3wtf\0\0\34\0\1\300\24\0"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 111socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="V2\1\0\0\1\0\0\0\0\0\0\7example\0\0\1\0\1", iov_len=25}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=25}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="3H\1\0\0\1\0\0\0\0\0\0\7example\0\0\34\0\1", iov_len=25}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=25}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "V2\201\203\0\1\0\0\0\1\0\0\7example\0\0\1\0\1\0\0\6\0\1\0\1"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 100recvfrom(5, "3H\201\203\0\1\0\0\0\1\0\0\7example\0\0\34\0\1\0\0\6\0\1\0\1"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 100brk(0x55ddf1d34000)                     = 0x55ddf1d34000


В данном случае нам не удалось отресолвить доменное имя example.wtf, которое также не понятно откуда возникло.

Ещё одна попытка:

$ hostname habr.ru$ nginx -tnginx: [emerg] host not found in upstream "example" in /nginx/vhosts/example2.com:9nginx: configuration file /etc/nginx/nginx.conf test failed$ strace nginx -t 2>&1 | grep -vE "(mmap|munmap|mprotect|poll|stat|ioctl|write|close|getsockname)" | grep -vE "^\)" | grep . | grep -FA 24 "/etc/resolv.conf"openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "nameserver 8.8.8.8\n", 4096)   = 19read(5, "", 4096)                       = 0uname({sysname="Linux", nodename="habr.ru", ...}) = 0openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5lseek(5, 0, SEEK_CUR)                   = 0read(5, "127.0.0.1\tlocalhost\n::1\t\tlocalho"..., 4096) = 129lseek(5, 0, SEEK_CUR)                   = 129read(5, "", 4096)                       = 0openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\21\0\0\0\0\0\0"..., 832) = 832openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240C\0\0\0\0\0\0"..., 832) = 832socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\302\300\1\0\0\1\0\0\0\0\0\0\7example\2ru\0\0\1\0\1", iov_len=28}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=28}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\345\315\1\0\0\1\0\0\0\0\0\0\7example\2ru\0\0\34\0\1", iov_len=28}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=28}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "\302\300\201\203\0\1\0\0\0\1\0\0\7example\2ru\0\0\1\0\1\300\24\0\6"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 89recvfrom(5, "\345\315\201\203\0\1\0\0\0\1\0\0\7example\2ru\0\0\34\0\1\300\24\0\6"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 89socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="N6\1\0\0\1\0\0\0\0\0\0\7example\0\0\1\0\1", iov_len=25}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=25}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\216P\1\0\0\1\0\0\0\0\0\0\7example\0\0\34\0\1", iov_len=25}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=25}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "N6\201\203\0\1\0\0\0\1\0\0\7example\0\0\1\0\1\0\0\6\0\1\0\1"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 100recvfrom(5, "\216P\201\203\0\1\0\0\0\1\0\0\7example\0\0\34\0\1\0\0\6\0\1\0\1"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 100brk(0x55d9815a6000)                     = 0x55d9815a6000


Теперь наш hostname habr.ru, а Nginx не запускается, так как не может разрешить имя example.ru.

Ну и на закуску:

$ hostname www.yandex.com$ nginx -tnginx: 213.180.204.242:80nginx: [2a02:6b8::242]:80nginx: upstream "example" in /nginx/vhosts/example2.com:9nginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successful$ strace nginx -t 2>&1 | grep -vE "(mmap|munmap|mprotect|poll|stat|ioctl|write|close|getsockname)" | grep -vE "^\)" | grep . | grep -FA 24 "/etc/resolv.conf"openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "nameserver 8.8.8.8\n", 4096)   = 19read(5, "", 4096)                       = 0uname({sysname="Linux", nodename="www.yandex.com", ...}) = 0openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 5lseek(5, 0, SEEK_CUR)                   = 0read(5, "127.0.0.1\tlocalhost\n::1\t\tlocalho"..., 4096) = 129lseek(5, 0, SEEK_CUR)                   = 129read(5, "", 4096)                       = 0openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\21\0\0\0\0\0\0"..., 832) = 832openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 5read(5, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240C\0\0\0\0\0\0"..., 832) = 832socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, 16) = 0sendmmsg(5, [{msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\305\324\1\0\0\1\0\0\0\0\0\0\7example\6yandex\3com\0"..., iov_len=36}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=36}, {msg_hdr={msg_name=NULL, msg_namelen=0, msg_iov=[{iov_base="\346\345\1\0\0\1\0\0\0\0\0\0\7example\6yandex\3com\0"..., iov_len=36}], msg_iovlen=1, msg_controllen=0, msg_flags=0}, msg_len=36}], 2, MSG_NOSIGNAL) = 2recvfrom(5, "\305\324\201\200\0\1\0\2\0\0\0\0\7example\6yandex\3com\0"..., 2048, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 79recvfrom(5, "\346\345\201\200\0\1\0\2\0\0\0\0\7example\6yandex\3com\0"..., 65536, 0, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("8.8.8.8")}, [28->16]) = 91openat(AT_FDCWD, "/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 5read(5, "# Configuration for getaddrinfo("..., 4096) = 2584read(5, "", 4096)                       = 0futex(0x7f40c6c22044, FUTEX_WAKE_PRIVATE, 2147483647) = 0socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, IPPROTO_IP) = 5connect(5, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("213.180.204.242")}, 16) = 0


С указанным hostname www.yandex.com Nginx успешно стартует, но ресолвит доменное имя example.yandex.com и соответственно проксирует трафик на 213.180.204.242.

Итого: если каталог с апстримами указан неверно (или не указан совсем) Nginx ресолвит доменное имя согласно правилам описанным в /etc/resolve.conf (учтены будут не только директивы nameserver, но и, например, search), а само доменное имя формирует путём замены части имени hostname (низшего уровня) на имя указанное в директиве proxy_pass.

Пример:
Если
hostname задан как uno.duo.tres.yandex.ru,
proxy_pass как http://example,
то ресолвить Nginx будет example.duo.tres.yandex.ru

Тут хотелось написать что-то язвительное, но к тому моменту, когда я решил этот ребус, не осталось никаких сил.

В общем, пишите конфиги без ошибок, ибо кривые админские руки способны поломать даже такой хороший продукт как Nginx.
Подробнее..

Как Иван ошибку в бэкенде локализовывал

09.09.2020 12:15:30 | Автор: admin
В комментариях к одной из моих статей про базовые команды Linux shell для тестировщиков справедливо заметили, что в ней не было указано применение команд в процессе тестирования. Я подумал, что лучше поздно, чем никогда, поэтому решил рассказать историю Backend QA-инженера Вани, который столкнулся с неожиданным поведением сервиса и попытался разобраться, где именно случилась ошибка.



Что тестировал Ваня


Ваня знал, что ему предстоит тестировать связку nginx + сервис.
Здесь я сразу сделаю ремарку: такая связка была выбрана для этой статьи просто потому, что она нагляднее всего может продемонстрировать применение различных утилит при дебаге проблемы и потому, что её очень просто сконфигурировать и поднять. В реальных условиях это может быть либо просто сервис, либо цепочка сервисов, которые делают запросы друг другу.

В качестве сервиса выступает дефолтный HTTP сервер Python SimpleHTTPServer, который в ответ на запрос без параметров выводит содержимое текущей директории:

[root@ivan test_dir_srv]# ls -ltotal 0-rw-r--r-- 1 root root 0 Aug 25 11:23 test_file[root@ivan test_dir_srv]# python3 -m http.server --bind 127.0.0.1 8000Serving HTTP on 127.0.0.1 port 8000 (http://personeltest.ru/away/127.0.0.1:8000/) ...

Nginx же сконфигурирован следующим образом:

upstream test {server 127.0.0.1:8000;}server {listen    80;location / {proxy_pass http://test;}}

Ване нужно было протестировать один-единственный тест-кейс: проверить, что запрос на / работает. Он проверил, и всё работало:

MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Directory listing for /</title></head><body><h1>Directory listing for /</h1><hr><ul><li><a href="test_file">test_file</a></li></ul><hr></body></html>

Но затем в один момент на тестовом стенде разработчики что-то обновили, и Ваня получил ошибку:

MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78<html><head><title>502 Bad Gateway</title></head><body bgcolor="white"><center><h1>502 Bad Gateway</h1></center><hr><center>nginx/1.14.2</center></body></html>

Он решил не скидывать эту непонятную ошибку разработчикам, а получить доступ по ssh на сервер и разобраться, в чём там дело. Знаний в области такого рода дебага проблем у него было мало, но он очень хотел научиться, поэтому вооружился поисковиками, логикой и пошёл локализовывать баг.

Первая мысль Вани: логи


Действительно, если случилась ошибка, то нужно просто найти её в лог-файле. Но сначала нужно найти сам лог-файл. Ваня полез в Google и узнал, что часто логи лежат в директории /var/log. Действительно, там нашлась директория nginx:

[root@ivan ~]# ls /var/log/nginx/access.log access.log-20200831 error.log error.log-20200831

Иван посмотрел последние строчки error лога и понял, в чём дело: разработчики ошиблись в конфигурации nginx, в порт upstream закралась опечатка.

[root@ivan ~]# tail /var/log/nginx/error.log2020/08/31 04:36:21 [error] 15050#15050: *90452 connect() failed (111: Connection refused) while connecting to upstream, client: 31.170.95.221, server: , request: "GET / HTTP/1.0", upstream: "http://127.0.0.1:8009/", host: "12.34.56.78"

Какой можно сделать из этого вывод? Логи лучший друг тестировщиков и разработчиков при локализации ошибок. Если есть какое-то неожиданное поведение сервиса, а в логах при этом ничего нет, то это повод вернуть задачу в разработку с просьбой добавить логов. Ведь если б nginx не писал в лог о неудачной попытке достучаться до апстрима, то, согласитесь, искать проблему было бы сложнее?

В тот момент Ваня задумался: А что, если бы в nginx логи лежали в другой директории? Как бы я их нашёл? Через пару лет у Вани будет больше опыта работы с сервисами в Linux, и он будет знать, что путь к лог-файлу часто передают сервису аргументом командной строки, либо он содержится в файле конфигурации, путь к которому также часто передают сервису аргументом командной строки. Ну и в идеале путь к лог-файлу должен быть прописан в документации сервиса.

Кстати, через файл конфигурации можно найти путь к лог-файлу и в nginx:

[root@ivan ~]# ps ax | grep nginx | grep masterroot   19899 0.0 0.0 57392 2872 ?    Ss  2019  0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf[root@ivan ~]# grep "log" /etc/nginx/nginx.conferror_log /var/log/nginx/error.log warn;log_format main '$remote_addr - $remote_user [$time_local] "$request" 'access_log /var/log/nginx/access.log main;

А что если в логах ничего нет?


В свободное время Ваня решил подумать, а как бы он справился с задачей, если бы nginx не писал ничего в лог. Ваня знал, что сервис слушает порт 8000, поэтому решил посмотреть трафик на этом порту на сервере. С этим ему помогла утилита tcpdump. При правильной конфигурации он видел запрос и ответ:

Дамп трафика на порту 8000
[root@ivan ~]# tcpdump -nn -i lo -A port 8000tcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes09:10:42.114284 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [S], seq 3390176024, win 43690, options [mss 65495,sackOK,TS val 830366494 ecr 0,nop,wscale 8], length 0E..<..@.@..............@.............0.........1~c.........09:10:42.114293 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [S.], seq 4147196208, ack 3390176025, win 43690, options [mss 65495,sackOK,TS val 830366494 ecr 830366494,nop,wscale 8], length 0E..<..@.@.<..........@...110.........0.........1~c.1~c.....09:10:42.114302 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 1, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0E..4..@.@..............@.....111.....(.....1~c.1~c.09:10:42.114329 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [P.], seq 1:88, ack 1, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 87E.....@.@..b...........@.....111...........1~c.1~c.GET / HTTP/1.0Host: testConnection: closeUser-Agent: curl/7.64.1Accept: */*09:10:42.114333 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [.], ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0E..4R/@.@............@...111...p.....(.....1~c.1~c.09:10:42.115062 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [P.], seq 1:155, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 154E...R0@.@............@...111...p...........1~c.1~c.HTTP/1.0 200 OKServer: SimpleHTTP/0.6 Python/3.7.2Date: Mon, 07 Sep 2020 13:10:42 GMTContent-type: text/html; charset=utf-8Content-Length: 34009:10:42.115072 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 155, win 175, options [nop,nop,TS val 830366494 ecr 830366494], length 0E..4.@.@..............@...p.11......(.....1~c.1~c.09:10:42.115094 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [P.], seq 155:495, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 340E...R1@.@..<.........@...11....p.....|.....1~c.1~c.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>Directory listing for /</title></head><body><h1>Directory listing for /</h1><hr><ul><li><a href="test_file">test_file</a></li></ul><hr></body></html>09:10:42.115098 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 495, win 180, options [nop,nop,TS val 830366494 ecr 830366494], length 0E..4.@.@..............@...p.13......(.....1~c.1~c.09:10:42.115128 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [F.], seq 495, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0E..4R2@.@............@...13....p.....(.....1~c.1~c.09:10:42.115264 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [F.], seq 88, ack 496, win 180, options [nop,nop,TS val 830366495 ecr 830366494], length 0E..4..@.@..............@...p.13 .....(.....1~c.1~c.09:10:42.115271 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [.], ack 89, win 171, options [nop,nop,TS val 830366495 ecr 830366495], length 0E..4R3@.@............@...13 ...q.....(.....1~c.1~c.^C12 packets captured24 packets received by filter0 packets dropped by kernel


При неправильной конфигурации (с портом 8009 в апстриме nginx) на порту 8000 никакого трафика не было. Ваня обрадовался: теперь даже если разработчики забыли реализовать запись в лог при сетевых ошибках, всё равно можно хотя бы узнать, идёт ли трафик на нужный хост или порт.

Какой вывод можно сделать из этой истории? Даже если логов нет, в Linux есть утилиты, которые могут помочь с локализацией проблем.

А если не сеть?


Всё хорошо работало, но однажды Ваня снова получил ошибку, на этот раз другую:

MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN""http://www.w3.org/TR/html4/strict.dtd"><html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8"><title>Error response</title></head><body><h1>Error response</h1><p>Error code: 404</p><p>Message: File not found.</p><p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p></body></html>

Ваня снова зашёл на сервер, но в этот раз проблема не была связана с сетью. В логе сервиса тоже было написано File not found, и Ваня решил разобраться, почему внезапно появилась такая ошибка. Он знает, что есть процесс python3 -m http.server, но не знает, содержимое какой директории выводит этот сервис (или, другими словами, какая у этого процесса current working directory). Он узнаёт это с помощью команды lsof:

[root@ivan ~]# ps aux | grep python | grep "http.server"root   20638 0.0 0.3 270144 13552 pts/2  S+  08:29  0:00 python3 -m http.server[root@ivan ~]# lsof -p 20638 | grep cwdpython3 20638 root cwd  DIR   253,1   4096 1843551 /root/test_dir_srv2

Также это можно сделать с помощью команды pwdx или с помощью директории proc:

[root@ivan ~]# pwdx 2063820638: /root/test_dir_srv2[root@ivan ~]# ls -l /proc/20638/cwdlrwxrwxrwx 1 root root 0 Aug 31 08:37 /proc/20638/cwd -> /root/test_dir_srv2

Такая директория действительно есть на сервере, и в ней лежит файл с именем test_file. В чём же дело? Иван погуглил и нашёл утилиту strace, с помощью которой можно смотреть, какие системные вызовы выполняет процесс (про strace, кстати, есть хорошая статья на Хабре, и даже не одна). Можно либо запускать новый процесс через strace, либо подключаться этой утилитой к уже запущенному процессу. Ване подходил второй вариант:

Вывод утилиты strace
[root@ivan ~]# strace -ff -p 20638strace: Process 20638 attachedrestart_syscall(<... resuming interrupted poll ...>) = 0poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 1 ([{fd=4, revents=POLLIN}])accept4(4, {sa_family=AF_INET, sin_port=htons(57530), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 5clone(child_stack=0x7f2beeb28fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f2beeb299d0, tls=0x7f2beeb29700, child_tidptr=0x7f2beeb299d0) = 21062futex(0x11204d0, FUTEX_WAIT_PRIVATE, 0, NULLstrace: Process 21062 attached<unfinished ...>[pid 21062] set_robust_list(0x7f2beeb299e0, 24) = 0[pid 21062] futex(0x11204d0, FUTEX_WAKE_PRIVATE, 1) = 1[pid 20638] <... futex resumed> )    = 0[pid 20638] futex(0x921c9c, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 27, {1598879772, 978949000}, ffffffff <unfinished ...>[pid 21062] futex(0x921c9c, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x921c98, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1[pid 20638] <... futex resumed> )    = 0[pid 20638] futex(0x921cc8, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>[pid 21062] futex(0x921cc8, FUTEX_WAKE_PRIVATE, 1) = 1[pid 20638] <... futex resumed> )    = 0[pid 20638] futex(0x921cc8, FUTEX_WAKE_PRIVATE, 1) = 0[pid 20638] poll([{fd=4, events=POLLIN}], 1, 500 <unfinished ...>[pid 21062] recvfrom(5, "GET / HTTP/1.1\r\nConnection: upgr"..., 8192, 0, NULL, NULL) = 153[pid 21062] stat("/root/test_dir_srv/", 0x7f2beeb27350) = -1 ENOENT (No such file or directory)[pid 21062] open("/root/test_dir_srv/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)[pid 21062] write(2, "127.0.0.1 - - [31/Aug/2020 09:16"..., 70) = 70[pid 21062] write(2, "127.0.0.1 - - [31/Aug/2020 09:16"..., 60) = 60[pid 21062] sendto(5, "HTTP/1.0 404 File not found\r\nSer"..., 184, 0, NULL, 0) = 184[pid 21062] sendto(5, "<!DOCTYPE HTML PUBLIC \"-//W3C//D"..., 469, 0, NULL, 0) = 469[pid 21062] shutdown(5, SHUT_WR)    = 0[pid 21062] close(5)          = 0[pid 21062] madvise(0x7f2bee329000, 8368128, MADV_DONTNEED) = 0[pid 21062] exit(0)           = ?[pid 21062] +++ exited with 0 +++<... poll resumed> )          = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500)  = 0 (Timeout)poll([{fd=4, events=POLLIN}], 1, 500^Cstrace: Process 20638 detached<detached ...>


Обычно вывод strace довольно объёмный (а может быть и очень большим), поэтому удобнее сразу перенаправлять его в файл и потом уже искать в нём нужные системные вызовы. В данном же случае можно сразу обнаружить, что сервис пытается открыть директорию /root/test_dir_srv/ кто-то переименовал её и не перезапустил после этого сервис, поэтому он возвращает 404.

Если сразу понятно, какие именно системные вызовы нужно посмотреть, можно использовать опцию -e:

[root@ivan ~]# strace -ff -e trace=open,stat -p 20638strace: Process 20638 attachedstrace: Process 21396 attached[pid 21396] stat("/root/test_dir_srv/", 0x7f2beeb27350) = -1 ENOENT (No such file or directory)[pid 21396] open("/root/test_dir_srv/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)[pid 21396] +++ exited with 0 +++^Cstrace: Process 20638 detached

Вывод: иногда можно немножко залезть под капот процессу, а помогает с этим strace. Так как эта утилита выводит все системные вызовы, которые использует процесс, то с её помощью также можно находить и сетевые проблемы (например, к какому хосту/порту пытается подключиться процесс), что делает её довольно универсальным инструментом дебага. Также существует похожая утилита ltrace.

Есть ли что-то ещё?


Ваня на этом не остановился и узнал, что есть GNU Project Debugger GDB. С его помощью можно залезть в процесс и даже немного модифицировать его. И Ваня решил попробовать обнаружить последнюю ошибку с помощью GDB. Он предположил, что раз сервис выводит содержимое директории, то можно попробовать поставить breakpoint на функции open() и посмотреть, что будет:
Вывод утилиты gdb
[root@ivan ~]# gdb -p 23998GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-119.el7Copyright (C) 2013 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.  Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Attaching to process 23998 <здесь много сообщений о загрузке символов и отсутствии debugging symbols...>...0x00007f2284c0b20d in poll () at ../sysdeps/unix/syscall-template.S:8181T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)Missing separate debuginfos, use: debuginfo-install keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.15.1-34.el7.x86_64 libcom_err-1.42.9-13.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libselinux-2.5-14.1.el7.x86_64 openssl-libs-1.0.2k-16.el7.x86_64 pcre-8.32-17.el7.x86_64 zlib-1.2.7-18.el7.x86_64(gdb) set follow-fork-mode child(gdb) b openBreakpoint 1 at 0x7f2284c06d20: open. (2 locations)(gdb) cContinuing.[New Thread 0x7f227a165700 (LWP 24030)][Switching to Thread 0x7f227a165700 (LWP 24030)]Breakpoint 1, open64 () at ../sysdeps/unix/syscall-template.S:8181T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)(gdb) n83T_PSEUDO_END (SYSCALL_SYMBOL)(gdb) n_io_FileIO___init___impl (opener=<optimized out>, closefd=<optimized out>, mode=<optimized out>, nameobj=0x7f227a68f6f0, self=0x7f227a68f6c0) at ./Modules/_io/fileio.c:381381                Py_END_ALLOW_THREADS(gdb) n379                self->fd = open(name, flags, 0666);(gdb) n381                Py_END_ALLOW_THREADS(gdb) print name$1 = 0x7f227a687c90 "/root/test_dir_srv/"(gdb) qA debugging session is active.Inferior 1 [process 23998] will be detached.Quit anyway? (y or n) yDetaching from program: /usr/local/bin/python3.7, process 23998[Inferior 1 (process 23998) detached]


После команды c (continue) Ваня в другой консоли запустил curl, попал в дебаггере в точку останова и стал выполнять эту программу (то есть сервис) по шагам. Как только он нашёл в коде open по какому-то пути name, он вывел значение этой переменной и увидел /root/test_dir_srv/.
GDB это мощный инструмент, и здесь описан простейший вариант его использования. Иногда он может помочь в воспроизведении каких-либо сложных кейсов (например, можно приостановить процесс в нужный момент и воспроизвести состояние гонки), также он помогает с чтением core dump файлов.

А что если Docker?


В один момент DevOps решили, что сервис теперь будет деплоиться Docker-контейнером, и нужно было провести ретест всех кейсов, которые нашёл Ваня. Ваня без проблем нагуглил следующее:

  1. Использовать tcpdump, strace и gdb можно и внутри контейнера, но нужно иметь ввиду Linux capabilities (есть статья, которая объясняет, почему strace не работал в контейнере без --cap-add=SYS_PTRACE).
  2. Можно использовать опцию --pid.

Но ему стало интересно, можно ли посмотреть весь трафик, идущий в контейнер (или из контейнера), прям с хоста. У tcpdump есть возможность выводить трафик какого-либо интерфейса (опция -i), каждому контейнеру соответствует один виртуальный интерфейс veth (это можно увидеть, например, через ifconfig или ip a), но как понять, какому контейнеру какой интерфейс соответствует? Если контейнер не использует host networking, то внутри него будет сетевой интерфейс eth0, через который он может общаться по сети с другими контейнерами и хостом. Остаётся лишь найти, ifindex какого интерфейса на хосте совпадает с iflink интерфейса eth0 контейнера (что это означает можно почитать здесь).

[root@ivan ~]# for f in `ls /sys/class/net/veth*/ifindex`; do echo $f; cat $f; done | grep -B 1 `docker exec test_service_container cat /sys/class/net/eth0/iflink` | head -1/sys/class/net/veth6c18dba/ifindex

Теперь можно запускать tcpdump для интерфейса veth6c18dba:

tcpdump -i veth6c18dba

Но есть способ проще: можно найти IP-адрес контейнера в его сети и слушать трафик на нём:

[root@ivan ~]# docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test_service_container172.17.0.10[root@ivan ~]# tcpdump -i any host 172.17.0.10

Вывод: дебаг в Docker-контейнере это не страшно. Утилиты в нём работают, а для чтения логов можно использовать docker logs.

Выводы


Как ответственный инженер, Ваня решил кратко законспектировать новую для себя информацию во внутренней базе знаний. Вот что он написал:

  • Логи лучший друг человека. Если встречается неожиданное поведение сервиса и при этом он не пишет ничего в лог это повод попросить разработчиков добавить логов.
  • Иногда бывает, что локализовать ошибку надо, даже если в логах ничего нет. К счастью, в Linux есть много утилит, которые помогают с этим.
  • С дебагом любых сетевых коммуникаций помогает tcpdump. Он помогает видеть, какой трафик откуда и куда идёт на сервере.
  • Заглянуть внутрь процесса помогают утилиты strace, ltrace и gdb.
  • Всё это можно использовать, если сервис работает в Docker-контейнере.
  • Много информации о процессе есть в директориях /proc/PID. Например, в /proc/PID/fd находятся симлинки на все открытые процессом файлы.
  • Также помочь получить различную информацию о системе, файлах или процессах могут утилиты ps, ls, stat, lsof, ss, du, top, free, ip, ldconfig, ldd и прочие.

Надеюсь, вам была полезна эта история, и хотя бы однажды она поможет вам понять, в чём дело, когда вы будете что-то дебажить в Linux. Если Ваня что-то упустил, делитесь этим в комментариях!
Подробнее..

Перевод Разбираемся с системными вызовами в Linux с помощью strace

26.10.2020 02:07:54 | Автор: admin
Перевод статьи подготовлен специально для студентов базового и продвинутого курсов Administrator Linux.



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

В операционной системе можно выделить два режима работы:

  • Режим ядра (kernel mode) привилегированный режим, используемый ядром операционной системы.
  • Пользовательский режим (user mode) режим, в котором выполняется большинство пользовательских приложений.


Пользователи при повседневной работе обычно используют утилиты командной строки и графический интерфейс (GUI). При этом в фоне незаметно работают системные вызовы, обращаясь к ядру для выполнения работы.

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

Большая часть этих деталей скрыта от пользователя в системных библиотеках (glibc в Linux-системах). Системные вызовы по своей природе являются универсальными, но несмотря на это, механика их выполнения во многом аппаратно-зависима.

В этой статье рассматривается несколько практических примеров анализа системных вызовов с помощью strace. В примерах используется Red Hat Enterprise Linux, но все команды должны работать и в других дистрибутивах Linux:

[root@sandbox ~]# cat /etc/redhat-releaseRed Hat Enterprise Linux Server release 7.7 (Maipo)[root@sandbox ~]#[root@sandbox ~]# uname -r3.10.0-1062.el7.x86_64[root@sandbox ~]#


Для начала убедитесь, что в вашей системе установлены необходимые инструменты. Проверить установлен ли strace можно с помощью приведенной ниже команды. Для просмотра версии strace запустите ее с параметром -V:

[root@sandbox ~]# rpm -qa | grep -i stracestrace-4.12-9.el7.x86_64[root@sandbox ~]#[root@sandbox ~]# strace -Vstrace -- version 4.12[root@sandbox ~]#


Если strace не установлен, то установите запустив:

yum install strace


Для примера создайте тестовый каталог в /tmp и два файла с помощью команды touch:

[root@sandbox ~]# cd /tmp/[root@sandbox tmp]#[root@sandbox tmp]# mkdir testdir[root@sandbox tmp]#[root@sandbox tmp]# touch testdir/file1[root@sandbox tmp]# touch testdir/file2[root@sandbox tmp]#


(Я использую каталог /tmp только потому, что доступ к нему есть у всех, но вы можете использовать любой другой.)

С помощью команды ls проверьте, что в каталоге testdir создались файлы:

[root@sandbox tmp]# ls testdir/file1  file2[root@sandbox tmp]#


Вероятно, вы используете команду ls каждый день, не осознавая того, что под капотом работают системные вызовы. Здесь в игру вступает абстракция. Вот как работает эта команда:

Утилита командной строки -> Функции системных библиотек (glibc) -> Системные вызовы


Команда ls вызывает функции из системных библиотек Linux (glibc). Эти библиотеки, в свою очередь, вызывают системные вызовы, которые выполняют большую часть работы.

Если вы хотите узнать, какие функции вызывались из библиотеки glibc, то используйте команду ltrace со следующей за ней командой ls testdir/:

ltrace ls testdir/


Если ltrace не установлен, то установите:

yum install ltrace


На экране будет много информации, но не беспокойтесь мы это рассмотрим далее. Вот некоторые из важных библиотечных функций из вывода ltrace:

opendir("testdir/")                                  = { 3 }readdir({ 3 })                                       = { 101879119, "." }readdir({ 3 })                                       = { 134, ".." }readdir({ 3 })                                       = { 101879120, "file1" }strlen("file1")                                      = 5memcpy(0x1665be0, "file1\0", 6)                      = 0x1665be0readdir({ 3 })                                       = { 101879122, "file2" }strlen("file2")                                      = 5memcpy(0x166dcb0, "file2\0", 6)                      = 0x166dcb0readdir({ 3 })                                       = nilclosedir({ 3 })    


Изучив этот вывод, вы, вероятно, поймете, что происходит. Каталог с именем testdir открывается с помощью библиотечной функции opendir, после чего следуют вызовы функций readdir, читающих содержимое каталога. В конце происходит вызов функции closedir, которая закрывает каталог, открытый ранее. Пока проигнорируйте остальные функции, такие как strlen и memcpy.

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

Для просмотра системных вызовов используйте strace с командой ls testdir, как показано ниже. И вы снова получите кучу бессвязной информации:

[root@sandbox tmp]# strace ls testdir/execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0brk(NULL)                               = 0x1f12000<<< truncated strace output >>>write(1, "file1  file2\n", 13file1  file2)          = 13close(1)                                = 0munmap(0x7fd002c8d000, 4096)            = 0close(2)                                = 0exit_group(0)                           = ?+++ exited with 0 +++[root@sandbox tmp]#

В результате выполнения strace вы получите список системных вызовов, выполненных при работе команды ls. Все системные вызовы можно разделить на следующие категории:

  • Управление процессами
  • Управление файлами
  • Управление каталогами и файловой системой
  • Прочие


Есть удобный способ анализа полученной информации записать вывод в файл с помощью опции -o.

[root@sandbox tmp]# strace -o trace.log ls testdir/file1  file2[root@sandbox tmp]#


На этот раз на экране не будет никаких данных команда ls отработает, как и ожидается, показав список файлов и записав весь вывод strace в файл trace.log. Для простой команды ls файл содержит почти 100 строк:

[root@sandbox tmp]# ls -l trace.log-rw-r--r--. 1 root root 7809 Oct 12 13:52 trace.log[root@sandbox tmp]#[root@sandbox tmp]# wc -l trace.log114 trace.log[root@sandbox tmp]#


Взгляните на первую строку в файле trace.log:

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0


  • В начале строки находится имя выполняемого системного вызова это execve.
  • Текст в круглых скобках это аргументы, передаваемые системному вызову.
  • Число после знака = (в данном случае 0) это значение, возвращаемое системным вызовом.


Теперь результат не кажется слишком пугающим, не так ли? И вы можете применить ту же логику и для других строк.

Обратите внимание на ту единственную команду, которую вы вызвали ls testdir. Вам известно имя каталога, используемое командой ls, так почему бы не воспользоваться grep для testdir в файле trace.log и не посмотреть, что найдется? Посмотрите внимательно на результат:

[root@sandbox tmp]# grep testdir trace.logexecve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3[root@sandbox tmp]#


Возвращаясь к приведенному выше анализу execve, можете ли вы сказать, что делает следующий системный вызов?

execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 0


Не нужно запоминать все системные вызовы и то, что они делают: все есть в документации. Man-страницы спешат на помощь! Перед запуском команды man убедитесь, что установлен пакет man-pages:

[root@sandbox tmp]# rpm -qa | grep -i man-pagesman-pages-3.53-5.el7.noarch[root@sandbox tmp]#


Помните, что вам нужно добавить 2 между командой man и именем системного вызова. Если вы прочитаете в man про man (man man), то увидите, что раздел 2 зарезервирован для системных вызовов. Аналогично если вам нужна информация о библиотечных функциях, то нужно добавить 3 между man и именем библиотечной функции.

Ниже приведены номера разделов man:

1. Выполняемые программы или команды для командной оболочки.2. Системные вызовы (функции, предоставляемые ядром).3. Библиотечные вызовы (функции программных библиотек).4. Специальные файлы (которые обычно находятся в /dev).


Для просмотра документации по системному вызову запустите man с именем этого системного вызова.

man 2 execve


В соответствии с документацией системный вызов execve выполняет программу, которая передается ему в параметрах (в данном случае это ls). В него также передаются дополнительные параметры для ls. В этом примере это testdir. Следовательно, этот системный вызов просто запускает ls с testdir в качестве параметра:

'execve - execute program''DESCRIPTION       execve()  executes  the  program  pointed to by filename'


В следующий системный вызов stat передается параметр testdir:

stat("testdir/", {st_mode=S_IFDIR|0755, st_size=32, ...}) = 0


Для просмотра документации используйте man 2 stat. Системный вызов stat возвращает информацию об указанном файле. Помните, что все в Linux файл, включая каталоги.

Далее системный вызов openat открывает testdir. Обратите внимание, что возвращается значение 3. Это дескриптор файла, который будет использоваться в последующих системных вызовах:

openat(AT_FDCWD, "testdir/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3


Теперь откройте файл
trace.log
и обратите внимание на строку, следующую после системного вызова openat. Вы увидите системный вызов getdents, который делает большую часть необходимой работы для выполнения команды ls testdir. Теперь выполним grep getdents для файла trace.log:

[root@sandbox tmp]# grep getdents trace.loggetdents(3, /* 4 entries */, 32768)     = 112getdents(3, /* 0 entries */, 32768)     = 0[root@sandbox tmp]#


В документации (man getdents) говорится, что getdents читает записи каталога, это, собственно, нам и нужно. Обратите внимание, что аргумент для getdent равен 3 это дескриптор файла, полученный ранее от системного вызова openat.

Теперь, когда получено содержимое каталога, нужен способ отобразить информацию в терминале. Итак, делаем grep для другого системного вызова write, который используется для вывода на терминал:

[root@sandbox tmp]# grep write trace.logwrite(1, "file1  file2\n", 13)          = 13[root@sandbox tmp]#


В аргументах вы можете видеть имена файлов, которые будут выводится: file1 и file2. Что касается первого аргумента (1), вспомните, что в Linux для любого процесса по умолчанию открываются три файловых дескриптора:

  • 0 стандартный поток ввода
  • 1 стандартный поток вывода
  • 2 стандартный поток ошибок


Таким образом, системный вызов write выводит file1 и file2 на стандартный вывод, которым является терминал, обозначаемый числом 1.

Теперь вы знаете, какие системные вызовы сделали большую часть работы для команды ls testdir/. Но что насчет других 100+ системных вызовов в файле trace.log?

Операционная система выполняет много вспомогательных действий для запуска процесса, поэтому многое из того, что вы видите в файле trace.log это инициализация и очистка процесса. Посмотрите файл trace.log полностью и попытайтесь понять, что происходит во время запуска команды ls.

Теперь вы можете анализировать системные вызовы для любых программ. Утилита strace так же предоставляет множество полезных параметров командной строки, некоторые из которых описаны ниже.

По умолчанию strace отображает не всю информацию о системных вызовах. Однако у нее есть опция -v verbose, которая покажет дополнительную информацию о каждом системном вызове:

strace -v ls testdir


Хорошая практика использовать параметр -f для отслеживания дочерних процессов, созданных запущенным процессом:

strace -f ls testdir


А если вам нужны только имена системных вызовов, количество их запусков и процент времени, затраченного на выполнение? Вы можете использовать опцию -c, чтобы получить эту статистику:

strace -c ls testdir/


Если вы хотите отследить определенный системный вызов, например, open, и проигнорировать другие, то можно использовать опцию -e с именем системного вызова:

[root@sandbox tmp]# strace -e open ls testdiropen("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libcap.so.2", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libacl.so.1", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libc.so.6", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libattr.so.1", O_RDONLY|O_CLOEXEC) = 3open("/lib64/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3file1  file2+++ exited with 0 +++[root@sandbox tmp]#


А что, если нужно отфильтровать по нескольким системным вызовам? Не волнуйтесь, можно использовать ту же опцию -e и разделить необходимые системные вызовы запятой. Например, для write и getdent:

[root@sandbox tmp]# strace -e write,getdents ls testdirgetdents(3, /* 4 entries */, 32768)     = 112getdents(3, /* 0 entries */, 32768)     = 0write(1, "file1  file2\n", 13file1  file2)          = 13+++ exited with 0 +++[root@sandbox tmp]#


До сих пор мы отслеживали только явный запуск команд. Но как насчет команд, которые были запущены ранее? Что, если вы хотите отслеживать демонов? Для этого у strace есть специальная опция -p, которой вы можете передать идентификатор процесса.

Мы не будем запускать демона, а используем команду cat, которая отображает содержимое файла, переданного ему в качестве аргумента. Но если аргумент не указать, то команда cat будет просто ждать ввод от пользователя. После ввода текста она выведет введенный текст на экран. И так до тех пор, пока пользователь не нажмет Ctrl+C для выхода.

Запустите команду cat на одном терминале.

[root@sandbox tmp]# cat


На другом терминале найдите идентификатор процесса (PID) с помощью команды ps:

[root@sandbox ~]# ps -ef | grep catroot      22443  20164  0 14:19 pts/0    00:00:00 catroot      22482  20300  0 14:20 pts/1    00:00:00 grep --color=auto cat[root@sandbox ~]#


Теперь запустите strace с опцией -p и PID'ом, который вы нашли с помощью ps. После запуска strace выведет информацию о процессе, к которому он подключился, а также его PID. Теперь strace отслеживает системные вызовы, выполняемые командой cat. Первый системный вызов, который вы увидите это read, ожидающий ввода от потока с номером 0, то есть от стандартного ввода, который сейчас является терминалом, на котором запущена команда cat:

[root@sandbox ~]# strace -p 22443strace: Process 22443 attachedread(0,


Теперь вернитесь к терминалу, где вы оставили запущенную команду cat, и введите какой-нибудь текст. Для демонстрации я ввел x0x0. Обратите внимание, что cat просто повторил то, что я ввел и x0x0 на экране будет дважды.

[root@sandbox tmp]# catx0x0x0x0


Вернитесь к терминалу, где strace был подключен к процессу cat. Теперь вы видите два новых системных вызова: предыдущий read, который теперь прочитал x0x0, и еще один для записи write, который записывает x0x0 обратно в терминал, и снова новый read, который ожидает чтения с терминала. Обратите внимание, что стандартный ввод (0) и стандартный вывод (1) находятся на одном и том же терминале:

[root@sandbox ~]# strace -p 22443strace: Process 22443 attachedread(0, "x0x0\n", 65536)                = 5write(1, "x0x0\n", 5)                   = 5read(0,


Представляете, какую пользу может принести вам запуск strace для демонов: вы можете увидеть все, что делается в фоне. Завершите команду
cat
, нажав
Ctrl+C
. Это также прекратит сеанс
strace
, так как отслеживаемый процесс был прекращен.

Для просмотра отметок времени системных вызовов используйте опцию -t:

[root@sandbox ~]#strace -t ls testdir/14:24:47 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 014:24:47 brk(NULL)                      = 0x1f0700014:24:47 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2530bc800014:24:47 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)14:24:47 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3


А если вы хотите узнать время, проведенное между системными вызовами? Есть удобная опция -r, которая показывает время, затраченное на выполнение каждого системного вызова. Довольно полезно, не так ли?

[root@sandbox ~]#strace -r ls testdir/0.000000 execve("/usr/bin/ls", ["ls", "testdir/"], [/* 40 vars */]) = 00.000368 brk(NULL)                 = 0x19660000.000073 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fb6b11550000.000047 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)0.000119 open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3


Заключение


Утилита strace очень удобна для изучения системных вызовов в Linux. Чтобы узнать о других параметрах командной строки, обратитесь к man и онлайн-документации.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru