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

Networking

Ядерный шелл поверх ICMP

28.08.2020 14:05:29 | Автор: admin


TL;DR: пишу модуль ядра, который будет читать команды из пейлоада ICMP и выполнять их на сервере даже в том случае, если у вас упал SSH. Для самых нетерпеливых весь код на github.

Осторожно! Опытные программисты на C рискуют разрыдаться кровавыми слезами! Я могу ошибаться даже в терминологии, но любая критика приветствуются. Пост рассчитан на тех, кто имеет самое приблизительное представление о программировании на C и хочет заглянуть во внутренности Linux.

В комментариях к моей первой статье упомянули SoftEther VPN, который умеет мимикрировать под некоторые обычные протоколы, в частности, HTTPS, ICMP и даже DNS. Я представляю себе работу только первого из них, так как хорошо знаком с HTTP(S), а туннелирование поверх ICMP и DNS пришлось изучать.

image

Да, я в 2020 году узнал, что в ICMP-пакеты можно вставить произвольный пейлоад. Но лучше поздно, чем никогда! И раз уж с этим можно что-то сделать, значит нужно делать. Так как в своей повседневности чаще всего я пользуюсь командной строкой, в том числе через SSH, идея ICMP-шелла пришла мне в голову первым делом. А чтобы собрать полное буллщит-бинго, решил писать в виде модуля Linux на языке, о котором я имею лишь приблизительное представление. Такой шелл не будет виден в списке процессов, можно загрузить его в ядро и он не будет лежать на файловой системе, вы не увидите ничего подозрительного в списке прослушиваемых портов. По своим возможностям это полноценный руткит, но я надеюсь доработать и использовать его в качестве шелла последней надежды, когда Load Average слишком высокий для того, чтобы зайти по SSH и выполнить хотя бы echo i > /proc/sysrq-trigger, чтобы восстановить доступ без перезагрузки.

Берём текстовый редактор, базовые скиллы программирования на Python и C, гугл и которую не жалко пустить под нож если всё поломается (опционально локальный VirtualBox/KVM/etc) и погнали!

Клиентская часть


Мне казалось, что для клиентской части придётся писать скрипт строк эдак на 80, но нашлись добрые люди, которые сделали за меня . Код оказался неожиданно простым, умещается в 10 значащих строк:

import sysfrom scapy.all import sr1, IP, ICMPif len(sys.argv) < 3:    print('Usage: {} IP "command"'.format(sys.argv[0]))    exit(0)p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))if p:    p.show()

Скрипт принимает два аргумента, адресом и пейлоад. Перед отправкой пейлоад предваряется ключём run:, он нам понадобится чтобы исключить пакеты со случайным пейлоадом.

Ядро требует привилегий для того чтобы крафтить пакеты, поэтому скрипт придётся запускать с правами суперпользователя. Не забудьте дать права на выполнение и установить сам scapy. В Debian есть пакет, называется python3-scapy. Теперь можно проверять, как это всё работает.

Запуск и вывод команды
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
\options \
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!


Так это выглядит в сниффере
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1]
[Response time: 19.094 ms]
Data (17 bytes)

0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]

^C2 packets captured


Пейлоад в пакете с ответом не меняется.

Модуль ядра


Для сборки в виртуалке с Debian понадобятся как минимум make и linux-headers-amd64, остальное подтянется в виде зависимостей. В статье код целиком приводить не буду, вы его можете склонировать на гитхабе.

Настройка хука


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

#include <linux/module.h>#include <linux/netfilter_ipv4.h>static struct nf_hook_ops nfho;static int __init startup(void){  nfho.hook = icmp_cmd_executor;  nfho.hooknum = NF_INET_PRE_ROUTING;  nfho.pf = PF_INET;  nfho.priority = NF_IP_PRI_FIRST;  nf_register_net_hook(&init_net, &nfho);  return 0;}static void __exit cleanup(void){  nf_unregister_net_hook(&init_net, &nfho);}MODULE_LICENSE("GPL");module_init(startup);module_exit(cleanup);

Что здесь происходит:

  1. Подтягиваются два заголовочных файла для манипуляций собственно с модулем и с нетфильтром.
  2. Все операции проходят через нетфильтр, в нём можно задавать хуки. Для этого нужно заявить структуру, в которой хук будет настраиваться. Самое важное указать функцию, которая будет выполняться в качестве хука: nfho.hook = icmp_cmd_executor; до самой функции я ещё доберусь.
    Затем я задал момент обработки пакета: NF_INET_PRE_ROUTING указывает обрабатывать пакет, когда он только появился в ядре. Можно использовать NF_INET_POST_ROUTING для обработки пакета на выходе из ядра.
    Вешаю фильтр на IPv4: nfho.pf = PF_INET;.
    Назначаю своему хуку наивысшей приоритет: nfho.priority = NF_IP_PRI_FIRST;
    И регистрирую структуру данных как собсвенно хук: nf_register_net_hook(&init_net, &nfho);
  3. В завершающей функции хук удаляется.
  4. Лицензия обозначена явно чтобы компилятор не ругался.
  5. Функции module_init() и module_exit() задают другие функции в качестве инициализирующей и завершающей работу модуля.

Извлечение пейлоада


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

#include <linux/ip.h>#include <linux/icmp.h>#define MAX_CMD_LEN 1976char cmd_string[MAX_CMD_LEN];struct work_struct my_work;DECLARE_WORK(my_work, work_handler);static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){  struct iphdr *iph;  struct icmphdr *icmph;  unsigned char *user_data;  unsigned char *tail;  unsigned char *i;  int j = 0;  iph = ip_hdr(skb);  icmph = icmp_hdr(skb);  if (iph->protocol != IPPROTO_ICMP) {    return NF_ACCEPT;  }  if (icmph->type != ICMP_ECHO) {    return NF_ACCEPT;  }  user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));  tail = skb_tail_pointer(skb);  j = 0;  for (i = user_data; i != tail; ++i) {    char c = *(char *)i;    cmd_string[j] = c;    j++;    if (c == '\0')      break;    if (j == MAX_CMD_LEN) {      cmd_string[j] = '\0';      break;    }  }  if (strncmp(cmd_string, "run:", 4) != 0) {    return NF_ACCEPT;  } else {    for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {      cmd_string[j] = cmd_string[j+4];      if (cmd_string[j] == '\0')break;    }  }  schedule_work(&my_work);  return NF_ACCEPT;}

Что происходит:

  1. Пришлось подключить дополнительные заголовочные файлы, на этот раз для манипуляция с IP- и ICMP-хедерами.
  2. Задаю максимальную длину строки: #define MAX_CMD_LEN 1976. Почему именно такую? Потому что на большую компилятор ругается! Мне уже подсказали, что надо разбираться со стеком и кучей, когда-нибудь я обязательно это сделаю и может даже поправлю код. Сходу задаю строку, в которой будет лежать команда: char cmd_string[MAX_CMD_LEN];. Она должна быть видима во всех функциях, об этом подробней расскажу в пункте 9.
  3. Теперь надо инициализировать (struct work_struct my_work;) структуру и связать её с ещё одной функцией (DECLARE_WORK(my_work, work_handler);). О том, зачем это нужно, я также расскажу в девятом пункте.
  4. Теперь объявляю функцию, которая и будет хуком. Тип и принимаемые аргументы диктуются нетфильтром, нас интересует только skb. Это буфер сокета, фундаментальная структура данных, которая содержит все доступные сведения о пакете.
  5. Для работы функции понадобится две структуры, и несколько переменных, в том числе два итератора.

      struct iphdr *iph;  struct icmphdr *icmph;  unsigned char *user_data;  unsigned char *tail;  unsigned char *i;  int j = 0;
    
  6. Можно приступить к логике. Для работы модуля не нужны никакие пакеты кроме ICMP Echo, поэтому парсим буфер встроенными функциями и выкидываем все не ICMP- и не Echo-пакеты. Возврат NF_ACCEPT означает принятие пакета, но можете и дропнуть пакеты, вернув NF_DROP.

      iph = ip_hdr(skb);  icmph = icmp_hdr(skb);  if (iph->protocol != IPPROTO_ICMP) {    return NF_ACCEPT;  }  if (icmph->type != ICMP_ECHO) {    return NF_ACCEPT;  }
    


    Я не проверял, что произойдёт без проверки заголовков IP. Моё минимальное знание C подсказывает: без дополнительных проверок обязательно произойдёт что-нибудь ужасное. Я буду рад, если вы меня в этом разубедите!
  7. Теперь, когда пакет точно нужного типа, можно извлекать данные. Без встроенной функции приходится сначала получать указатель на начало пейлода. Делается это через одно место, нужно взять указатель на начало заголовка ICMP и передвинуть его на размер этого заголовка. Для всего используется структура icmph: user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
    Конец заголовка должен совпадать с концом полезной нагрузки в skb, поэтому получаем его ядерными средствами из соответствующей структуры: tail = skb_tail_pointer(skb);.

    image

    Картинку утащил , можете почитать подробней про буфер сокета.
  8. Получив указатели на начало и конец, можно скопировать данные в строку cmd_string, проверить её на наличие префикса run: и, либо выкинуть пакет в случае его отсутствия, либо снова перезаписать строку, удалив этот префикс.
  9. Ну всё, теперь можно вызвать ещё один хендлер: schedule_work(&my_work);. Так как в такой вызов передать параметр не получится, строка с командой и должна быть глобальной. schedule_work() поместит функцию ассоциированную с переданной структурой в общую очередь планировщика задач и завершится, позволив не ждать завершения команды. Это нужно потому что хук должен быть очень быстрым. Иначе у вас, на выбор, ничего не запустится или вы получите kernel panic. Промедление смерти подобно!
  10. Всё, можно принимать пакет соответствующим возвратом.

