BashTex | Linux
2.51K subscribers
77 photos
13 videos
496 links
Авторский канал для тех, кто хочет глубже погрузиться в мир Linux.

Подойдет для разработчиков, системных администраторов и DevOps

Реклама: @dad_admin
Download Telegram
Почему Permission denied: как найти каталог, на котором ломаются права

Иногда файл имеет правильные права, пользователь состоит в нужной группе, но Permission denied всё равно появляется.

Причина может быть выше по пути: чтобы открыть /var/www/site/config.php, процесс должен иметь право x на каждый каталог в этом пути.

Разберем, как быстро найти место, где ломается доступ.

1️⃣namei показывает права на весь путь

namei -l /var/www/site/config.php


В выводе будут отдельно показаны:

drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-x--- root www-data www
drwx------ root root site
-rw-r----- root www-data config.php


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

2️⃣Почему важен x у каталога

Для каталога x означает не «запуск», а возможность проходить через него и обращаться к объектам внутри.
Например:

chmod 644 /var/www/site


Файлы внутри могут быть доступны для чтения, но войти в каталог без x уже нельзя.
Проверить можно:

sudo -u www-data cat /var/www/site/config.php


3️⃣Проверяем конкретного пользователя
Если доступ должен быть у www-data:

sudo -u www-data namei -l /var/www/site/config.php


А группы:

id www-data


Это помогает понять, действительно ли процесс запускается с теми группами, которые вы ожидаете.

4️⃣ACL тоже могут менять картину

Обычных ls -l иногда недостаточно. Проверить ACL:

getfacl /var/www/site/config.php


Там могут быть дополнительные разрешения для конкретного пользователя или группы.

5️⃣Почему ls -l файл не всегда помогает
Команда:

ls -l /var/www/site/config.php


показывает права самого файла.

Но если проблема находится в /var/www/site, информация о файле сама по себе этого не объяснит.

Именно поэтому namei -l удобен при странных Permission denied: он показывает всю цепочку каталогов, через которую процесс должен пройти до нужного файла.

BashTex 📱 #bash #systemd
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
Please open Telegram to view this post
VIEW IN TELEGRAM
😁7
Архитектура большого bash-скрипта

Пока скрипт на 30 строк - все терпимо. На 300 строк начинается хаос. На 1000 - уже никто не понимает, где init, где логика, где cleanup. Чтобы bash не превратился в лапшу, ему нужна архитектура.

📂 Нормальная структура проекта


project/
├── main.sh
├── lib/
│ ├── log.sh
│ ├── config.sh
│ ├── checks.sh
│ └── deploy.sh
├── conf/
│ └── app.conf
└── tmp/


Где:

main.sh - точка входа
lib/ - функции по темам
conf/ - конфиги
tmp/ - временные файлы


▪️ Деление на модули

Не надо держать все в одном файле.
Лучше так:


source "$(dirname "$0")/lib/log.sh"
source "$(dirname "$0")/lib/checks.sh"


Примеры модулей:

log.sh - логгер
config.sh - загрузка переменных
checks.sh - проверки окружения
actions.sh - основная логика


▪️ Naming: единый стиль

Худший вариант:


doStuff()
x()
RunAll()


Лучше:


log_info()
check_dependencies()
deploy_app()
cleanup_tmp()


Для приватных функций можно префикс:


_internal_parse_config()


▪️ Поток исполнения. В начале файла:


main() {
load_config
check_dependencies
run_tasks
}

main "$@"


Это делает скрипт читаемым как программу, а не как свалку команд.

BashTex 📱 #bash #scripts
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
tee + process substitution: один поток и несколько получателей

Иногда нужно одновременно: увидеть вывод команды в терминале, записать его в файл и передать на обработку другой программе Если делать это по очереди - потеряется поток данных. Здесь помогает связка tee + process substitution.

▪️ Что делает tee. tee дублирует поток:


command | tee file.log


вывод остается в терминале и одновременно пишется в file.log

▪️ Несколько получателей. tee может писать сразу в несколько файлов:


command | tee out1.log out2.log


Но иногда нужно не просто файл, а другую команду.

