Как добавить несколько IP-адресов на один сетевой интерфейс в Linux

Как добавить несколько IP-адресов на один сетевой интерфейс в Linux


Зачем мне понадобилось несколько IP на одной сетевой карте

На прошлой неделе я столкнулся с задачей. На моём домашнем сервере крутятся NFS и DLNA-сервер для фотографий на 192.168.90.106. Я хотел добавить CoreDNS, но при этом изолировать его от остальных сервисов. Разные правила фаервола, более чистые логи, проще разбираться с проблемами.

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

Что происходит, когда вы добавляете второй IP-адрес

IP-алиасинг назначает несколько адресов одному интерфейсу. Ядро рассматривает каждый адрес как отдельную конечную точку. Приложения могут привязываться к конкретным IP. Фаервол применяет разные правила для каждого адреса.

Я разбирался, как это работает, на своём Ubuntu-сервере с интерфейсом enp2s0. Основной IP - 192.168.90.106. В качестве вторичного я добавил 192.168.90.152.

Как добавить IP-алиас в Linux

Команда простая:

sudo ip addr add 192.168.90.152/24 dev enp2s0

Я проверил, что всё сработало:

ip addr show enp2s0

Вот что я увидел:

2: enp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:16:96:ed:75:a2 brd ff:ff:ff:ff:ff:ff
    inet 192.168.90.106/24 brd 192.168.90.255 scope global enp2s0
       valid_lft forever preferred_lft forever
    inet 192.168.90.152/24 brd 192.168.90.255 scope global secondary enp2s0
       valid_lft forever preferred_lft forever

Адрес .152 помечен как secondary. Это важно для исходящих соединений. По умолчанию ядро использует основной адрес в качестве source IP.

Что Linux-ядро делает за кулисами

Мне стало интересно, что на самом деле происходит в ядре, когда я добавляю этот IP. Оказалось, что ядро обновляет сразу несколько таблиц маршрутизации.

Посмотрим локальную таблицу маршрутов:

ip route show table local | grep 192.168.90

Я увидел вот это:

local 192.168.90.106 dev enp2s0 proto kernel scope host src 192.168.90.106 
local 192.168.90.152 dev enp2s0 proto kernel scope host src 192.168.90.106 
broadcast 192.168.90.255 dev enp2s0 proto kernel scope link src 192.168.90.106

Оба IP получили маршруты с типом local. Это говорит ядру, что эти адреса принадлежат данной машине. Пакеты, адресованные на эти IP, доставляются локальным приложениям, а не пробрасываются дальше.

Обратите внимание: в обоих случаях в качестве source используется 192.168.90.106. Это основной адрес, который ядро подставляет для ответов.

Внутри FIB (Forwarding Information Base) ядра Linux

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

FIB использует сжатую древовидную структуру, которая называется trie. Благодаря этому поиск работает очень быстро - даже если маршрутов тысячи.

Я посмотрел, что именно ядро там построило:

cat /proc/net/fib_trie | grep -A 5 "192.168.90"

Вывод был таким:

+-- 192.168.90.0/24 3 0 4
        |-- 192.168.90.0
           /24 link UNICAST
        |-- 192.168.90.106
           /32 host LOCAL
        |-- 192.168.90.152
           /32 host LOCAL
        |-- 192.168.90.255
           /32 link BROADCAST

Я понял, что FIB организует маршруты по длине префикса. Вот что означает каждая запись:

192.168.90.0/24 - это маршрут сети. Он обрабатывает весь трафик подсети 192.168.90.x. Тип link UNICAST означает, что сеть напрямую подключена к интерфейсу.

192.168.90.106/32 и 192.168.90.152/32 - это маршруты хоста. Маска /32 означает точное совпадение для одного IP-адреса. Ключевой момент - тип LOCAL. Он говорит ядру: «этот IP мой, доставляй пакеты локальным процессам».