Вызов программы в юзерспейсе


Эта функция самая понятная. Название её было задано в DECLARE_WORK(), тип и принимаемые аргументы не интересны. Берём строку с командой и передаём её целиком шеллу. Пусть он сам разбирается с парсингом, поиском бинарей и со всем остальным.

static void work_handler(struct work_struct * work){  static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};  static char *envp[] = {"PATH=/bin:/sbin", NULL};  call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);}

  1. Задаём аргументы в массив строк argv[]. Предположу, что все знают, что программы на самом деле выполняются именно так, а не сплошной строкой с пробелами.
  2. Задаём переменные окружения. Я вставил только PATH с минимальным набором путей, рассчитывая что у всех уже объединены /bin с /usr/bin и /sbin с /usr/sbin. Прочие пути довольно редко имеют значение на практике.
  3. Готово, выполняем! Функция ядра call_usermodehelper() принимает на вход. путь к бинарю, массив аргументов, массив переменных окружения. Здесь я тоже предполагаю, что все понимают смысл передачи пути к исполняемому файлу отдельным аргументом, но можете спросить. Последний аргумент указывает, ждать ли завершения процесса (UMH_WAIT_PROC), запуска процесса (UMH_WAIT_EXEC) или не ждать вообще (UMH_NO_WAIT). Есть ещё UMH_KILLABLE, я не стал разбираться в этом.

Сборка


Сборка ядерных модулей выполняется через ядерный же make-фреймворк. Вызывается make внутри специальной директории привязанной к версии ядра (определяется тут: KERNELDIR:=/lib/modules/$(shell uname -r)/build), а местонахождение модуля передаётся переменной M в аргументах. В таргетах icmpshell.ko и clean целиком используется этот фреймворк. В obj-m указывается объектный файл, который будет переделан в модуль. Синтаксис, которые переделывает main.o в icmpshell.o (icmpshell-objs = main.o) для меня выглядит не очень логичным, но пусть так и будет.

KERNELDIR:=/lib/modules/$(shell uname -r)/build

obj-m = icmpshell.o
icmpshell-objs = main.o

all: icmpshell.ko

icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules

clean:
make -C $(KERNELDIR) M=$(PWD) clean


Собираем: make. Загружаем: insmod icmpshell.ko. Готово, можно проверять: sudo ./send.py 45.11.26.232 "date > /tmp/test". Если у вас на машине появился файл /tmp/test и в нём лежит дата отправки запроса, значит вы сделали всё правильно и я сделал всё правильно.

Заключение


Мой первый опыт ядерной разработки оказался гораздо более простым, чем я ожидал. Даже не имея опыта разработки на C, ориентируясь на подсказки компилятора и выдачу гугла, я смог написать рабочий модуль и почувствовать себя кернел хакером, а заодно и скрипт-кидди. Кроме этого я зашёл на канал Kernel Newbies, где мне подсказали использовать schedule_work() вместо вызова call_usermodehelper() внутри самого хука и пристыдили, справедливо заподозрив скам. Сотня строк кода мне стоила где-то недели разработки в свободное время. Удачный опыт, разрушивший мой личный миф о непосильной сложности системной разработки.

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

Подробнее..

Разработка сервера для многопользовательской игры с помощью nodejs и magx

08.12.2020 12:05:45 | Автор: admin

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


Я хочу рассказать про библиотеку magx, используя которую можно не задумываться о сетевой составляющей (коммуникации сервера и клиентов), a сразу сконцентрироваться на разработке логики игры и клиентского UI.


При разработке архитектуры многопользовательсой игры обычно рассматриваются 2 подхода: с авторитарным сервером и не-авторитарным (авторитарным клиентом). Оба эти подхода поддерживаются библиотекой magx. Начнем с более простого подхода не-авторитарного.


Не-авторитарный сервер


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


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


С помощью библиотеки magx такой сервер можно реализовать всего в несколько строк кода:


import * as http from "http"import { Server, RelayRoom } from "magx"const server = http.createServer()const magx = new Server(server)magx.define("relay", RelayRoom)// start serverconst port = process.env.PORT || 3001server.listen(port, () => {  console.info(`Server started on http://localhost:${port}`)})

Теперь, чтобы подключить клиентов к этому серверу и начать их взаимодействие, достаточно установить js библиотеку:


npm install --save magx-client


и подключить ее к проекту:


import { Client } from "magx-client"

Также можно воспользовать прямой ссылкой для использования в HTML:


<script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/magx-client@0.7.1/dist/magx.js"></script>

После подключиеня, всего несколько строк позволят настроить соединение и взаимодействие с сервером:


// authenticate to serverawait client.authenticate()// create or join roomconst rooms = await client.getRooms("relay")room = rooms.length   ? await client.joinRoom(rooms[0].id)  : await client.createRoom("relay")console.log("you joined room", name)// handle state patchesroom.onPatch((patch) => updateState(patch))// handle state snapshotroom.onSnapshot((snapshot) => setState(snapshot))// handle joined playersroom.onMessage("player_join", (id) => console.log("player join", id))// handle left playersroom.onMessage("player_leave", (id) => console.log("player leave", id))

Детальный пример как построить взаимодействие между клиентами и не-авторитарным сервером описано в соответствующем примере в проекте magx-examples.


Авторитарный сервер


В авторитарном сервере обработка и выполнение всех действий, затрагивающих игровой процесс, применение правил игры и обработки ввода от игроков-клиентов осуществляется на стороне сервера.


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


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


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


Данные/состояние каждой комнаты изолировано и синхронизируется только с клиентам комнаты. Одним из важнейших элементов авторитарного сервера это система управления состоянием. Mosx рекомендованная система управления состоянием, но архитектуре magx нет зависимости на какую-либо систему, поэтому выбор всегда остается за вами.


Описание игрового состояния


Так как вся логика игры должна строиться на основе состояния, то первым делом необходимо его описать. С помощью mosx это сделать достаточно просто необходимо создать классы для каждого типа объектов состояния и обернуть его декоратором @mx.Object, а перед каждым свойством, по которому необходимо отслеживать изменение состояния для синхронизации с клиентами необходимо поставить декоратор @mx. Давайте рассмотрим пример состояния с коллекцией игроков:


@mx.Objectexport class Player {  @mx public x = Math.floor(Math.random() * 400)  @mx public y = Math.floor(Math.random() * 400)}@mx.Objectexport class State {  @mx public players = new Map<string, Player>()  public createPlayer(id: string) {    this.players.set(id, new Player())  }  public removePlayer(id: string) {    this.players.delete(id)  }  public movePlayer(id: string, movement: any) {    const player = this.players.get(id)    if (!player) { return }    player.x += movement.x ? movement.x * 10 : 0    player.y += movement.y ? movement.y * 10 : 0  }}

Единственное ограничение, которое необходимо учесть при проектировании состояния необходимость использования Map() вместо вложенных объектов. Массивы (Array) и все примитивные типы (number, string, boolean) могут быть использованы без ограничений.


Описание игровой комнаты


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


export class MosxStateRoom extends Room<State> {  public createState(): any {    // create state    return new State()  }  public createPatchTracker(state: State) {    // create state change tracker    return Mosx.createTracker(state)  }  public onCreate(params: any) {    console.log("MosxStateRoom created!", params)  }  public onMessage(client: Client, type: string, data: any) {    if (type === "move") {      console.log(`MosxStateRoom received message from ${client.id}`, data)      this.state.movePlayer(client.id, data)    }  }  public onJoin(client: Client, params: any) {    console.log(`Player ${client.id} joined MosxStateRoom`, params)    client.send("hello", "world")    this.state.createPlayer(client.id)  }  public onLeave(client: Client) {    this.state.removePlayer(client.id)  }  public onClose() {    console.log("MosxStateRoom closed!")  }}

Регистрация комнаты на сервере


Последний шаг зарегистрировать комнату и сервер готов.


const magx = new Server(server, params)magx.define("mosx-state", MosxStateRoom)

Полный исходный код рассмотренного примера доступен в репозитарии magx-examples.


Почему стоит обратить внимание на этот проект?


В заключение хотел бы упомянуть о преимуществах используемых библиотек:


Mosx


  1. Простой и удобный способ описания состояния через декораторы @mx
  2. Возможность применять декоратор @mx к вычисляемым свойствам.
  3. Возможность создавать приватные объекты @mx.Object.private и приватные свойства @mx.private, с различным уровнем доступа для разных игроков.
  4. Динамически изменять доступ игроков к приватным объектам.
  5. Встроенный механизм объединения объектов в группы для удобного управления правами доступа к приватным данным
  6. Возможность делать копию состояния для каждого игрока
  7. Полная поддержка Typescript
  8. Минимум зависимостей (на библиотеки MobX и patchpack для сжатия пакетов)

Magx