▪️ Process substitution. Bash позволяет подставить вывод команды как файл:


>(command)

Это называется process substitution.

▪️ Комбинируем


command | tee >(grep ERROR > errors.log)


command генерирует поток
tee дублирует его

Одна копия идет в терминал, а другая в grep. Если найден ERROR, запись попадет в errors.log.

▪️ Более реальный пример. Допустим, идет сбор логов:


journalctl -f | tee >(grep ERROR >> errors.log)


Теперь: полный поток остается в терминале, ошибки автоматически сохраняются

▪️ Можно делать несколько обработчиков


journalctl -f | tee \
>(grep ERROR >> errors.log) \
>(grep WARN >> warn.log)


Один поток и сразу несколько фильтров.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍2
DEBUG trap - как Bash выполняет код перед каждой командой

В Bash есть специальный DEBUG trap, который позволяет выполнить собственный код непосредственно перед выполнением команды.
Это удобно не только для отладки. Через него можно посмотреть, какие команды реально выполняет скрипт, с какими аргументами и в каком контексте.

▪️Самый простой пример

trap 'echo "CMD: $BASH_COMMAND"' DEBUG

echo "hello"
mkdir /tmp/test
rm -rf /tmp/test


Перед каждой командой Bash вызовет trap:

CMD: echo "hello"
CMD: mkdir /tmp/test
CMD: rm -rf /tmp/test


$BASH_COMMAND содержит команду, которую Bash собирается выполнить.

▪️Можно добавить PID и функцию

trap 'printf "[%s] %s: %s\n" "$$" "${FUNCNAME[1]:-main}" "$BASH_COMMAND"' DEBUG


Теперь при выполнении функций можно получить что-то вроде:

[4217] main: prepare
[4217] deploy: mkdir -p /srv/app
[4217] deploy: cp app.conf /srv/app/


Это уже превращается в простой трассировщик Bash-скрипта.

▪️Но DEBUG не означает буквально «перед каждой строкой»

Trap срабатывает перед выполнением простых команд, for, case, некоторых условных конструкций и других элементов shell execution.
Поведение также зависит от того, включено ли наследование trap внутри функций и subshell.
Например:

trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

foo() {
echo "inside"
}

foo


Чтобы DEBUG распространялся внутрь функций, часто используют:

set -T


или:

shopt -s functrace


▪️У trap есть интересный побочный эффект
Сам код внутри DEBUG тоже выполняется Bash, поэтому слишком сложный обработчик может сам стать источником неожиданных эффектов.
Для диагностики лучше держать его простым:

trap 'printf "%s\n" "$BASH_COMMAND" >> /tmp/bash-debug.log' DEBUG


А после диагностики обязательно убрать:

trap - DEBUG


▪️Практический сценарий

Есть большой deployment-скрипт, который запускает десятки функций и команд. Лог показывает только:

deployment failed


Вместо того чтобы расставлять echo по всему скрипту, временно включаем:

trap 'printf "[%s] %s\n" "$SECONDS" "$BASH_COMMAND"' DEBUG


и получаем последовательность реально выполнявшихся команд.

Важно: DEBUG предназначен прежде всего для трассировки. Для полноценного production-аудита он неудобен: trap может влиять на поведение скрипта и генерировать очень много вывода.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
flock vs fcntl - почему две блокировки одного файла могут вести себя совершенно по-разному

В Linux есть несколько механизмов блокировки файлов. Наиболее часто встречаются flock() и fcntl().

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

▪️flock блокирует сам файл

Простейший вариант:

flock /tmp/app.lock -c 'echo "working"; sleep 10'


Пока первый процесс держит блокировку, второй:

flock -n /tmp/app.lock -c 'echo "working"'


получит ошибку и сразу завершится из-за -n.
Часто этот механизм используют для защиты cron-задач:

flock -n /run/myjob.lock /usr/local/bin/myjob


▪️fcntl работает через диапазоны файла
Здесь можно блокировать не только весь файл, но и конкретный диапазон байтов.

На уровне C:

struct flock lock = {
.l_type = F_WRLCK,
.l_whence = SEEK_SET,
.l_start = 0,
.l_len = 100
};

