Работа с Ingress-контроллерами обычно предполагает работу с Kubernetes в облаке, где внешние ip присваиваются автоматически. Я изучаю Kubernetes, обходясь обычным ноутбуком за NAT, на котором в виртуальных машинах запущены разные разновидности Kubernetes. Когда я разбирался с Ingress-контроллером, у меня возникло непреодолимое желание завести в него публичный ip и обратиться к нему извне. Давайте посмотрим, как это можно сделать.


Публичный ip я решил позаимствовать у vps. Для этого в reg.ru (не реклама, просто здесь все заработало) я арендовал на пару часов виртуалку с ubuntu20.04 на борту и парой ip адресов. Один будем использовать для доступа по ssh, второй снимем с интерфейса виртуальной машины и заведем в наш Kubernetes (работу можно организовать и проще, DNATами, но так интересней). Понятно, что публичные ip адреса, указанные далее, у каждого будут свои, и их необходимо заменить соответственно.


VPS


Состояние vps на начальном этапе:


# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:73:f5:f6 brd ff:ff:ff:ff:ff:ff
    inet 95.163.241.96/24 brd 95.163.241.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 89.108.76.161/24 brd 89.108.76.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 2a00:f940:2:4:2::51d4/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe73:f5f6/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 52:54:00:9a:da:36 brd ff:ff:ff:ff:ff:ff

Послушав eth0 убеждаемся, что гипервизор регулярно посылает arp запросы для подтверждения ip адресов. В дальнейшем мы отвяжем ip адрес 89.108.76.161 от интерфейса и запустим демон, который будет отвечать на эти arp запросы, изображая наличие ip адреса:


# tcpdump -i eth0 -n -v arp 
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
14:53:20.229845 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 95.163.241.96 tell 37.140.193.29, length 28
14:53:20.229879 ARP, Ethernet (len 6), IPv4 (len 4), Reply 95.163.241.96 is-at 52:54:00:73:f5:f6, length 28
14:54:05.031046 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 89.108.76.161 tell 37.140.193.29, length 28
14:54:05.031103 ARP, Ethernet (len 6), IPv4 (len 4), Reply 89.108.76.161 is-at 52:54:00:73:f5:f6, length 28
14:54:09.126771 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 95.163.241.96 tell 37.140.193.29, length 28
14:54:09.126827 ARP, Ethernet (len 6), IPv4 (len 4), Reply 95.163.241.96 is-at 52:54:00:73:f5:f6, length 28
14:54:49.573563 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 89.108.76.161 tell 37.140.193.29, length 28
14:54:49.573615 ARP, Ethernet (len 6), IPv4 (len 4), Reply 89.108.76.161 is-at 52:54:00:73:f5:f6, length 28
14:54:54.693462 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 95.163.241.96 tell 37.140.193.29, length 28
14:54:54.693493 ARP, Ethernet (len 6), IPv4 (len 4), Reply 95.163.241.96 is-at 52:54:00:73:f5:f6, length 28

Прокинем туннель с vps до домашнего ноута с помощью wireguard. Инструкций полно на просторах интернета, так что здесь ничего особенного:


# apt update
# apt install wireguard
# wg genkey | tee /etc/wireguard/private.key
# chmod go= /etc/wireguard/private.key
# cat /etc/wireguard/private.key | wg pubkey | tee /etc/wireguard/public.key
# cat  > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.15.0.1/24
SaveConfig = true
ListenPort = 51820
PrivateKey = gFzlk6/oBAkRnqTSqRQ0A03IR8iX2NY0Q9518xMTDmI=
EOF

Поднимаем wireguard:


# systemctl start wg-quick@wg0.service

Удаляем внешний ip с интерфейса:


# ip addr del 89.108.76.161/24 brd 89.108.76.255 dev eth0

Добавляем маршрутизацию к внешнему ip через туннель:


# ip r add 89.108.76.161 via 10.15.0.2

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


# iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE

Разрешаем доступ к внешнему ip и адресу ноутбука в сети wireguard через туннель:


# wg set wg0 peer hd7clB/uztrTOlsWTrHCF7mu9g6ECp+FhE2lhohWf1s= allowed-ips 89.108.76.161,10.15.0.2

Разрешаем форвардинг между интерфейсами:


# sysctl -w net.ipv4.ip_forward=1

и убеждаемся, что цепочка FORWARD не заблокирована:


# iptables-save | grep FORWARD   
:FORWARD ACCEPT [450722:544073659]
:FORWARD ACCEPT [4633:3846037]

После запуска wireguard в системе появится интерфейс wg0:


# ip a
4: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.15.0.1/24 scope global wg0
       valid_lft forever preferred_lft forever

Ноутбук (Ubuntu20.04)


Устанавливаем wireguard и генерируем ключи по аналогии:


# cat  > /etc/wireguard/wg2.conf <<EOF 
[Interface]
PrivateKey = Some private key
Address = 10.15.0.2/24
Table = off

[Peer]
PublicKey = aU3tLYzJPTKCtelYgVTtAfgnvixWdNK5jC2wnXgvemw=
AllowedIPs = 0.0.0.0/0
Endpoint = 95.163.241.96:51820
PersistentKeepalive = 25
EOF

Поднимаем туннель:


# systemctl start wg-quick@wg2.service

Проверяем наличие интерфейса wireguard:


# ip a
221: wg2: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.15.0.2/24 scope global wg2
       valid_lft forever preferred_lft forever

и связности с сервером:


# ping 10.15.0.1
PING 10.15.0.1 (10.15.0.1) 56(84) bytes of data.
64 bytes from 10.15.0.1: icmp_seq=1 ttl=64 time=16.3 ms
64 bytes from 10.15.0.1: icmp_seq=2 ttl=64 time=8.91 ms
64 bytes from 10.15.0.1: icmp_seq=3 ttl=64 time=9.00 ms

Для первоначальной проверки повесим внешний ip на loopback ноутбука:


# ip addr add 89.108.76.161 dev lo

Направляем весь трафик ноутбука через туннель, чтобы доходили обратные пакеты до клиентов, которые будут обращаться к 89.108.76.161 (192.168.88.1 — шлюз ноутбука по умолчанию):


# ip r add 95.163.241.96/32 via 192.168.88.1 
# ip r add default via 10.15.0.1 

Убедимся, что цепочка FORWARD не заблокирована:


# iptables-save | grep FORWARD
:FORWARD ACCEPT [67644779:42335638975]
:FORWARD ACCEPT [149377:28667150]

и


# sysctl -w net.ipv4.ip_forward=1

VPS


Проверяем доступность 89.108.76.161 с VPS:


# ping 89.108.76.161
PING 89.108.76.161 (89.108.76.161) 56(84) bytes of data.
64 bytes from 89.108.76.161: icmp_seq=1 ttl=64 time=6.90 ms
64 bytes from 89.108.76.161: icmp_seq=2 ttl=64 time=38.7 ms
64 bytes from 89.108.76.161: icmp_seq=3 ttl=64 time=59.9 ms

Запускаем демон, который будет отвечать на arp запросы:


# farpd -d -i eth0 89.108.76.161

Теперь заработает ping 89.108.76.161 из внешней сети (например, с телефона, подключенного к сети оператора).


Ноутбук


Напомним, на ноутбуке (гипервизор) запущена виртуальная машина (ВМ), в которой бегает minikube. Она соединена с бриджем virbr0 гипервизора:


# ip a
19: virbr0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 52:54:00:c3:6e:e6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
       valid_lft forever preferred_lft forever

Удалим внешний адрес с lo:


# ip addr del 89.108.76.161 dev lo

Настроим маршрутизацию пакетов к 89.108.76.161 в сторону ВМ:


# ip r add 89.108.76.161 via 192.168.122.245

ВМ


Интерфейсы ВМ:


l@minikube2:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 52:54:00:a5:b3:df brd ff:ff:ff:ff:ff:ff
    inet 192.168.122.245/24 brd 192.168.122.255 scope global dynamic enp1s0
       valid_lft 2292sec preferred_lft 2292sec
    inet6 fe80::5054:ff:fea5:b3df/64 scope link 
       valid_lft forever preferred_lft forever
3: br-5b72cdfd77e4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:01:94:a2:a5 brd ff:ff:ff:ff:ff:ff
    inet 192.168.58.1/24 brd 192.168.58.255 scope global br-5b72cdfd77e4
       valid_lft forever preferred_lft forever
    inet6 fe80::42:1ff:fe94:a2a5/64 scope link 
       valid_lft forever preferred_lft forever

Состояние форвардинга:


l@minikube2:~$ sysctl -w net.ipv4.ip_forward
net.ipv4.ip_forward = 1