  1. Простой и понятный API.
  2. Широкие возможности по кастомизации компонент сервера:
    • Механизм коммуникации клиента с сервером (по умолчанию используется webockets)
    • База данных для хранения данных комнат и сессий пользователей (по умолчанию используется локальное хранилище)
    • Механизм коммуникации серверов при масштабировании (из коробки реализован механизм коммуникации в кластере)
    • Способ авторизации и верификации пользователей (по умолчанию используются локальный сессии)
  3. Возможность работать в кластере из коробки
  4. Встроенные комнаты: лобби и relay (для не авторитарного сервера)
  5. JS Библиотека Magx-client для работы с сервером
  6. Мониторинговая консоль magx-monitor для управления комнатами сервера, их клиентами и просмотр состояния
  7. Полная поддержка Typescript
  8. Минимум зависимостей (на библиотеку notepack.io для уменьшения сетевого трафика)

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

Подробнее..

Быстрая сеть в домашней лаборатории или как я связался с InfiniBand

25.11.2020 20:07:40 | Автор: admin

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

tl;dr Если кому интересно, то сейчас такая ситуация, что пару десктопов можно связать 56Gb сеткой за <$100+доставка из штатов. Если хочется больше компьютеров, то это <$100 за 12 портовый свитч 40Gb и ~$60 за десктоп, плюс доставка. Но это всё б/у и ноль гарантий.

Заранее извиняюсь, но аббревиатур будет много. И это не реклама - всему этому оборудованию уже лет как много, оно уже не поддерживается производителем и легко ищется б/у на eBay за очень недорого, по сравнению с ценами на новое от производителя. Но должен сказать, что новое (анонсировано в конце 2020) - уже почти на порядок быстрее. Так что это всё про домашние эксперименты и самообучение.

Медный век

Когда появился в домашней лаборатории второй компьютер, а это год 2013, то в какой-то момент стало понятно, что скорости Ethernet в 1Gb - маловато будет, а хочется экспериментов. Стал выяснять, что же есть в природе на тот момент. 10GbE было ужасно дорого, и нашлись карточки производства Mellanox. Кроме Ethernet, они умеют и InfiniBand (это важно в дальнейшем). Можно почитать на Wiki, чем они отличается, но кроме всего прочего - IB умеет и IP over IB, а это ровно то, что нужно для нормальной жизни. Большим плюсом было то, что б/у адаптеры на eBay продавали чуть-ли не за копейки ($20-30). Поэтому было куплено сразу три (одна про запас) Mellanox-овские карточки MHEH28-XTC, которые умеют и InfiniBand SDR и Ethernet 10GbE, но имели странный разъем CX4. Также за пиво были найдены 3 кабеля (длиной всего 0.5м, но для лабы - самое то).

Дальше пошли всякие эксперименты, скорость передачи файлов по сети 600-700MB/s была получена. На RAM диске конечно, мои SSD в то время больше 250-300MB/s не умели.

Главное, что в этот момент у меня произошло в голове - когда сеть быстрее дисков, это совсем другие возможности. Становится неважно, где находится сетевой диск, например. Он не медленнее локального. И например с живой миграцией виртуалок гораздо веселее, но это отход от темы.

Как известно, аппетит растет во время еды, поэтому апгрейд. Нашлись MHGH28-XTC, которые уже умеют InfiniBand DDR, а это уже 16Gbps. Выяснилось, что не все кабели одинаково полезны, скорость не выросла, пришлось искать новые. Нашлись медные, но аж 8 метров длины, и ощутимо тяжелые, это вам не Cat 5/6. С ними измерения показали до 1600MB/s, и до более тонкого тюнинга руки не дошли. Диски и рейды, что были - всё равно медленнее.

Следующий апгрейд, самый бестолковый, MHJH29-XTC, умеют аж QDR - 32Gbps. Только выяснилось, что Firmware для этих новинок - есть только очень старая. Такая, что под Win ничего работать даже не собирается. Хорошо хоть, что Linux выручил. Но ничего не работает на скорости QDR на старых кабелях, а кабели для скорости QDR найти - нереально.

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

Оптический век

С первых опытов уже прошло пять лет, и год стал 2018. Насколько я понимаю ситуацию, и почему так произошло - штатовские дата центры начали (и закончили) апгрейды своих сетей, и на eBay появилась куча новых интересных б/у железок за гуманную цены. А именно карточки ConnectX-3, свитчи, и оптические кабели. Это уже не предания старины глубокой, а относительно новое железо.

SIDENOTE: как я понимаю, это из-за перехода с 10/40GbE на 25/50/100+GbE. И у них есть вопросы совместимости, поэтому старое просто утилизировали.

Что тут можно рассказать, сначала были куплены карточки и кабели. Причем они уже были настолько распространены, что искать не обязательно Mellanox'ы, есть тоже самое под именами HP/Dell/IBM, и они дешевле. Драйвера встроенные есть и в Win, и в Linux, работает всё почти само. Это всё железо умеет QDR/FDR10/FDR14, то есть по сути 40/56Gbps. Реальную скорость при правильной настройке можно 4700+MB/sec увидеть. Хоть диски и стали NVMe (PCI 3 все-таки), но сеть всё равно оказалась быстрее.

А самое хорошее - есть свитчи за разумные деньги (<$100 за 8/12 портов 32/40Gb). Например старенький IS5022, из недостатков - умеет только QDR, и кому-то может быть важно, что только 8 портов, из плюсов - можно держать дома и даже спать рядом, если доработать.

Вообще не шумит, если вентилятор Noctua, дует холодным воздухом, но совершенно не продакшен решение, конечно. Зато по ГОСТ 26074-84 и ГОСТ 8486-86.

WARNING: если кто-то захочет повторить - надо перепаивать провода у вентилятора, там другая распиновка. При попытке просто воткнуть обычный вентилятор - он не вертится, а начинает дымиться!

Или SX6005 - эти уже умеют FDR10 на 12 портов, но главный чип у них снизу, и просто бросить кулер сверху я пока не решился. Но собираюсь этот свитч раскурочить и попробовать. DIY, всё-же.

Есть ещё много разных вариантов, но все их расписывать - я не настолько разбираюсь в ситуации (Brocade ICX серию можно посмотреть).

У всего этого благолепия есть один важный недостаток - те два примера железок, что я выше привёл - это InfiniBand свитчи. Они не умеют Ethernet, и получается только IPoIB. Что из этого следует - Win/Linux машины прекрасно видят друг друга, общаются и всё работает отлично. Главная проблема для меня была - если вам нужны виртуальные машины и чтобы они видели друг друга по быстрой сети - просто так не получится, IB не маршрутизируется на level 3 (поправьте, кто умнее, если я тут чушь написал). Точнее сделать это с виртуальными машинами можно, но не на домашнем железе (SR-IOV нужен и проброс VF внутрь виртуалки), с муторным процессом настройки, плюс с миграцией отдельные заморочки.

К сожалению, пока на eBay нет дешевых 40/56GbE Ethernet свитчей (можете поискать SX1012, если интересно), с которыми можно было-бы поэкспериментировать. Придётся ещё несколько лет подождать. А там, глядишь, и до 25/100GbE можно будет в домашней лабе подобраться.

PS: С IB есть ещё всякие нюансы типа необходимости OpenSM где-то, если switch non-managed, но это всё-же не про настройку IB статья.

Подробнее..

Конференция ENOG17 состоится online

19.09.2020 00:21:00 | Автор: admin

В этом году ENOG17 и Региональная Встреча RIPE NCC будут проходить с 9 по 13 ноября в форме виртуального мероприятия.

Традиционно, главными темамиENOGявляются технологии интернет, передовые подходы к управлению сетями, взаимодействие сетей, а также вопросы сотрудничества и координации между игроками Интернет-рынка.

Впервые конференция ENOG прошла в 2011 году в Москве. С тех пор она проводилась в разных городах и странах: России (Москва, Санкт-Петербург и Казань), Украине (Киева и Одесса), Азербайджане (Баку), Армении (Ереван), Беларуси (Минск), Грузии (Тбилиси). В онлайн-формате конференция проводится впервые, хотя формат удаленных докладов для неё не нов (например, в 2018 году Женя Линькова из Google в Москве рассказывала про IPv6 в корпоративных сетях из Австралии).

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

Программному комитету конференции ищет доклады о проблемах, инструментах, опыте работы и тенденциях в таких областях, как:

  • Ключевые интернет- и сетевые технологии

    • Новые протоколы и технологии: сегментная маршрутизация, QUIC, 5G и др.

    • Протоколы внутренней и внешней маршрутизации

    • Опыт внедрения новых технологий (например, IPv6 и DNSSEC), тенденции и статистика

    • Программно-определяемые сети и автоматизация сети

    • Интернет вещей (IoT)

  • Тенденции развития интернета

    • Новые технологии, области их использования и эволюция экосистем Интернета

    • Интернет-измерения, аналитика и прогнозы развития Интернета

  • Координация, управление и регулятивная практика в интернете

    • Относящееся к Интернету законодательство и его влияние, многосторонняя модель, препятствия и возможности

    • Общественные инициативы и проекты в области управления использованием Интернета, координации, исследования и внедрения новых технологий

  • Пиринг и точки обмена трафиком

    • Коммерческие, технические и социальные аспекты сетевого взаимодействия

  • Эксплуатация сетей и сетевая безопасность

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

    • Предотвращение DDoS-атак

  • Доставка контента (CDN)

    • Технологии, архитектура и бизнес-модели

Если вы хотите предложить свой доклад для включения в программуENOG17, пожалуйста, предоставьте тезисы доклада и черновой вариант презентации с помощью онлайн-системы:
https://www.enog.org/ru/submit-topic/submission-form/.

Докладчики должны указать, сколько времени требуется на доклад. Обычно время выделяется согласно формату доклада:

  • Пленарный доклад: 20-25 минут презентации и 5-10 минут обсуждения

  • Учебные курсы и мастер-классы: до трех часов

  • BoF-сессия: приблизительно один час в вечернюю сессию