192.168.90.255/32 - широковещательный адрес. Тип link BROADCAST означает, что пакеты на этот адрес уходят всем хостам в локальной сети.

Именно trie-структура объясняет, почему маршрутизация такая быстрая. Когда пакет приходит на 192.168.90.152, ядро за наносекунды спускается от сетевого маршрута /24 к хост-маршруту /32.

Проверка вторичного IP-адреса

Я проверил алиас обычным ping:

ping -c 3 192.168.90.152

Всё заработало сразу. Ядро маршрутизировало эти пакеты внутри системы, вообще не трогая сеть.

Затем я посмотрел, как именно ядро прокладывает маршрут к этому алиасу:

ip route get 192.168.90.152

Вывод был таким:

local 192.168.90.152 dev lo src 192.168.90.106 uid 0

Сначала это меня удивило. В маршруте указан интерфейс lo (loopback), хотя адрес принадлежит enp2s0. Оказалось, это оптимизация. Когда ядро доставляет пакеты на собственные адреса, оно использует loopback-интерфейс, чтобы избежать лишней обработки в сетевом стеке.

Как привязать сервисы к конкретным IP-адресам

Теперь к практике. Мне нужно было, чтобы разные сервисы работали на разных IP.

Моя схема:

  • NFS-сервер на 192.168.90.106
  • DLNA-сервер с фотографиями на 192.168.90.106
  • CoreDNS на 192.168.90.152

Для проверки я использовал простой HTTP-сервер из Python:

python3 -m http.server 8080 --bind 192.168.90.152

С другой машины в сети:

curl http://192.168.90.152:8080

Соединение повисло по таймауту. Сервис запущен, но не отвечает.

Я проверил правила фаервола:

iptables -L INPUT -n -v | head -n 2
Chain INPUT (policy DROP 2532 packets, 153K bytes)

Политика INPUT - DROP. По умолчанию блокируется всё. Я посмотрел, какие порты вообще разрешены:

iptables -L ufw-user-input -n -v

В выводе были правила для SSH, NFS, DLNA и Samba. Но для порта 8080 - ничего. Тестовый сервер просто блокировался. Я добавил правило фаервола специально для IP-алиаса:

ufw allow from 192.168.90.0/24 to 192.168.90.152 port 8080 proto tcp comment 'Test server'

Проверил, что правило появилось:

iptables -L ufw-user-input -n -v | grep 8080
0     0 ACCEPT     6    --  *      *       192.168.90.0/24      192.168.90.152       tcp dpt:8080

Правило активно. Обратите внимание: в качестве назначения указан именно 192.168.90.152, а не любой адрес. Я попробовал ещё раз:

curl http://192.168.90.152:8080

Заработало. Потом я проверил основной IP:

curl http://192.168.90.106:8080

Connection refused. Идеально. Сервис слушает только .152. Именно такую изоляцию я и хотел.

Чтобы посмотреть, какие сервисы на каких IP слушают порты, я использую:

ss -tlnp | grep 192.168.90

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

Как выбирается source IP для исходящих соединений

Я узнал важную вещь про исходящие соединения. Когда сервер сам инициирует подключение - какой IP он использует?

ip route get 8.8.8.8

В выводе видно src 192.168.90.106. По умолчанию ядро выбирает основной адрес.

Но при необходимости можно принудительно указать source IP:

curl --interface 192.168.90.152 http://httpbin.org/ip

В этом случае соединение пойдёт с адреса 192.168.90.152. Полезно для тестов или в ситуациях, где нужна строгая маршрутизация.

Как протокол ARP обрабатывает несколько IP на одном интерфейсе

Я решил посмотреть, что происходит на сетевом уровне. Оба IP используют один и тот же физический MAC-адрес.

С другой машины:

arp -a | grep 192.168.90

Вывод:

192.168.90.106 at 00:16:96:ed:75:a2
192.168.90.152 at 00:16:96:ed:75:a2