fcntl(fd, F_SETLK, &lock);


Можно заблокировать первые 100 байт, а другой процесс при этом сможет работать с другим диапазоном.

▪️Главная ловушка

Процесс A:

flock /tmp/test.lock


Процесс B использует fcntl() для того же файла.

Они не обязаны конфликтовать.

Причина в том, что flock и традиционные POSIX advisory locks через fcntl исторически используют разные пространства блокировок.
То есть наличие:

flock → locked


не означает автоматически:

fcntl → blocked


И наоборот.

▪️Обе блокировки advisory

Ни flock, ни fcntl сами по себе не запрещают процессу открыть и изменить файл.

Если приложение вообще не проверяет блокировку, оно может спокойно сделать:

echo "data" >> /tmp/file


Поэтому механизм работает только тогда, когда все участники системы договариваются его соблюдать.

▪️Есть ещё одна важная разница

Для flock блокировка связана с открытым file description. При fork() дочерний процесс наследует соответствующую блокировку.

У fcntl семантика другая: POSIX locks связаны с процессом и имеют собственные правила поведения при close().

Из-за этого без понимания модели владения FD легко получить ситуацию, когда один close() неожиданно освобождает блокировку.

▪️Практический вывод
Если нужно просто гарантировать, что cron или несколько экземпляров shell-скрипта не выполняются одновременно:

flock -n /run/myjob.lock /usr/local/bin/myjob


обычно достаточно.

Если приложение должно блокировать отдельные диапазоны файла или ему нужна POSIX-модель блокировок, используют fcntl().

И главное: нельзя считать flock и fcntl взаимозаменяемыми только потому, что оба называются file locking.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
madvise() - как процесс подсказывает ядру, как он собирается использовать память

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

Linux предоставляет madvise() - системный вызов, через который процесс может сообщить ядру предполагаемый характер использования памяти.

Это особенно интересно для mmap() и больших memory-mapped файлов.

▪️Последовательный доступ

Если программа собирается читать память последовательно:

madvise(addr, length, MADV_SEQUENTIAL);


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

Это позволяет иначе управлять page cache и упреждающим чтением.
Например, обработчик большого файла может пройти по нему от начала до конца вместо случайных обращений.

▪️Случайный доступ

Для случайного доступа есть:

madvise(addr, length, MADV_RANDOM);


Если приложение читает страницы в произвольном порядке, агрессивное read-ahead может оказаться бесполезным.
Подсказка позволяет ядру учитывать такой сценарий.

▪️Можно сообщить, что страницы больше не нужны

Особенно интересен:

madvise(addr, length, MADV_DONTNEED);


Процесс говорит ядру, что содержимое этой области ему сейчас не требуется.
Для анонимной памяти это может позволить освободить физические страницы. Для файлового отображения поведение связано с отображёнными страницами и page cache.
Важно: это не то же самое, что free().

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

▪️Есть и подсказка WILLNEED

madvise(addr, length, MADV_WILLNEED);


Она сообщает ядру, что страницы, вероятно, скоро понадобятся.

Это может помочь подготовить данные заранее, например перед обработкой большого memory-mapped файла.
Но madvise() именно подсказывает, а не заставляет ядро выполнить конкретную стратегию.

▪️Практический сценарий
Представим программу, которая через mmap() обрабатывает несколько гигабайт файла строго последовательно.

Вместо случайного поведения с page cache она может сделать:

void *p = mmap(NULL, size,
PROT_READ,
MAP_PRIVATE,
fd, 0);

madvise(p, size, MADV_SEQUENTIAL);


Теперь приложение явно сообщает ядру характер будущего доступа.

А когда большой участок больше не нужен:

madvise(p, size, MADV_DONTNEED);


Это может уменьшить давление на память, не требуя немедленно уничтожать само отображение.

▪️Важный нюанс
madvise() не является универсальной кнопкой «ускорить память».
Эффект зависит от конкретного флага, типа mapping, версии ядра и сценария доступа.

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

То есть madvise() - это интерфейс, через который userspace сообщает kernel memory manager: «я примерно знаю, что собираюсь делать с этой памятью».

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
name_to_handle_at() - как получить файловый объект без обычного пути