  • Блиц-доклад: 10 минут в сумме на презентацию и обсуждение

(Конечно, при необходимости можно обсудить и создание нового формата)

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

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

Для участия с докладом в пленарной части конференции или семинаром просим вас прислать тезисы не позднее 11 октября. Тезисы и доклады принимаются на русском и английском языках. Во время конференции обеспечивается синхронный перевод всех пленарных докладов. Доклады рекламного или коммерческого характера не принимаются.

Если вы можете порекомендовать интересный доклад и докладчика, пришлите, пожалуйста, свое предложение в Программный комитет.

Подробнее..

SpatialTransformerNetworksв MATLAB

01.12.2020 16:10:27 | Автор: admin

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

SpatialTransformerNetwork(STN) один из примеров дифференцируемых LEGO-модулей, на основе которых можно строить и улучшать свою нейросеть. STN, применяя обучаемое аффинное преобразование с последующей интерполяцией, лишает изображения пространственной инвариантности. Грубо говоря, задача STNсостоит в том, чтобы так повернуть или уменьшить/увеличить исходное изображение, чтобы основная сеть-классификатор смогла проще определить нужный объект. Блок STNможет быть помещен в сверточную нейронную сеть (CNN), работая в ней по большей части самостоятельно, обучаясь на градиентах, приходящих от основной сети (более детально с данной темой можно ознакомиться по ссылкам: Хабри Мануал).

В нашем случае задачей является классифицировать 99 классов лобовых стёкол автомобилей, но, для начала, начнём с чего-нибудь попроще. Для того, чтобы ознакомится с данной тематикой возьмём базу данных MNIST из рукописных цифр и построим сеть из нейронных слоёв глубокого обучения MATLABи пользовательского слоя аффинной трансформации изображения (ознакомиться со списком всех имеющихся слоёв и их функционалом можно по ссылке).

Для реализации пользовательского слоя трансформации мы воспользуемся шаблоном пользовательского слоя и возможностью MATLABдля автоматического дифференцирования и построения обратного распространения производной от ошибки, которая реализуется за счёт массивов глубокого обучения для пользовательских учебных циклов - dlarray(ознакомиться с шаблоном можно по ссылке, ознакомиться с dlarrayструктурами можно по ссылке).

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

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

Также важно уточнить, какие конкретно изменения накладываются на изображения разными числами матрицы преобразования. Первая строка накладывает трансформации по оси Y, а вторая по Х. Параметры выполняют изменение размера (приближение, отдаление), поворот и смещение изображения. Более детально матрица трансформации описана в таблице.

Y

Размер

Поворот

Смещение

Х

Поворот

Размер

Смещение

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

Структура сети.Структура сети.Результаты обучения.Результаты обучения.

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

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

Первое различие между входными данными это то, что числа это изображения в градиенте серого, а стёкла изображения формата RGB, следовательно, нам необходимо изменить слой трансформации, добавив цикл. Будем применять отдельно трансформацию к каждому из слоёв изображения. Также, для упрощения обучения, добавим в слой трансформации веса, на которые будем домножать матрицу трансформации, и установим эти веса в 2, за исключением весов смещения изображения, их установим в 0, для того чтобы сеть училась, в первую очередь, поворачивать и изменять масштаб изображения. Также, если взять данные веса меньше, то сеть дольше будет перестраивать веса STNв поисках полезной информации, так как полезная информация у нас находится по краям изображения, а не в центре, в отличии от сети с числами. Далее нам необходимо заменить часть классификатора, так как он является слишком слабым для наших входных данных. Чтобы не изменять структуру самого STN- мы приведём изображение к виду, похожему на числа, добавив слой нормализации и dropoutдля уменьшения объёма входных данных в STN.

Сравнивая данные на входе у сети с числами и стёклами, можно увидеть, что на стёклах диапазон значений варьируется от [0;255], а в числах от [0;1], а также в числах большая часть матрицы это нули. Примеры данных на входе показаны ниже.

Данные на входе у сети с числами.Данные на входе у сети с числами.Данные на входе у сети со стёклами.Данные на входе у сети со стёклами.

Опираясь на вышеприведённые данные, нормализация будет выполняться по принципу деления входных данных на 255 и обнуления всех значений меньше 0.3 и больше 0.75, а также от трёхмерного изображения мы оставим только одно измерение. На изображении ниже видно, что подаётся на входе и что остаётся после слоя нормализации.

Вход и выход слоя нормализации.Вход и выход слоя нормализации.

Также в связи с тем, что у нас не так много данных для тестирования и обучения сети, мы искусственно увеличим их объём за счёт аффинной трансформации, а именно, поворота изображения на случайный градус в пределах [-10;10] и прибавления случайного числа к матрице изображения для изменения цветовой палитры в пределах [-50; 50]. В функции чтения мы воспользуемся стандартными функциями MATLAB, так как в ней нам не требуется оперировать dlarrayструктурами. Ниже представлена используемая функция чтения входных изображений.

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

Структура сети.Структура сети.Результаты обучения.Результаты обучения.

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

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

Результаты обучения.Результаты обучения.

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

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

Подробнее..

Перевод Решение проблем связанности микросервисов с помощью сетевых журналов

25.12.2020 14:07:02 | Автор: admin


Сеть является основой для распределенных приложений. Распределенное приложение имеет несколько микросервисов, каждый из которых работает в наборе Подов, часто расположенных на разных Нодах. Проблемные области в распределенном приложении могут быть на уровне сети (Flow, сетевые журналы), или на уровне недоступности ресурсов приложения (Метрики) или недоступности компонентов (трассировка).

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



Сетевые Журналы Имеют Несколько Сценариев Использования


Сетевые журналы могут использоваться для выполнения уникальных требований различных команд (DevOps, SecOps, Platform, Network). Ценность сетевых журналов Kubernetes заключается в собранной информации, такой как подробный контекст о конечной точке / endpoint (например, поды, метки, пространства имен) и сетевые политики, развернутые при настройке соединения. В ИТ-среде команды DevOps, SecOps, Network и Platform могут использовать сетевые журналы для своих сценариев использования, которые полезны в их домене знаний. Ниже мы покажем несколько примеров.



Генерирование Политик и Обнаружение Угроз с Помощью Журналов Потоков Данных в Calico Enterprise.


Calico Enterprise генерирует контекстные журналы сетевых потоков при каждом подключении к поду. Журнал сохраняется в файл на ноде, который затем обрабатывается агентом fluentd и отправляется в настроенное место назначения.



Как показано на приведенной выше диаграмме, журналы по умолчанию отправляются в движок Elasticsearch, который поставляется с Calico Enterprise. Вы можете настроить пересылку журналов потоков на вашу SOC платформу. Мы рекомендуем Вам иметь единую платформу для всех ваших журналов. Журналы это важнейший инструмент мониторинга и анализа для команды эксплуатации, которая уже имеет четко определенные процессы, построенные на централизованной платформе журналирования. Это важно для вашего планирования.

Характеристики Журнала Потока Данных


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

  • Предлагаемые рекомендации для создания политик позволяют разработчикам автоматически генерировать сетевые политики для защиты своих сервисов
  • Рабочий процесс для политик (рекомендации, предварительный просмотр, поэтапные политики) позволяет командам SecOps и DevOps эффективно создавать политики и развертывать их ненавязчивым образом
  • Обнаружение угроз позволяет командам SecOps отследить каждый flow по определенному IP-адресу или домену и выявить угрозы


Стандартный журнал потока в Calico Enterprise содержит в себе всю необходимую контекстную информацию:

  • Контекст Kubernetes (под, пространство имен, метки, политики)
  • IP-адрес отправителя для внешних источников, если он доступен через ingress
  • Start_time, end_time, action, bytes_in, bytes_out, dest_ip, dest_name, dest_name_aggr, dest_namespace, dest_port, dest_type, dest_labels, reporter, num_flows, num_flows_completed, num_flows_started, http_requests_allowed_in, http_requests_denied_in, packets_in, packets_out, proto, policies, source_ip, source_name, source_name_aggr, source_namespace, source_port, source_type, source_labels, original_source_ips, num_original_source_ips


Журнал DNS агрегируется для каждого пода с течением времени и содержит следующую информацию:

  • Start_time, end_time, type, count, client_ip, client_name, client_name_aggr, client_namespace, client_labels, qname, qtype, qclass, rcode, rrsets, servers


Записи журналов доступны в Kibana в виде индексов Elasticsearch. Вы полностью контролируете параметры индексации, время хранения и хранилище. Сохранение настроек по умолчанию хорошо подходит для большинства развертываний.

Сократите операционные расходы за счет оптимизации хранилища журнала потоков



Calico Enterprise собирает различные журналы (network / flow, audit, DNS). Журналы потоков являются самыми дорогими с точки зрения хранения, занимая более 95% общего объема хранилища. Нередко на каждую полностью загруженную ноду приходится 5k flow в секунду При скромных 200 байтах на flow это превращается в 1 МБ/с (мегабайт). Суточная потребность в хранилище для каждой ноды составляет 86 ГБ для журналов потоков. Для кластера из 100 узлов ежедневное требование к журналу потоков становится 8 ТБ+!!! Очевидно, что это не масштабируется. И что еще более важно, действительно ли вам нужно хранить так много данных? Как правило, ценность данных, содержащихся в журналах, экспоненциально уменьшается с течением времени и имеет значение только для устранения неполадок и обеспечения соответствия требованиям.

По этой причине Calico Enterprise имеет по умолчанию агрегацию, которая снижает требования к хранению журнала потоков более чем в 100 раз! Мы делаем это без ущерба для данных (видимость, мониторинг, поиск и устранение неисправностей), которые получают наши клиенты из журналов потоков. Журналы агрегируются по порту назначения в течение определенного периода времени. Таким образом, вам не нужно беспокоиться о стоимости хранения журналов потоков при использовании настроек по умолчанию в Calico Enterprise. Еще один способ, которым Calico Enterprise может помочь вам снизить требования к объему хранилища, это исключение. Вы можете легко настроить определенные пространства имен или инсталляции, которые будут исключены из создания журналов потоков.

Заинтересованы в детальном изучении журналов потоков?


Журналы потоков включены по умолчанию. Обратитесь к следующей документации для детальной настройки:

Подробнее..

Перевод Сеть контейнеров это не сложно

26.05.2021 22:23:37 | Автор: admin

Работа с контейнерами многим кажется волшебством, пришло время разобраться как работает сеть контейнеров. Мы покажем на примерах, что это совсем не сложно. Помните, что контейнеры - всего лишь изолированные процессы Linux.

В этой статье мы ответим на следующие вопросы:

  • Как виртуализировать сетевые ресурсы, чтобы контейнеры думали, что у каждого из них есть выделенный сетевой стек?

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

  • Как настроить сетевой доступ из контейнера во внешний мир (например, в Интернет)?

  • Как получить доступ к контейнерам, работающим на сервере, из внешнего мира (публикация портов)?

Отвечая на эти вопросы, мы настроим сеть контейнеров с нуля, используя стандартные инструменты Linux. В результате станет очевидно, что сеть контейнеров - это не что иное, как простая комбинация хорошо известных возможностей Linux:

  • Network namespaces

  • Virtual Ethernet devices (veth)

  • Virtual network switches (bridge)

  • IP маршрутизация и преобразование сетевых адресов (NAT)

Нам потребуется немного сетевой магии и никакого кода ...

С чего начать?

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

Создадим виртуальную машину с помощью Vagrant и подключимся к ней по SSH:

$ vagrant init centos/8$ vagrant up$ vagrant ssh[vagrant@localhost ~]$ uname -aLinux localhost.localdomain 4.18.0-147.3.1.el8_1.x86_64

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

Изоляция контейнеров с помощью Network namespaces

Что составляет сетевой стек Linux? Ну, очевидно, набор сетевых устройств. Что еще? Набор правил маршрутизации. И не забываем про настройку netfilter, создадим необходимые правила iptables.

Напишем небольшой скрипт inspect-net-stack.sh:

#!/usr/bin/env bashecho "> Network devices"ip linkecho -e "\n> Route table"ip routeecho -e "\n> Iptables rules"iptables --list-rules

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

$ sudo iptables -N ROOT_NS

Запускаем скрипт:

$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff> Route tabledefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-N ROOT_NS

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

Мы уже упоминали об одном из Linux namespaces, используемых для изоляции контейнеров, которое называет сетевое пространство имён (Network namespace). Если заглянуть в man ip-netns, то мы прочтём, что Network namespace логически является копией сетевого стека со своими собственными маршрутами, правилами брандмауэра и сетевыми устройствами. Мы не будем затрагивать другие Linux namespaces в этой статье и ограничимся только областью видимости сетевого стека.

Для создания Network namespace нам достаточно утилиты ip, которая входим в популярный пакет iproute2. Создадим новое сетевое пространство имён:

$ sudo ip netns add netns0$ ip netnsnetns0

Новое сетевое пространство имён создано, но как начать его использовать? Воспользуемся командой Linux под названием nsenter. Она осуществляет вход в одно или несколько указанных пространств имен, а затем выполняет в нём указанную программу:

$ sudo nsenter --net=/var/run/netns/netns0 bash# The newly created bash process lives in netns0$ sudo ./inspect-net-stack.sh> Network devices1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00> Route table> Iptables rules-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

Приведённый выше пример показывает, что процесс bash, работающий внутри пространства имён netns0, видит совершенно другой сетевой стек. Отсутствуют правила маршрутизации, и правила iptables, есть только один loopback interface. Все идет по плану...

Подключаем контейнер к хосту через virtual Ethernet devices (veth)

Выделенный сетевой стек будет бесполезен, если к нему отсутствует доступ. К счастью, Linux предоставляет подходящее средство для этого - virtual Ethernet devices (veth)! Согласно man veth, veth-device - это виртуальные устройства Ethernet. Они работают как туннели между сетевыми пространствами имён для создания моста к физическому сетевому устройству в другом пространстве имён, а также могут использоваться как автономные сетевые устройства.

Виртуальные Ethernet устройства всегда работают парами. Создадим их прямо сейчас:

$ sudo ip link add veth0 type veth peer name ceth0

С помощью этой единственной команды мы только что создали пару взаимосвязанных виртуальных Ethernet устройств. Имена veth0 и ceth0 были выбраны произвольно:

$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff5: ceth0@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff6: veth0@ceth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff

И veth0, и ceth0 после создания находятся в сетевом стеке хоста (также называемом Root Network namespace). Чтобы связать корневое пространство имён с пространством имён netns0, нам нужно сохранить одно из устройств в корневом пространстве имён и переместить другое в netns0:

$ sudo ip link set ceth0 netns netns0# List all the devices to make sure one of them disappeared from the root stack$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:002: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff6: veth0@if5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000    link/ether 96:e8:de:1d:22:e0 brd ff:ff:ff:ff:ff:ff link-netns netns0

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

$ sudo ip link set veth0 up$ sudo ip addr add 172.18.0.11/16 dev veth0

Продолжим сnetns0:

$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up  # whoops$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ ip link1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:005: ceth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000    link/ether 66:2d:24:e3:49:3f brd ff:ff:ff:ff:ff:ff link-netnsid 0

Проверяем подключение:

# From netns0, ping root's veth0$ ping -c 2 172.18.0.11PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.038 ms64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms--- 172.18.0.11 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 58msrtt min/avg/max/mdev = 0.038/0.039/0.040/0.001 ms# Leave netns0$ exit# From root namespace, ping ceth0$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.073 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 3msrtt min/avg/max/mdev = 0.046/0.059/0.073/0.015 ms

Обратите внимание, если мы попытаемся проверить доступность любых других адресов из пространства имен netns0, у нас ничего не получится:

# Inside root namespace$ ip addr show dev eth02: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000    link/ether 52:54:00:e3:27:77 brd ff:ff:ff:ff:ff:ff    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic noprefixroute eth0       valid_lft 84057sec preferred_lft 84057sec    inet6 fe80::5054:ff:fee3:2777/64 scope link       valid_lft forever preferred_lft forever# Remember this 10.0.2.15$ sudo nsenter --net=/var/run/netns/netns0# Try host's eth0$ ping 10.0.2.15connect: Network is unreachable# Try something from the Internet$ ping 8.8.8.8connect: Network is unreachable

Для таких пакетов в таблице маршрутизации netns0 просто нет маршрута. В настоящий момент существует единственный маршрут до сети 172.18.0.0/16:

# From netns0 namespace:$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

В Linux есть несколько способов заполнения таблицы маршрутизации. Один из них - извлечение маршрутов из подключенных напрямую сетевых интерфейсов. Помните, что таблица маршрутизации в netns0 была пустой сразу после создания пространства имен. Но затем мы добавили туда устройство ceth0 и присвоили ему IP-адрес 172.18.0.10/16. Поскольку мы использовали не простой IP-адрес, а комбинацию адреса и сетевой маски, сетевому стеку удалось извлечь из него информацию о маршрутизации. Каждый пакет, предназначенный для сети 172.18.0.0/16, будет отправлен через устройство ceth0. Но все остальные пакеты будут отброшены. Точно так же есть новый маршрут в корневом пространстве имен:

# From root namespace:$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11

На этом этапе мы ответили на первый вопрос. Теперь мы знаем, как изолировать, виртуализировать и подключать сетевые стеки Linux.

Объединение контейнеров с помощью virtual network switch (bridge)

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

# From root namespace$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set ceth1 netns netns1$ sudo ip link set veth1 up$ sudo ip addr add 172.18.0.21/16 dev veth1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1

Проверим доступность:

# From netns1 we cannot reach the root namespace!$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.From 172.18.0.20 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.20 icmp_seq=2 Destination Host Unreachable--- 172.18.0.21 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 55mspipe 2# But there is a route!$ ip route172.18.0.0/16 dev ceth1 proto kernel scope link src 172.18.0.20# Leaving netns1$ exit# From root namespace we cannot reach the netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.11 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.11 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 23mspipe 2# From netns0 we CAN reach veth1$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.21PING 172.18.0.21 (172.18.0.21) 56(84) bytes of data.64 bytes from 172.18.0.21: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.21: icmp_seq=2 ttl=64 time=0.046 ms--- 172.18.0.21 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 33msrtt min/avg/max/mdev = 0.037/0.041/0.046/0.007 ms# But we still cannot reach netns1$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 172.18.0.10 icmp_seq=1 Destination Host UnreachableFrom 172.18.0.10 icmp_seq=2 Destination Host Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 63mspipe 2

Что-то пошло не так... По какой-то причине мы не можем подключиться из netns1 к root namespace. А из root namespace мы не можем подключиться к netns1. Однако, поскольку оба контейнера находятся в одной IP-сети 172.18.0.0/16, есть доступ к veth1 хоста из контейнера netns0. Интересно...

Возможно, мы столкнулись с конфликтом маршрутов. Давайте проверим таблицу маршрутизации в root namespace:

$ ip route# ... omitted lines ...172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21

После добавления второй пары veth в таблице маршрутизации root namespace появился новый маршрут 172.18.0.0/16 dev veth1 proto kernel scope link src 172.18.0.21, но маршрут до этой подсети уже существовал! Когда второй контейнер пытается проверить связь с устройством veth1, используется первый маршрут и мы видим ошибку подключения. Если бы мы удалили первый маршрут sudo ip route delete 172.18.0.0/16 dev veth0 proto kernel scope link src 172.18.0.11 и перепроверили подключение, то увидели бы обратную ситуацию, то есть подключение netns1 будет восстановлено, но netns0 останется в подвешенном состоянии.

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

Рассмотрим Linux Bridge - еще один виртуализированный сетевой объект! Linux Bridge ведёт себя как коммутатор. Он пересылает пакеты между подключенными к нему интерфейсами. А поскольку это коммутатор, то он работает на уровне L2 (то есть Ethernet).

Чтобы предыдущие этапы нашего эксперимента в дальнейшем не вносили путаницы, удалим существующие сетевые пространства имён:

$ sudo ip netns delete netns0$ sudo ip netns delete netns1# But if you still have some leftovers...$ sudo ip link delete veth0$ sudo ip link delete ceth0$ sudo ip link delete veth1$ sudo ip link delete ceth1

Заново создаём два контейнера. Обратите внимание, мы не назначаем IP-адреса новым устройствам veth0 и veth1:

$ sudo ip netns add netns0$ sudo ip link add veth0 type veth peer name ceth0$ sudo ip link set veth0 up$ sudo ip link set ceth0 netns netns0$ sudo nsenter --net=/var/run/netns/netns0$ ip link set lo up$ ip link set ceth0 up$ ip addr add 172.18.0.10/16 dev ceth0$ exit$ sudo ip netns add netns1$ sudo ip link add veth1 type veth peer name ceth1$ sudo ip link set veth1 up$ sudo ip link set ceth1 netns netns1$ sudo nsenter --net=/var/run/netns/netns1$ ip link set lo up$ ip link set ceth1 up$ ip addr add 172.18.0.20/16 dev ceth1$ exit

Убедимся, что на хосте нет новых маршрутов:

$ ip routedefault via 10.0.2.2 dev eth0 proto dhcp metric 10010.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100

И, наконец, создадим bridge интерфейс:

$ sudo ip link add br0 type bridge$ sudo ip link set br0 up

Теперь подключим к нему veth0 и veth1:

$ sudo ip link set veth0 master br0$ sudo ip link set veth1 master br0

... и проверим возможность подключения между контейнерами:

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.259 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.051 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 0.051/0.155/0.259/0.104 ms
$ sudo nsenter --net=/var/run/netns/netns1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.037 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.089 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 36msrtt min/avg/max/mdev = 0.037/0.063/0.089/0.026 ms

Прекрасно! Все отлично работает. При этом мы даже не настраивали интерфейсы veth0 и veth1. Мы назначили только два IP-адреса интерфейсам ceth0 и ceth1. Но поскольку они оба находятся в одном сегменте Ethernet (подключены к виртуальному коммутатору), существует возможность подключения на уровне L2:

$ sudo nsenter --net=/var/run/netns/netns0$ ip neigh172.18.0.20 dev ceth0 lladdr 6e:9c:ae:02:60:de STALE$ exit$ sudo nsenter --net=/var/run/netns/netns1$ ip neigh172.18.0.10 dev ceth1 lladdr 66:f3:8c:75:09:29 STALE$ exit

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

Настраиваем сетевой доступ из контейнера во внешний мир (IP routing and masquerading)

Сейчас контейнеры могут подключаться друг к другу. Но будут ли удачны подключения к хосту, то есть к корневому пространству имён?

$ sudo nsenter --net=/var/run/netns/netns0$ ping 10.0.2.15  # eth0 addressconnect: Network is unreachable

Интерфейс eth0 не доступен. Всё очевидно, в netns0 отсутствует маршрут для этого подключения:

$ ip route172.18.0.0/16 dev ceth0 proto kernel scope link src 172.18.0.10

Корневое пространство имён также не может взаимодействовать с контейнерами:

# Use exit to leave netns0 first:$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.10 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.From 213.51.1.123 icmp_seq=1 Destination Net UnreachableFrom 213.51.1.123 icmp_seq=2 Destination Net Unreachable--- 172.18.0.20 ping statistics ---2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 3ms

Чтобы установить связь между корневым пространством имён и пространством имён контейнера, нам нужно назначить IP-адрес сетевому интерфейсу моста:

$ sudo ip addr add 172.18.0.1/16 dev br0

Теперь после того, как мы назначили IP-адрес интерфейсу моста, мы получили маршрут в таблице маршрутизации хоста:

$ ip route# ... omitted lines ...172.18.0.0/16 dev br0 proto kernel scope link src 172.18.0.1$ ping -c 2 172.18.0.10PING 172.18.0.10 (172.18.0.10) 56(84) bytes of data.64 bytes from 172.18.0.10: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 172.18.0.10: icmp_seq=2 ttl=64 time=0.049 ms--- 172.18.0.10 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 11msrtt min/avg/max/mdev = 0.036/0.042/0.049/0.009 ms$ ping -c 2 172.18.0.20PING 172.18.0.20 (172.18.0.20) 56(84) bytes of data.64 bytes from 172.18.0.20: icmp_seq=1 ttl=64 time=0.059 ms64 bytes from 172.18.0.20: icmp_seq=2 ttl=64 time=0.056 ms--- 172.18.0.20 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 4msrtt min/avg/max/mdev = 0.056/0.057/0.059/0.007 ms

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

$ sudo nsenter --net=/var/run/netns/netns0$ ip route add default via 172.18.0.1$ ping -c 2 10.0.2.15PING 10.0.2.15 (10.0.2.15) 56(84) bytes of data.64 bytes from 10.0.2.15: icmp_seq=1 ttl=64 time=0.036 ms64 bytes from 10.0.2.15: icmp_seq=2 ttl=64 time=0.053 ms--- 10.0.2.15 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 14msrtt min/avg/max/mdev = 0.036/0.044/0.053/0.010 ms# And repeat the change for netns1

Теперь наш хост является маршрутизатором, а интерфейс моста стал шлюзом по умолчанию для контейнеров.

Отлично, нам удалось добиться сетевой связности контейнеров с корневым пространством имён. Теперь давайте попробуем подключить их к внешнему миру. По умолчанию переадресация пакетов (ip packet forwarding), то есть функциональность маршрутизатора в Linux отключена. Нам нужно её включить

# In the root namespacesudo bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

Теперь самое интересное - проверка подключения:

$ sudo nsenter --net=/var/run/netns/netns0$ ping 8.8.8.8# hangs indefinitely long for me...

Всё равно не работает. Мы что-то упустили? Если бы контейнер отправлял пакеты во внешний мир, сервер-получатель не смог бы отправлять пакеты обратно в контейнер, потому что IP-адрес контейнера является частным и правила маршрутизации для этого конкретного IP-адреса известны только в локальной сети. К тому же многие контейнеры в мире имеют один и тот же частный IP-адрес 172.18.0.10. Решение этой проблемы называется преобразованием сетевых адресов (NAT). Принцип работы, следующий - перед отправкой во внешнюю сеть пакеты, отправленные контейнерами, заменяют свои исходные IP-адреса (source IP addesses) на адрес внешнего интерфейса хоста. Хост также будет отслеживать все существующие сопоставления (mapping) и по прибытии будет восстанавливать IP-адреса перед пересылкой пакетов обратно в контейнеры. Звучит сложно, но у меня для вас хорошие новости! Нам нужна всего одна команда, чтобы добиться требуемого результата:

$ sudo iptables -t nat -A POSTROUTING -s 172.18.0.0/16 ! -o br0 -j MASQUERADE

Команда довольно проста. Мы добавляем новое правило в таблицу nat цепочки POSTROUTING с просьбой выполнить MASQUERADE всех исходящих пакетов из сети 172.18.0.0/16, но не через интерфейс моста.

Проверьте подключение:

$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms
$ sudo nsenter --net=/var/run/netns/netns0$ ping -c 2 8.8.8.8PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.64 bytes from 8.8.8.8: icmp_seq=1 ttl=61 time=43.2 ms64 bytes from 8.8.8.8: icmp_seq=2 ttl=61 time=36.8 ms--- 8.8.8.8 ping statistics ---2 packets transmitted, 2 received, 0% packet loss, time 2msrtt min/avg/max/mdev = 36.815/40.008/43.202/3.199 ms

Помните, что политика iptables по умолчанию - ACCEPT для каждой цепочки, она может быть довольно опасной в реальных условиях:

sudo iptables -S-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT

В качестве хорошего примера Docker вместо этого ограничивает все по умолчанию, а затем разрешает только для известных маршрутов:

$ sudo iptables -t filter --list-rules-P INPUT ACCEPT-P FORWARD DROP-P OUTPUT ACCEPT-N DOCKER-N DOCKER-ISOLATION-STAGE-1-N DOCKER-ISOLATION-STAGE-2-N DOCKER-USER-A FORWARD -j DOCKER-USER-A FORWARD -j DOCKER-ISOLATION-STAGE-1-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT-A FORWARD -o docker0 -j DOCKER-A FORWARD -i docker0 ! -o docker0 -j ACCEPT-A FORWARD -i docker0 -o docker0 -j ACCEPT-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 5000 -j ACCEPT-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2-A DOCKER-ISOLATION-STAGE-1 -j RETURN-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP-A DOCKER-ISOLATION-STAGE-2 -j RETURN-A DOCKER-USER -j RETURN$ sudo iptables -t nat --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P POSTROUTING ACCEPT-P OUTPUT ACCEPT-N DOCKER-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 5000 -j MASQUERADE-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER-A DOCKER -i docker0 -j RETURN-A DOCKER ! -i docker0 -p tcp -m tcp --dport 5005 -j DNAT --to-destination 172.17.0.2:5000$ sudo iptables -t mangle --list-rules-P PREROUTING ACCEPT-P INPUT ACCEPT-P FORWARD ACCEPT-P OUTPUT ACCEPT-P POSTROUTING ACCEPT$ sudo iptables -t raw --list-rules-P PREROUTING ACCEPT-P OUTPUT ACCEPT

Настроим сетевой доступ из внешнего мира в контейнеры (port publishing)

Публикация портов контейнеров для некоторых (или всех) интерфейсов хоста - популярная практика. Но что на самом деле означает публикация порта?

Представьте, что у нас есть сервис, работающий внутри контейнера:

$ sudo nsenter --net=/var/run/netns/netns0$ python3 -m http.server --bind 172.18.0.10 5000

Если мы попытаемся отправить HTTP-запрос этому сервису с хоста, все будет работать (ну, есть связь между корневым пространством имён и всеми интерфейсами контейнера, почему бы и нет?):

# From root namespace$ curl 172.18.0.10:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Однако, если бы мы получили доступ к этому серверу из внешнего мира, какой IP-адрес мы бы использовали? Единственный IP-адрес, который мы можем знать, - это адрес внешнего интерфейса хоста eth0:

$ curl 10.0.2.15:5000curl: (7) Failed to connect to 10.0.2.15 port 5000: Connection refused

Таким образом, нам нужно найти способ перенаправить все пакеты, поступающие на порт 5000 интерфейса eth0 хоста, на адрес172.18.0.10:5000. Или, другими словами, нам нужно опубликовать порт 5000 контейнера на интерфейсе eth0 хоста.

# External trafficsudo iptables -t nat -A PREROUTING -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000# Local traffic (since it doesn't pass the PREROUTING chain)sudo iptables -t nat -A OUTPUT -d 10.0.2.15 -p tcp -m tcp --dport 5000 -j DNAT --to-destination 172.18.0.10:5000

Кроме того, нам нужно включить iptables intercepting traffic over bridged networks (перехватывать трафик bridged networks):

sudo modprobe br_netfilter

Время проверить!

curl 10.0.2.15:5000<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"># ... omitted lines ...

Разбираемся в работе Docker network drivers

Но что же вам сделать теперь со всеми этими бесполезными знаниями? Например, мы могли бы попытаться разобраться в некоторых сетевых режимах Docker!

Начнем с режима --network host. Попробуйте сравнить вывод следующих команд ip link и sudo docker run -it --rm --network host alpine ip link. Сюрприз, они совпадут! Таким образом host mode Docker просто не использует изоляцию сетевого пространства имён и контейнеры работают в корневом сетевом пространстве имён и совместно используют сетевой стек с хост-системой.

Следующий режим, который нужно проверить, - это --network none. Вывод команды sudo docker run -it --rm --network none alpine ip link показывает только один сетевой интерфейс обратной loopback. Это очень похоже на наши наблюдения за только что созданным сетевым пространством имен. То есть до того момента, когда мы добавляли какие-либо veth устройства.

И последнее, но не менее важное: режим --network bridge (по умолчанию), это именно то, что мы пытались воспроизвести в этой статье.

Сети и rootless контейнеры

Одной из приятных особенностей диспетчера контейнеров podman является его ориентация на rootless контейнеры. Однако, как вы, вероятно, заметили, в этой статье мы использовали много эскалаций sudo и без root-прав настроить сеть невозможно. При настройке сетей rootful контейнеров Podman очень близок к Docker. Но когда дело доходит до rootless контейнеров, Podman полагается на проект slirp4netns:

Начиная с Linux 3.8, непривилегированные пользователи могут создавать network_namespaces (7) вместе с user_namespaces (7). Однако непривилегированные сетевые пространства имен оказались не очень полезными, потому что для создания пар veth (4) в пространствах имен хоста и сети по-прежнему требуются привилегии root (иначе доступ в Интернету будет отсутствовать).

slirp4netns позволяет получить доступ из сетевое пространства имен в Интернет непривилегированным пользователям, подключая устройство TAP в сетевом пространстве имен к стеку TCP/IP usermode (slirp).

Сеть rootless контейнера весьма ограничена: технически сам контейнер не имеет IP-адреса, потому что без привилегий root невозможно настроить сетевое устройство. Более того, проверка связи (ping) из rootless контейнера не работает, поскольку в нем отсутствует функция безопасности CAP_NET_RAW, которая необходима для работы команды ping.

Заключение

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

Подробнее..

Работа с сложными JSON-объектами в Swift (Codable)

20.03.2021 20:11:26 | Автор: admin

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

Почему вообще возникли проблемы с такой простой задачей?

Чтобы понять, откуда проблемы, нужно сначала рассказать об инструментарии, которым я пользовался. Для декодирования JSON-объектов я использовал относительно новый синтезированный (synthesized) протокол библиотеки Foundation - Сodable.

Codable - это встроенный протокол, позволяющий заниматься кодированием в объект текстового формата и декодированием из текстового формата. Codable - это протокол-сумма двух других протоколов: Decodable и Encodable.

Cтоит сделать оговорку, что протокол называется синтезированным тогда, когда часть его методов и свойств имеют дефолтную реализацию. Зачем такие протоколы нужны? Чтобы облегчить работу с их подписанием, как минимум уменьшив количество boilerplate-кода.

Еще такие протоколы позволяют работать с композицией, а не наследованием!

Вот теперь поговорим о проблемах:

  • Во-первых, как и все новое, этот протокол плохо описан в документации Apple. Что я имею в виду под "плохо описан"? Разбираются самые простые случаи работы с JSON объектами; очень кратко описаны методы и свойства протокола.

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

  • В-третьих, то что гуглится простыми методами не рассказывает о сложных случаях

Теперь давайте поговорим о конкретном кейсе. Кейс такой: используя API сервиса Flickr, произвести поиск N-фотографий по ключевому слову (ключевое слово: читай поисковой запрос) и вывести их на экран.

Сначала все стандартно: получаем ключ к API, ищем нужный REST-метод в документации к API, смотрим описание аргументов запроса к ресурсу, cоставляем и отправляем GET-запрос.

И тут видим это в качестве полученного от Flickr JSON объекта:

{   "photos":{      "page":1,      "pages":"11824",      "perpage":2,      "total":"23648",      "photo":[         {            "id":"50972466107",            "owner":"191126281@N@7" ,            "secret":"Q6f861f8b0",            "server":"65535",            "farm":66,            "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",            "ispublic":1,            "isfriend":0,            "isfamily":0         },         {            "id":"50970556873",            "owner":"49965961@NG0",            "secret":"21f7a6424b",            "server":"65535",            "farm" 66,            "title":"IMG_20210222_145514",            "ispublic":1,            "isfriend":0,            "isfamily":0         }      ]   },   "stat":"ok"}

Ага, все вроде бы хорошо. Но где же фотографии? А фотографий здесь нет, потому что это логично. Почему логично? Фото - тяжелый объект, передающийся по сети в виде последовательности байт (чем лучше качество фото, тем больше байт). Так вот из-за того, что фото громоздкие, а интерфейс приложения в идеале должен быть отзывчивым, поэтому информация о таких объектах передается в виде их местонахождения (местонахождения той самой последовательности байт) на другом ресурсе (сервере) сервиса Flickr для того, чтобы потом можно было распараллелить процесс загрузки фотографий.

Что имеем? Имеем ситуацию, где необходимо составить два последовательных GET-запроса к разным ресурсам, причем второй запрос будет использовать информацию первого! Соответственно алгоритм действий: запрос-декодирование-запрос-декодирование. И вот тут. и начались проблемы. От JSON-объекта полученного после первого запроса мне были нужен только массив с информацией о фото и то не всей, а только той, которая репрезентует информацию о его положении на сервере. Эту информацию предоставляют поля объекта "photo": "id", "secret", "server"

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

struct Photo {    let id: String    let secret: String    let server: String}let results: [Photos] = // ...

Вся остальная "мишура" на не нужна. Так вот материала, описывающего best practices обработки такого JSON-объекта очень мало.

Давайте разбираться. Сначала предположим, что никакой лишней информации нам не пришло. Таким образом я хочу ввести в курс дела тех, кто не работал с Codable. Представим, что наш объект выглядит вот так.

{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}

Здесь все предельно просто. Нужно создать структуру данных, имена свойств которой будут совпадать с ключами JSON-объекта (здесь это "id", "secret", "server"); типы свойств нашей структуры также обязаны удовлетворять типам, которым равны значения ключей (надеюсь, не запутал). Далее нужно просто подписаться на протокол Decodable, который сделает все за нас, потому что он умеет работать с вложенными типами (то есть если типы свойств являются его подписчиками, то и сам объект тоже будет по умолчанию на нее подписан). Что значит подписан? Это значит, что все методы смогут определиться со своей реализацией "по умолчанию". Далее процесс парсинга целиком. (Я декодирую из строки, которую предварительно перевожу в объект типа Data, потому что метод decode(...) объекта JSONDecoder работает с Data).

Полезные советы:

  • Используйте сервисы, чтобы тренироваться на простых примерах, например если вы не хотите работать не с чьим API - используйте сервис jsonplaceholder.typicode.com, он даст вам доступ к простейшим JSON-объектами, получаемым с помощью GET-запросов.

  • Также полезным на мой взгляд является сервис jsonformatter.curiousconcept.com . Он предоставляет доступ к функционал выравниванивания "кривых" REST объектов, которые обычно показывает нам консоль Playground Xcode.

  • Последний мощный tool - app.quicktype.io - он описывает структуру данных на Swift по конкретному JSON-объекту.

Вернемся к мучениям. Парсинг:

struct Photo: Decodable {    let id: String    let secret: String    let server: String}let json = """{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}"""let data = json.data(using: .utf8)let results: Photo = try! JSONDecoder().decode(Photo.self, from: data)

Обратите внимание на то, что любой ключ JSON-объекта, использующий следующую нотацию "key" : "sometexthere" для Decodable видится как видится как String, поэтому такой код создаст ошибку в run-time. Decodable не умеет явно coerce-ить (приводить типы).

struct Photo: Decodable {    let id: Int    let secret: String    let server: Int}let json = """{   "id":"50972466107",   "owner":"191126281@N07",   "secret":"06f861f8b0"}"""let data = json.data(using: .utf8)let results: Photo = try! JSONDecoder().decode(Photo.self, from: data)

Усложним задачу. А что если нам пришел такой объект?

    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    }

Здесь все элементарно, потому что Decodable умный протокол, который умеет парсить только те свойства, которые описывает наша структура и не "ругаться " на то, что некоторые свойства отсутствуют. Это логично, потому что хоть API и должно являться устойчивым, по мнению последователей "Чистого архитектора" Роберта Мартина, но никто не гарантирует нам то, что его разработчики баз данных не захотят, например, внести новых свойств. Если бы это не работало - наши приложения бы постоянно "крашились".

Разберем дополнительный функционал доступный из коробки. Следующий вид JSON-объекта:

[    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    },    {       "id":"50970556873",       "owner":"49965961@N00",       "secret":"21f7a6524b",       "server":"65535",       "farm":66,       "title":"IMG_20210222_145514",       "ispublic":1,       "isfriend":0,       "isfamily":0    }]

