Случилась проблема в сервисе, ты засучив рукава проваливаешься в его контейнер, а там:

root@9a5c3a45d111:/# curl ...
bash: curl: command not found

или

root@9a5c3a45d111:/# ss -ntlp
bash: ss: command not found

или

root@9a5c3a45d111:/# dig ...
bash: dig: command not found

Частый кейс. И из таких ситуаций надо выкручиваться.

На примере задачи “Какие TCP порты открыты у контейнера?” посмотрим как это можно сделать.

При наличии доступа к хосту

Большинство случаев закрывает nsenter:

# man nsenter
NSENTER(1)         User Commands         NSENTER(1)
NAME
       nsenter - run program in different namespaces
...

Утилита позволяет запускать произвольные команды в разных namespaces операционной системы. Это те самые вокруг которых и строится “изоляция” контейнеров в Linux -man 7 namespaces.

Для этого:

  1. на хостовой машине найдем PID контейнера:
    # docker inspect --format='{{.State.Pid}}' 9a5c3a45d111
    1477639
    
  2. запустим nsenter в сетевом неймспейсе найденного PID:
    # nsenter -t 1477639 --net ss -ntl
    State     Recv-Q   Send-Q    Local Address:Port     Peer Address:Port
    LISTEN    0        4096         127.0.0.11:39031         0.0.0.0:*
    LISTEN    0        511                   *:8080                *:*
    LISTEN    0        511                   *:5555                *:*
    
    что можно перевести как: “Запусти команду ss -ntl в сетевом неймспейсе (--net) процесса с PID 1477639 (-t)”.

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

Так чтобы увидеть все запущенные процессы внутри контейнера, потребуется запуститься в mount и pid неймспейсах:

# nsenter -t 1477639 --pid --mount ps aux
PID   USER     TIME  COMMAND
    1 root      1h12 node /usr/local/bin/camouflage --config config.yml
   29 root      1h24 /usr/local/bin/node /usr/local/bin/camouflage --config config.yml
   ...
  135 root      0:00 ps aux

Когда доступ к хосту отсутствует

/proc

Самый надежный способ собирать информацию из /proc, а именно парсить файлы /proc/net/tcp и /proc/net/tcp6 (вывод сокращен):

# cat /proc/net/tcp
   sl  local_address rem_address   st ...
   0: 0B00007F:9877  00000000:0000 0A ...

# cat /proc/net/tcp6
   sl  local_address                        rem_address                           st ...             
   0: 00000000000000000000000000000000:1F90 00000000000000000000000000000000:0000 0A ...
   1: 00000000000000000000000000000000:15B3 00000000000000000000000000000000:0000 0A ...

Нас будут интересовать колонки local_address и st.

Так 0B00007F:9877 представляет собой <ip>:<port> в Hef формате, причем:

  • 0B00007F представлен в обратном порядке байтов (little-endian) и после перевода будет иметь вид 1.0.0.127, а читать его следует справа-налево - 127.0.0.1, ссылка на конвертер;
  • 9877 читается “по человечески” слева-направо и переводится в 39031.

А st это состояние соединения, нам интересно значение 0A, то есть TCP_LISTEN. Про состояния содинения тут.

Подробнее про файлы /proc/net/{tcp,tcp6} в документации к ядру.

Не самый удобный вариант, но вполне рабочий.

Использовать bash-магию

И заключительный на сегодня способ использовать функционал bash.

Зададим вопрос “открыт ли порт 39031 в контейнере?” и получим ответ:

# :> /dev/tcp/0.0.0.0/39031
# 

Точнее прямой ответ мы не получим, но его отсутствие и будет означать, что порт открыт;)

А “открыт ли порт 39566?”:

# :> /dev/tcp/0.0.0.0/39566
-bash: connect: Connection refused
-bash: /dev/tcp/0.0.0.0/39566: Connection refused

Ответ в данном случае более явный - порт закрыт.

Поищем про /dev/tcp в man bash:

 /dev/tcp/host/port
                     If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open the corresponding TCP socket.

А конструкция :> будет перенаправлять пустой вывод команды-заглушки : в открытый TCP сокет.

Информация по команде : так же доступна в man:

: [arguments]
              No effect; the command does nothing beyond expanding arguments and performing any specified redirections.  The return status is zero.

На основе конструкции мы можем накидать скрипт для сканирования портов:

# cat port-scan.sh
#!/bin/bash

for PORT in $(seq 1 60999); do
    if (:> /dev/tcp/0.0.0.0/$PORT) &>/dev/null; then
        echo "Port $PORT is open"
    fi
done

# ./port-scan.sh
Port 5555 is open
Port 8080 is open
Port 39031 is open

Этот метод не является идеальным, так как требует наличия bash в контейнере. Однако, чем больше у нас инструментов, тем выше вероятность, что один из них окажется подходящим.

Буду признателен, если вы поделитесь и другими способами, о которых я не упомянул.

Удачи!