l@minikube2:~$ sudo iptables-save | grep FORWARD
:FORWARD ACCEPT [2663492:1312451658]
:FORWARD ACCEPT [6299:278761]

На машине запущен миникуб с тремя нодами, которые представляют из себя контейнеры, соединенные бриджем br-5b72cdfd77e4:


l@minikube2:~$ docker ps
CONTAINER ID   IMAGE                                 COMMAND                  CREATED      STATUS        PORTS                                                                                                                                  NAMES
d672c95f6adc   gcr.io/k8s-minikube/kicbase:v0.0.37   "/usr/local/bin/entr…"   5 days ago   Up 34 hours   127.0.0.1:49197->22/tcp, 127.0.0.1:49196->2376/tcp, 127.0.0.1:49195->5000/tcp, 127.0.0.1:49194->8443/tcp, 127.0.0.1:49193->32443/tcp   helm-m03
6eac7091ea0c   gcr.io/k8s-minikube/kicbase:v0.0.37   "/usr/local/bin/entr…"   5 days ago   Up 34 hours   127.0.0.1:49192->22/tcp, 127.0.0.1:49191->2376/tcp, 127.0.0.1:49190->5000/tcp, 127.0.0.1:49189->8443/tcp, 127.0.0.1:49188->32443/tcp   helm-m02
c02b9bb12c98   gcr.io/k8s-minikube/kicbase:v0.0.37   "/usr/local/bin/entr…"   5 days ago   Up 34 hours   127.0.0.1:49187->22/tcp, 127.0.0.1:49186->2376/tcp, 127.0.0.1:49185->5000/tcp, 127.0.0.1:49184->8443/tcp, 127.0.0.1:49183->32443/tcp   helm

Маршрутизируем пакеты на третью ноду:


l@minikube2:~$ sudo ip r add 89.108.76.161 via 192.168.58.4

Зайдем на нее:


l@minikube2:~$ minikube ssh -n helm-m03

Повесим внешний адрес на lo:


docker@helm-m03:~$ sudo ip addr add 89.108.76.161 dev lo
docker@helm-m03:~$ ip a        
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever    
    inet 89.108.76.161/32 scope global lo
       valid_lft forever preferred_lft forever

21: eth0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:c0:a8:3a:04 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 192.168.58.4/24 brd 192.168.58.255 scope global eth0
       valid_lft forever preferred_lft forever

Поставим питон для проверки связности:


docker@helm-m03:~$ sudo apt update
docker@helm-m03:~$ sudo apt install python

и запустим сервер на порту 8080:


docker@helm-m03:~$ python -m http.server

Проверим доступ к 89.108.76.161 извне по http://89.108.76.161:8000.


Переходим к Ingress-контроллеру. Добавляем его в кластер:


l@minikube2:~$ minikube addons enable ingress

Внесем внешний ip в ingress controller:


l@minikube2:~$ k patch svc -n ingress-nginx ingress-nginx-controller -p '{"spec":{"externalIPs":["89.108.76.161"]}}'

и у нас автоматически добавляется DNAT на pod, отвечающий за работу с ingress-nginx-controller:


l@minikube2:~$ sudo iptables-save | grep 89.108.76.161
-A KUBE-SERVICES -d 89.108.76.161/32 -p tcp -m comment --comment "ingress-nginx/ingress-nginx-controller:http external IP" -m tcp --dport 80 -j KUBE-EXT-CG5I4G2RS3ZVWGLK
-A KUBE-SERVICES -d 89.108.76.161/32 -p tcp -m comment --comment "ingress-nginx/ingress-nginx-controller:https external IP" -m tcp --dport 443 -j KUBE-EXT-EDNDUDH2C75GIR6O

Развернем сервис whoami в Kubernetes:


l@minikube2:~$ cat > deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami  
  labels:
    app: whoami
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami
        image: traefik/whoami
        ports:
        - containerPort: 80
EOF

l@minikube2:~$ cat > service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: extip
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: whoami
EOF

l@minikube2:~$ cat ingress.yaml <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: extip

spec:
  ingressClassName: nginx
  rules:
  - host: extip.yourdomainhere
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: extip
            port:
              number: 80
EOF

l@minikube2:~$ k apply -f deployment.yaml
l@minikube2:~$ k apply -f service.yaml
l@minikube2:~$ k apply -f ingress.yaml

Пропишем в A записи домена extip.yourdomainhere внешний ip адрес 89.108.76.161. Обращаемся извне на http://extip.yourdomainhere, все работает!