В Linux обычно обращаются к файлу через путь:

/etc/hosts


Но ядро умеет работать с файловым объектом иначе. Системный вызов name_to_handle_at() позволяет получить специальный file handle, связанный с объектом filesystem.

Это используется низкоуровневыми инструментами, которым нужно сохранить ссылку на объект, а не его текстовый путь.

▪️Получаем handle

Сам системный вызов выглядит примерно так:

int name_to_handle_at(
int dirfd,
const char *path,
struct file_handle *handle,
int *mount_id,
int flags
);


Например, программа может передать:

/etc/hosts


и получить структуру с handle и идентификатором mount.

Важно: это не файловый дескриптор. Handle не позволяет просто сделать read().

▪️Зачем тогда он нужен

Основная идея - получить устойчивое представление объекта внутри filesystem, которое затем можно использовать для повторного обращения к нему.

Для этого существует парный системный вызов:

open_by_handle_at()


Схема получается такой:

path
↓
name_to_handle_at()
↓
file handle + mount ID
↓
open_by_handle_at()
↓
file descriptor


То есть сначала filesystem выдаёт идентификатор объекта, а позже по нему можно снова получить FD.

▪️Почему это отличается от обычного пути

Представим:

/var/data/report


Путь зависит от directory entries. Файл могут переименовать:

mv /var/data/report /var/archive/report


Сам inode при этом остаётся тем же объектом.
File handle предназначен именно для работы с объектом filesystem, а не с конкретным именем в каталоге.

▪️Но есть важное ограничение

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

Кроме того, open_by_handle_at() требует соответствующих привилегий. Обычное приложение не получает возможность произвольно открывать объекты по filesystem handles.

▪️Где это реально встречается

Механизм особенно интересен для низкоуровневых инструментов резервного копирования, мониторинга и работы с filesystem, где обычный путь может быть неудобен или недостаточно надёжен.

И главное: name_to_handle_at() не «обходит filesystem по inode».

Он просит конкретную filesystem предоставить handle для объекта, а затем этот handle может быть использован через соответствующий kernel API.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Ephemeral ports - почему заканчиваются исходящие TCP-порты

Когда приложение устанавливает исходящее TCP-соединение, порт выбирается автоматически. Это ephemeral port, временный исходный порт клиента.

Например:

10.0.0.10:49152 → 142.250.74.14:443


Если приложение создаёт тысячи соединений, диапазон таких портов может стать узким местом.

▪️Посмотреть доступный диапазон

В Linux его задаёт:

cat /proc/sys/net/ipv4/ip_local_port_range


Например:

32768 60999


Это около 28 тысяч портов для исходящих соединений с одного локального IP.

▪️Но количество соединений не всегда ограничено этим числом

TCP-соединение определяется не одним портом, а набором:

source IP
source port
destination IP
destination port
protocol


То есть:

10.0.0.10:50000 → 1.1.1.1:443
10.0.0.10:50001 → 1.1.1.1:443


это разные соединения.

Но тот же локальный порт теоретически может использоваться снова, если меняется destination tuple.

Поэтому несколько destination IP/портов значительно расширяют пространство комбинаций.

▪️Как увидеть исходящие соединения

ss -tan state established


А чтобы посмотреть локальные порты:

ss -tan | awk 'NR>1 {print $4}' | sort | uniq -c | sort -nr | head


Если приложение создаёт огромное количество короткоживущих TCP-соединений, дополнительно стоит посмотреть TIME-WAIT:

ss -tan state time-wait | wc -l


▪️Почему TIME_WAIT особенно неприятен

После закрытия TCP-соединения локальный порт не всегда сразу можно использовать для нового соединения.
Это нужно для защиты TCP от старых сегментов предыдущего соединения.
При большом количестве коротких запросов можно получить ситуацию:

requests
↓
тысячи TCP connections
↓
close()
↓
TIME_WAIT
↓
исчерпание доступных комбинаций


В итоге новое connect() начинает получать ошибки вроде:

Cannot assign requested address


▪️Практический сценарий

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

Поэтому сначала стоит проверить:

ss -s
cat /proc/sys/net/ipv4/ip_local_port_range
ss -tan state time-wait | wc -l


А уже потом решать проблему: расширять диапазон, использовать connection pooling или уменьшать количество коротких TCP-соединений.

Важно: увеличение ip_local_port_range не создаёт новые IP-адреса и не отменяет остальные ограничения TCP.

BashTex 📱 #bash #utils
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
/dev/tcp - сетевое соединение средствами Bash

В Bash есть малоизвестная возможность работать с TCP через псевдоустройства /dev/tcp.

Никакого nc, telnet или отдельного сетевого клиента не требуется.

▪️Проверить, доступен ли TCP-порт

if (echo > /dev/tcp/127.0.0.1/5432) 2>/dev/null; then
echo "порт открыт"
else
echo "соединение не установлено"
fi


Bash сам пытается выполнить TCP connect() к указанному адресу.

▪️Можно получить обычный файловый дескриптор

exec 3<>/dev/tcp/example.com/80


Теперь FD 3 связан с TCP-соединением:

printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3

cat <&3
exec 3>&-


То есть сетевой сокет можно использовать почти как файл.

▪️Почему это удобно в скриптах

Например, простая проверка нескольких сервисов:

for port in 22 80 443 8080; do
if (echo > /dev/tcp/127.0.0.1/$port) 2>/dev/null; then
echo "$port: open"
else
echo "$port: closed"
fi
done


Это удобно для минимальных recovery/health-check скриптов, когда не хочется зависеть от nc.

▪️Но есть важный нюанс

/dev/tcp - не настоящий файл устройства. Bash распознаёт специальный путь и превращает обращение к нему в сетевой системный вызов.
Поэтому:

ls -l /dev/tcp


не покажет какой-то TCP-интерфейс.

И работает это именно как возможность Bash, а не универсальная функция любого /bin/sh.

Например, в Bash:

bash -c 'echo > /dev/tcp/127.0.0.1/22'


а поведение другого shell может быть совершенно другим.


BashTex 📱 #bash #TCP
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Bridge FDB - как Linux bridge узнаёт, где находится MAC

Linux bridge не знает заранее, за каким интерфейсом находится конкретный MAC-адрес.

Он изучает это динамически, наблюдая за входящими кадрами.

Для этого используется FDB - Forwarding Database.

▪️Сначала bridge учится

Допустим:

eth0 ── PC-A
eth1 ── PC-B


PC-A отправляет кадр с:

src MAC = aa:aa:aa:aa:aa:aa


Bridge видит, что этот MAC пришёл через eth0, и запоминает:

aa:aa:aa:aa:aa:aa → eth0

Посмотреть таблицу:

bridge fdb show

▪️Что происходит с первым кадром

Если bridge ещё не знает MAC назначения:

PC-A → bridge → неизвестный MAC


он не может выбрать конкретный порт.

Поэтому кадр отправляется через все подходящие порты, кроме того, откуда он пришёл.
Это unknown unicast flooding.

Если MAC назначения уже есть в FDB:

aa:aa:aa:aa:aa:aa → eth1

bridge отправляет кадр только через eth1.
Получается:

unknown MAC
↓
flooding
↓
bridge learns MAC
↓
known MAC
↓
unicast

▪️Запись не вечная

FDB содержит динамические записи, и они имеют lifetime.
Посмотреть подробнее:

bridge -d fdb show

Удалить конкретную запись можно, например:

bridge fdb del aa:aa:aa:aa:aa:aa dev eth0

После этого bridge снова должен будет изучить расположение MAC.

▪️Почему это важно

Если FDB постоянно заполняется, очищается или содержит неожиданные MAC, это уже может быть полезным диагностическим сигналом.

Например:

bridge fdb show br br0


может показать, какой MAC bridge сейчас связывает с каждым портом.

А если один MAC начинает «прыгать» между интерфейсами, появляется MAC flapping. Такое бывает при петлях, неправильной коммутации или некоторых схемах виртуализации.

FDB - одна из причин, почему обычный Ethernet-коммутатор не рассылает каждый кадр на все порты: после обучения он знает, куда отправить unicast напрямую.

BashTex 📱 #bash #linux
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6