Один и тот же MAC для обоих IP. Сетевому коммутатору вообще нет дела до IP-адресов - он пересылает Ethernet-кадры на основе MAC. А уже ядро на принимающей стороне смотрит на IP назначения и решает, что делать с пакетом.

Параметры ядра Linux, которые влияют на работу с несколькими IP

Я нашёл два параметра ядра, которые имеют значение при такой конфигурации.

Первый - поведение ARP-анонсов:

cat /proc/sys/net/ipv4/conf/enp2s0/arp_announce

У меня было: 0

Это означает, что ядро может анонсировать любой локальный IP в ARP-запросах. Значение 2 ограничивает анонсы только теми IP, которые находятся в той же подсети, что и цель. Для моей простой схемы 0 вполне подходит.

Второй параметр - reverse path filtering:

cat /proc/sys/net/ipv4/conf/enp2s0/rp_filter

У меня было: 2

Это так называемый loose-режим. Ядро принимает пакеты, если для source IP существует любой валидный обратный маршрут - даже через другой интерфейс.

Значение 1 - строгий режим (адрес источника должен быть достижим именно через интерфейс, на который пришёл пакет).

Значение 0 полностью отключает проверку.

Я оставил 2. Это позволяет асимметричную маршрутизацию, когда пакеты приходят и уходят разными путями. Для большинства домашних сетей такая гибкость необходима.

Отладка, если сервисы не отвечают на алиас-IP

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

Во-первых, убедитесь, что сервис вообще слушает нужный IP:

ss -tlnp | grep 192.168.90.152

Если команда ничего не выводит - сервис не привязался к этому адресу. Проверяйте конфигурацию сервиса.

Во-вторых, проверьте, что маршрут существует:

ip route get 192.168.90.152

Должно вернуться что-то вроде local 192.168.90.152 dev lo. Если нет - IP добавлен некорректно.

В-третьих, протестируйте с самого сервера:

curl http://192.168.90.152:53

Если локально всё работает, а с других машин - нет, значит проблема в фаерволе. Он блокирует этот IP или порт.

Как сделать IP-алиасы постоянными после перезагрузки

Команда ip не переживает перезагрузку. Я понял это на практике, когда перезапустил сервер.

В Ubuntu для настройки сети используется netplan. Нужно отредактировать конфиг:

sudo nano /etc/netplan/00-installer-config.yaml

Добавляем оба адреса:

network:
  version: 2
  ethernets:
    enp2s0:
      addresses:
        - 192.168.90.106/24
        - 192.168.90.152/24
      routes:
        - to: default
          via: 192.168.90.1
      nameservers:
        addresses: [8.8.8.8]

Применяем настройки:

sudo netplan apply

Теперь оба IP поднимаются автоматически при загрузке системы.

Как удалить IP-алиас

Удаление тоже простое:

sudo ip addr del 192.168.90.152/24 dev enp2s0

Проверяем, что адрес исчез:

ip addr show enp2s0

Ядро автоматически убирает локальный маршрут и запись в FIB. Все активные соединения, использующие этот IP, сразу обрываются.

Практическая польза IP-алиасинга

После недели использования такой схемы я отметил несколько плюсов:

Раздельные правила фаервола для сервисов.

DNS-запросы приходят только на 192.168.90.152. NFS и DLNA остаются на 106. Можно разрешать и блокировать трафик по IP, а не жонглировать портами.

Более чистые логи.

В логах соединений сразу видно IP назначения - и сразу понятно, к какому сервису обращались. Не нужно вспоминать, какой порт за что отвечает.

Простая миграция сервисов.

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

Один сетевой кабель - несколько изолированных сервисов.

Я получаю разделение на сетевом уровне без добавления железа. Один порт на свиче обслуживает всё.

Этот подход отлично подходит для homelab-серверов, dev-окружений и любых ситуаций, где нужна изоляция сервисов без сложной сетевой магии. Ядро обрабатывает всё прозрачно. Приложения видят обычные IP-адреса. Сеть - один интерфейс.

Report Page