curl extip.yourdomainhere
Hostname: whoami-75d55b64f6-7q894
IP: 127.0.0.1
IP: 10.244.0.17
RemoteAddr: 10.244.0.3:50120
GET / HTTP/1.1
Host: extip.yourdomainhere
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.5
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 192.168.58.4
X-Forwarded-Host: extip.yourdomainhere
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Scheme: http
X-Real-Ip: 192.168.58.4
X-Request-Id: f3c1f071b171b2ab1036241410acebcb
X-Scheme: http

Итак, мы позаимствовали публичный ip у vps, завели его в Kubernetes, организовали маршрутизацию и связность до этого адреса, развернули сервис в Kubernetes и проверили его работу.


Надеюсь было интересно.

Комментарии (12)


  1. AlexGluck
    00.00.0000 00:00

    Это из разряда "ненормальное программирование". Офигенно, мне нравится. А если ассиметричный канал сделать, чтобы ответ от ВМ отправлялся сразу клиенту? ВМ сеть поднятая с macvtap, пакет не инкапсулируется в тунель хоста и с сетевой карты уходит на маршрутизатор, который отправляет пакет клиенту согласно таблице маршрутизации (без ната и других обработчиков). Возможно провайдер заблокирует за спуфинг адресов?
    Возможно надо будет адрес с петли перевесить на eth0.


    1. edo1h
      00.00.0000 00:00

      Я не знаю домашних провайдеров, которые готовы пропускать пакеты с чужим src ip


  1. victorinov
    00.00.0000 00:00

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


    1. dsoastro Автор
      00.00.0000 00:00
      +1

      Хотелось сделать без привязки к внешним сервисам типа ngrok и поэксперементировать с переносом ip адреса


  1. AnyKey80lvl
    00.00.0000 00:00

    Плюс за жесть. )

    А вообще идея - на сеньор девопса давать такую задачу на собеседовании )


    1. duckhawk
      00.00.0000 00:00
      +1

      И сеньор девопс скажет про reverse proxy :)


  1. duckhawk
    00.00.0000 00:00

    Это, конечно, очень интересно, но почему не банальный reverse proxy на nginx, поднятый на vps?


    1. dsoastro Автор
      00.00.0000 00:00

      Попробовал сделать "настоящий" кластер с публичным ip внутри.


  1. malykhin
    00.00.0000 00:00

    Есть ощущение, что решение вот прямо сильно прибито гвоздями к методу выдачи внешнего ip у данного, конкретного хостера.
    Или вообще где-то под капотом включается dnat.
    Не совсем понятно каким образом шлюз провайдера/хостера "согласился" идти за внешним адресом внутрь туннеля, который совсем не L2, а обычный L3.
    Ну или я что не понимаю.


    1. dsoastro Автор
      00.00.0000 00:00
      +1

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

      Более подробно. farpd -d -i eth0 89.108.76.161 отвечает на arp запросы гипервизора провайдера и говорит, что 89.108.76.161 находится на таком-то мак адресе. Гипервизор направляет фреймы с адреcом назначения 89.108.76.161 на это мак, т.е. в мою VPS. Далее линукс отбрасывает L2 часть, а L3 просто маршрутизирует через туннель. Дальше пакеты маршрутизируются уже на ноуте до финального контейнера, в котором и есть 89.108.76.161.

      Если провайдер использует что-то типа обычного kvm, то у него есть бридж в который подключены veth от виртуальных маших. И поиск нужного ip адреса в подсети бриджа осуществляется через arp. Я вначале проверял аналогичную схему на виртуалке внутри гипервизора kvm на своем ноуте, все работало.

      Если провайдер использует openstack, то там все по другому (в виртуалке нет этого адреса на интерфейсе, он находится у провайдера, который делает DNAT на непубличный адрес на интерфейсе виртуалки), и скорее нужно отвязывать этот непубличный адрес.

      Reg.ru я выбрал, потому что там можно взять виртуалку с почасовой оплатой и добавить еще один ip адрес. Так получилось, что у этого провайдера схема заработала. Скрытых DNATов не использовал. Всегда можно проверить, виртуалка там стоит меньше руб. в час.


  1. senindmitriy
    00.00.0000 00:00
    +1

    Сюда бы еще схему для наглядности, было бы вообще супер.


  1. dsoastro Автор
    00.00.0000 00:00
    +1

    Постараюсь нарисовать, если время будет..