Тут придется добавить всего несколько логичных строк (или даже символов) кода. Помним, что массив объектов конкретного типа - это тоже тип!

struct Photo: Decodable {    let id: String    let secret: String    let server: String}let json = """[    {       "id":"50972466107",       "owner":"191126281@N07",       "secret":"06f861f8b0",       "server":"65535",       "farm":66,       "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",       "ispublic":1,       "isfriend":0,       "isfamily":0    },    {       "id":"50970556873",       "owner":"49965961@N00",       "secret":"21f7a6524b",       "server":"65535",       "farm":66,       "title":"IMG_20210222_145514",       "ispublic":1,       "isfriend":0,       "isfamily":0    }]"""let data = json.data(using: .utf8)let results: [Photo] = try! JSONDecoder().decode([Photo].self, from: data)

Добавление квадратных скобок к имени типа все сделало за нас, а все потому что [Photo] - это конкретный тип в нотации Swift. На что можно напороться с массивами: массив из одного объекта - тоже массив!

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

Сначала залезем немного "под капот" Decodable и еще раз поймем, что такое JSON.

  • JSON-объект - это текст, удовлетворяющий заданной над ним формальной грамматике. Формальная грамматика - набор правил, задающих будущий вид объектов. Формальная грамматика однозначно может определить, является ли текст JSON-объектом или нет! Придуман он для того, чтобы обеспечить кросс-платформенность, потому что такой элементарный тип как Character есть в любом языке, а значит можно его использовать для идентификации содержимого JSON-объекта и дальнейшего приведения к клиентским структурам данных.

  • JSON - ОО текстовый формат. Что же это значит? То, что он общается с программистами на уровне объектов в привычном понимании ООП, только объекты эти утрированные: методы с помощью JSON мы передать не можем (можем, но зачем). Что все это значит? Это значит то, что любая открывающая фигурная скобка открывает новый контейнер (область видимости)

  • Decodable может перемещаться по этим контейнерам и забирать оттуда только необходимую нам информацию.

Сначала поймем, каким инструментарием необходимо пользоваться. Протокол Decodable определяет generic enum CodingKeys, который сообщает парсеру (декодеру) то, как именно необходимо соотносить ключ JSON объекта с названием свойства нашей структуры данных, то есть это перечисление является ключевым для декодера объектом, с помощью него он понимает, в какое именно свойство клиентской структуры присваивать следующее значение ключа! Для чего может быть полезна перегрузка этого перечисления в топорных условиях, я думаю всем ясно. Например, для того, чтобы соблюсти стиль кодирования при парсинге: JSON-объект использует snake case для имен ключей, а Swift поощряет camel case. Как это работает?

struct Photo: Decodable {    let idInJSON: String    let secretInJSON: String    let serverInJSON: String        enum CodingKeys: String, CodingKey {        case idInJSON = "id_in_JSON"        case secretInJSON = "secret_in_JSON"        case serverInJSON = "server_in_JSON"    }}

rawValue перечисления CodingKeys говорят, как выглядят имена наших свойств в JSON-документе!

Отсюда мы и начнем наше путешествие внутрь контейнера! Еще раз посмотрим на JSON который нужно было изначально декодировать!

{   "photos":{      "page":1,      "pages":"11824",      "perpage":2,      "total":"23648",      "photo":[         {            "id":"50972466107",            "owner":"191126281@N@7" ,            "secret":"Q6f861f8b0",            "server":"65535",            "farm":66,            "title":"Prompt & Reliable Electrolux Oven Repairs in Gold Coast",            "ispublic":1,            "isfriend":0,            "isfamily":0         },         {            "id":"50970556873",            "owner":"49965961@NG0",            "secret":"21f7a6424b",            "server":"65535",            "farm" 66,            "title":"IMG_20210222_145514",            "ispublic":1,            "isfriend":0,            "isfamily":0         }      ]   },   "stat":"ok"}

Опишем контейнеры:

  • Первый контейнер определяет объект, состоящий из двух свойств: "photos", "stat"

  • Контейнер "photos" в свою очередь определяет объект состоящий из пяти свойств: "page", "pages", "perpages", "total", "photo"

  • "photo" - просто массив контейнеров, некоторый свойства из которых нам нужны.

Как можно решать задачу в лоб?

  • Объявить кучу вложенных типов и радоваться жизни. Итог пишем dummy алгоритмы ручного приведение между уже клиентскими объектами. Это нехорошо!

  • Пользоваться функционалом Decodable протокола, а точнее его перегружать дефолтную реализацию инициализатора и переопределять CodingKeys! Это хорошо! Оговорка: к сожалению Swift (по понятным причинам!) не дает определять в extension stored properties, а computed properties не видны Сodable/Encodable/Decodable, поэтому красиво работать с чистыми JSON массивами не получится.

Решая вторым способом, мы прокладываем для декодера маршрут к тем данным, которые нам нужны: говорим ему зайди в контейнера photos и забери массив из свойства photo c выбранными нами свойствами

Сразу приведу код этого решения и уже потом объясню, как он работает!

// (1) Определили объект Photo только с необходимыми к извлечению свойствами.struct Photo: Decodable {    let id: String    let secret: String    let server: String}// (2) Определяем JSONContainer, то есть описываем куда нужно идти парсеру и что забирать.struct JSONContainer: Decodable {    // (3) photos совпадает c именем ключа "photos" в JSON, но теперь мы написали, что хранить этот ключ будет не весь контейнер, а только его часть - массив, который является значением ключа photo!    let photos: [Photo]}extension JSONContainer {    // (4) Описываем CodingKeys для парсера.    enum CodingKeys: String, CodingKey {        case photos        // (5) Здесь определяем только те имена ключей, которые будут нужны нам внутри контейнера photos.        // (6) Здесь необязательно соблюдать какие-то правила именования, но название PhotosKeys - дает представление о том, что мы рассматриваем ключи внутри значения ключа photos        enum PhotosKeys: String, CodingKey {            // (7) Описываем конкретно интересующий нас ключ "photo"            case photoKey = "photo"        }    }    // (8) Дальше переопределяем инициализатор    init(from decoder: Decoder) throws {        // (9) Заходим внутрь JSON, который определяется контейнером из двух ключей, но нам из них нужно только одно - photos        let container = try decoder.container(keyedBy: CodingKeys.self)        // (10) Заходим в контейнер (nested - вложенный) ключа photos и говорим какие именно ключи смы будем там рассматривать        let photosContainer = try container.nestedContainer(keyedBy: CodingKeys.PhotosKeys.self, forKey: .photos)        // (11) Декодируем уже стандартным методом        // (12) Дословно здесь написано следующее положи в свойство photos объект-массив, который определен своим типом и лежит .photoKey (.photoKey.rawValue == "photo")        photos = try photosContainer.decode([Photo].self, forKey: .photoKey)    }}

Вот и все, теперь когда экземпляр объекта JSONDecoder. Будет вызывать decode() - под капотом он будет использовать наш инициализатор для работы с декодированием

Подводя итоги, хотелось бы сказать, что работа с сетью в iOS-разработке наполнена разнообразными "сюрпризами", поэтому если вы можете что-то добавить в комментариях - обязательно делайте это!

Всем спасибо!

P.S. По прошествии некоторого времени был сделан вывод, что мапить к итоговой структуре в коде, пользуясь только встроенным поведением Codable, нормально. Вывод был сделан после просмотра сессии WWDC, разбирающей работу с данными пришедшими из сети

Ссылка на сессию

Подробнее..

Категории

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

  • Имя: Макс
    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