# Создаём под с Shadowsocks Для каждого VPN-сервера (локации) нужен отдельный клиентский под, который создаст SOCKS5-прокси внутри кластера. Другие поды будут подключаться к тому или иному SOCKS5-прокси в зависимости от их назначения. Все конфиги и манифесты K3S хранит в `etcd` и распространится по всем нодам кластера. Но создавать и вносить изменения непосредственно в `etcd` не удобно. Намного удобнее k3s-конфиги передавать через ConfigMap. К тому же это позволяет иметь копии конфигов на каком-нибудь хосте и делать резервные копии и восстанавливать их в случае необходимости. Не принципиально, где хранить конфиги и манифесты, так как после с помощью `kubectl` они будут загружены в k3s. Но лучше хранить их в одном месте, чтобы не искать по всему кластеру, где же они хранятся. Предлагаемая структура каталогов для хранения конфигураций и манифестов Kubernetes: ```text ~/k3s/ ├── vpn/ # Все VPN-клиенты │ ├── client-shadowsocks--moscow/ # Локация Москва │ │ ├── config.yaml # ConfigMap для Shadowsocks │ │ └── deployment.yaml # Deployment для Shadowsocks │ ├── client-shadowsocks--stockholm/ # Локация Стокгольм │ │ ├── config.yaml │ │ └── deployment.yaml │ └── cclient-shadowsocks--izmir/ # Локация Измир │ ├── config.yaml │ └── deployment.yaml ├── … └── … ``` Создаем файл `config.yaml` для первого Shadowsocks-клиента (Москва): ```bash nano ~/k3s/vpn/client-shadowsocks--moscow/config.yaml ``` И вставляем в него следующее: ```yaml apiVersion: v1 kind: ConfigMap metadata: name: shadowsocks-client-moscow namespace: kube-system # Ставим в kube-system, чтобы было системно data: config.json: | { "server": "", "server_port": <ПОРТ>, "local_address": "127.0.0.1", "local_port": 1081, "password": "", "method": "chacha20-ietf-poly1305", "mode": "tcp_and_udp" } ``` Что тут происходит: - `apiVersion: v1` — версия API Kubernetes. - `kind: ConfigMap` — это способ хранить конфиги внутри k3s. - `metadata:` — метаданные о конфиге. - `name:` — имя конфигурации. - `namespace:` — пространство имен, в котором будет храниться конфигурация. Мы используем `kube-system`, чтобы сделать его системным. - `data:` — данные конфигурации. - `config.json:` — имя файла, в который будет записан конфиг. - `|` — говорит, что дальше будет многострочный текст. - `{…}` — Собственно JSON-конфигурация нашего Shadowsocks-клиента. - `server` и `server_port` — адрес и порт нашего VPS. - `local_address` и `local_port` — где будет SOCKS5 внутри кластера. - `password` и `method` — пароль и метод шифрования. Метод шифрования `chacha20-ietf-poly1305` -- используется, например, VPN-сервисом Outline. Получить пароль для Outline можно с помощью base64 декодирования ключа. Структура строки подключения `ss://<ПАРОЛЬ_КОДИРОВАННЫЙ_В_BASE64>@:<ПОРТ>?type=tcp#<ИМЯ-КЛИЕНТА>` - `mode: tcp_and_udp` — включает поддержку TCP и UDP. Применим ConfigMap: ```bash sudo k3s kubectl apply -f /home/<ПОЛЬЗОВАТЕЛЬ>/k3s/vpn/client-shadowsocks--moscow/config.yaml ``` Важно указывать полный путь к файлу, а не от домашнего каталога `~\`. Запуская `kubectl` из под `sudo` (или от имени `root`), мы исполняем команды `k3s` от имени другого пользователя, а не от имени текущего. Когда выполним команду, то увидим что-то вроде: ```text configmap/shadowsocks-config-moscow created ``` Теперь создадим `Deployment` для Shadowsocks-клиента. Создаём файл `deployment.yaml`: ```bash nano ~/k3s/vpn/client-shadowsocks--moscow/deployment.yaml ``` И вставляем в него следующее: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: shadowsocks-client-moscow # Уникальное имя (должно совпадать с именем в config.yaml для ConfigMap) namespace: kube-system # В системном пространстве spec: replicas: 1 selector: matchLabels: app: shadowsocks-client-moscow template: metadata: labels: app: shadowsocks-client-moscow spec: containers: - name: shadowsocks-client image: shadowsocks/shadowsocks-libev:latest # Официальный образ command: ["ss-local"] # Запускаем клиент args: - "-c" # Указываем конфиг - "/etc/shadowsocks/config.json" # Путь внутри контейнер volumeMounts: - name: config-volume mountPath: /etc/shadowsocks # Монтируем ConfigMap ports: - containerPort: 1081 # Открываем порт SOCKS5 (TCP) protocol: TCP - containerPort: 1081 # Открываем порт SOCKS5 (UDP) protocol: UDP securityContext: privileged: true # Нужно для работы с сетью volumes: - name: config-volume configMap: name: shadowsocks-client-moscow # Связываем с ConfigMap ``` Объяснение: * `Pod` — это простейший объект в k3s, запускающий один контейнер. * `image` — официальный образ Shadowsocks. * `command` и `args` — запускают `ss-local` с конфигом из `ConfigMap`. * `volumeMounts` — подключают `config.json` из `ConfigMap` в контейнер. * `ports` — открываем 1080/TCP и 1080/UDP для SOCKS5. * `privileged: true` — даёт права для работы с сетью (в k3s это иногда нужно). Применим под: ```bash sudo k3s kubectl apply -f /home/opi/k3s/vpn/client-shadowsocks--moscow/deployment.yaml ``` ### Проверка Проверяем, что под запустился, посмотрев статус: ``` sudo k3s kubectl get pods -n kube-system ``` Увидим что-то типа: ```text NAME READY STATUS RESTARTS AGE … … shadowsocks-client-moscow-54d64bf5f4-trb6p 1/1 Running 0 24m … ``` Можно проверь логи: ```bash sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-54d64bf5f4-trb6p ``` Увидим, что клиент shadowsocks запустился: ```text 2025-03-09 09:48:24 INFO: initializing ciphers... chacha20-ietf-poly1305 2025-03-09 09:48:24 INFO: listening at 127.0.0.1:1081 2025-03-09 09:48:24 INFO: udprelay enabled ``` Запустился, но не подключился. Подключение произойдет при отправке первых пакетов через соединение. Для этого нужно зайти в под и запросить что-нибудь через `curl`. Но на поде нет `curl`, поэтому что по умолчанию образ контейнера shadowsocks-клиента минималистичен и в нём нет ничего лишнего. Нам придется собрать свой образ с `curl`. Создадим файл `Dockerfile` для сборки образа (да, сам Kubernetes не умеет собирать образы, для этого нужен Docker): ```bash nano k3s/vpn/client-shadowsocks--moscow/Dockerfile ``` И вставим в него следующее: ```dockerfile FROM shadowsocks/shadowsocks-libev:latest USER root RUN apk update && apk add curl netcat-openbsd ``` Что тут происходит: * `FROM` — базовый образ, от которого мы будем отталкиваться. * `USER root` — переключаемся на пользователя root, чтобы иметь возможность устанавливать пакеты. * `RUN` — выполнить команду в контейнере. В данном случае обновляем пакеты (`apk update`) и устанавливаем `curl` и `netcat-openbsd` (`apk add curl netcat-openbsd`). Cоберём образ: ```bash sudo docker build -t shadowsocks-with-tools:latest ~/k3s/vpn/client-shadowsocks--moscow/ ``` Увидим, что образ собрался: ```text [+] Building 1.4s (6/6) FINISHED docker:default => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 135B 0.0s => [internal] load metadata for docker.io/shadowsocks/shadowsocks-libev:latest 1.4s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [1/2] FROM docker.io/shadowsocks/shadowsocks-libev:latest@sha256:124d1bff89bf9e6be19d3843fdcd40c5f26524a7931c8accc5560a88d0a42374 0.0s => CACHED [2/2] RUN apk update && apk add curl netcat-openbsd 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:5708432467bcac4a0015cd97dbca968e9b69af06da192018169fff18673ed13f 0.0s => => naming to docker.io/library/shadowsocks-with-tools:latest ``` Перенесем полученный образ в k3s с помощью `ctr` (containerd CLI): ```bash sudo docker save shadowsocks-with-tools:latest | sudo k3s ctr images import - ``` Здесь: * `docker save` — экспортирует образ в tar-формат. * `k3s ctr` — вызывает ctr внутри k3s. * `images import -` — импортирует образ из stdin. Увидим что-то вроде: ```text unpacking docker.io/library/shadowsocks-with-tools:latest (sha256:ae615618ce9d2aac7d3764ef735108452adf3fc30bb65f23f28c345798880c80)...done ``` Проверим, что образ появился в k3s: ```bash sudo k3s ctr images ls | grep shadowsocks ``` Увидим что-то типа: ```text … docker.io/library/shadowsocks-with-tools:latest application/vnd.oci.image.manifest.v1+json sha256:… 22.5 MiB linux/arm64 io.cri-containerd.image=managed … … ``` Теперь нам нужно передать образ контейнера на другие ноды кластера. Как это сделать есть заметка "[Развертывание пользовательского контейнера в k3s](k3s-custom-container-deployment.md)" Когда наш контейнер окажется на всех нодах, изменим `deployment.yaml` Shadowsocks-клиента, чтобы использовать наш новый образ. Закомментируем строку `image: shadowsocks/shadowsocks-libev:latest` и вставим две строки после неё (обратите внимание на заметки): ```yaml … spec: containers: - name: shadowsocks-client # image: shadowsocks/shadowsocks-libev:latest image: shadowsocks-with-tools # Без :latest, чтобы k3s не "ходил" за контейнером в реестр (например, DockerHub) imagePullPolicy: Never # Только локальный образ, не тянуть из реестра … … ``` Уберём старый под из deployment и удалим сам под из k3s: ```bash sudo k3s kubectl delete deployment -n kube-system shadowsocks-client-moscow sudo k3s kubectl delete pod -n kube-system -l app=shadowsocks-client-moscow --force --grace-period=0 ``` Запустим новый под с нашим новым образом: ```bash sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--v/deployment.yaml ``` Проверим, что под запустился, посмотрев статус: ```bash sudo k3s kubectl get pods -n kube-system ``` Увидим что-то типа: ```text NAME READY STATUS RESTARTS AGE … shadowsocks-client-moscow-6cf7b956b8-mtsg4 1/1 Running 0 9s … ``` #### Проверка работы Shadowsocks Посмотрим логи пода с Shadowsocks-клиентом: ```bash sudo k3s kubectl logs -n kube-system -l app=shadowsocks-client-moscow ``` Увидим, что клиент shadowsocks запустился: ```text 2025-03-14 21:01:59 INFO: initializing ciphers... chacha20-ietf-poly1305 2025-03-14 21:01:59 INFO: listening at 127.0.0.1:1081 2025-03-14 21:01:59 INFO: udprelay enabled 2025-03-14 21:01:59 INFO: running from root user ``` Проверим TCP-соединение. Зайдём в под: ```bash sudo k3s kubectl exec -it -n kube-system shadowsocks-client-moscow- -- sh ``` И выполним внутри пода команду: ```bash curl --socks5 127.0.0.1:1081 http://ifconfig.me curl -k --socks5 127.0.0.1:1081 https://ifconfig.me ``` `ifconfig.me` -- это публичный сервис, который показывает IP-адрес, с которого к нему пришёл запрос. В первом случае проверяем http-соединение, а во втором — https. Ожидаемый результат: `` (IP-адрес нашего VPS). Выходим из пода: ```bash exit ``` Проверим логи еще раз: ```bash sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow- ``` Увидим, что клиент shadowsocks отработал: ```text 2025-03-14 21:01:59 INFO: running from root user 2025-03-14 21:03:01 INFO: connection from 127.0.0.1:55226 2025-03-14 21:03:01 INFO: connect to 34.160.111.145:80 2025-03-14 21:03:01 INFO: remote: :56553 2025-03-14 21:03:10 INFO: connection from 127.0.0.1:33382 2025-03-14 21:03:10 INFO: connect to 34.160.111.145:443 2025-03-14 21:03:10 INFO: remote: :56553 ``` ## Изменение конфигурации для доступа с других подов (внутри кластера) Кстати, если нам понадобится внести изменения в конфиг, то можно просто отредактировать файл и применить его снова. Старые данные автоматически заменятся на новые. "Умная" команда `kubectl apply` сравнивает текущий объект в k3s (в `etcd`) с тем, что указан в файле. Если объект уже существует (по `metadata.name` и `namespace`), он обновляется. Если объекта нет, он создаётся. Сейчас `SOCKS5` shadowsocks-контейнера доступен только внутри пода и для других контейнеров в том же поде (если такие контейнеры появятся). Нр для моего проекта нужно чтобы shadowsocks-прокси были доступны из других подов (поды-парсеры поисковика и сборщики данных). Для этого shadowsocks-контейнер должен слушать на `0.0.0.0` (внешний IP). Для этого нужно изменить _local_address_ в конфиге shadowsocks-клиента `config.yaml`: ```yaml … "server_port": <ПОРТ>, # "local_address": "127.0.0.1", "local_address": "0.0.0.0", "local_port": 1081, … ``` Применим конфиг: ```bash sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/config.yaml ``` И обновим под. Обратите внимание, что сам собой под не обновится. Он в памяти, исполняется и никак не может узнать, что конфиг изменился. Поэтому удалиv старый под и Deployment автоматически создаст его заново, но уже с новым конфигом: ```bash sudo k3s kubectl delete pod -n kube-system -l app=shadowsocks-client-moscow --force --grace-period=0 ``` Здесь `-l` — это селектор, который выбирает все поды с меткой `app=shadowsocks-client-moscow`. `--force` и `--grace-period=0` — принудительно удалить под без ожидания завершения работы. ## Создание сервиса для доступа к поду Так как в Kubernetes (и k3s) поды — это временные сущности (они создаются, умирают, перезапускаются, переезжают на другие ноды и тому подобное) их IP-адреса и полные имена (из-за изменения суффиксов) постоянно меняются. Для решения этой проблемы в k3s есть абстракция **Service**. Она позволяет обращаться к подам по имени, а не по IP-адресу. _Service_ предоставляет стабильный IP-адрес (и имя) для доступа к подам, независимо от их текущих IP. Кроме того он обеспечивает Балансировку. Например, если у нас несколько подов с Shadowsocks (_replicas: 3_), _Service_ распределит запросы между ними. Так же, благодаря внутреннему DNS _Service_ позволяет обращаться к поду/подам по имени. Создадим манифест `service.yaml`: ```bash nano ~/k3s/vpn/client-shadowsocks--moscow/service.yaml ``` И вставим в него следующее: ```yaml apiVersion: v1 kind: Service metadata: name: ss-moscow-service namespace: kube-system spec: selector: app: shadowsocks-client-moscow ports: - name: tcp-1081 # Уникальное имя для TCP-порта protocol: TCP port: 1081 targetPort: 1081 - name: udp-1081 # Уникальное имя для UDP-порта protocol: UDP port: 1081 targetPort: 1081 type: ClusterIP ``` Что тут происходит: * `apiVersion: v1` — версия API Kubernetes. * `kind: Service` — это тип способ создать сервис внутри k3s. * `metadata:` — метаданные о сервисе. * `name:` — имя сервиса. * `namespace:` — пространство имен, в котором будет храниться сервис. Мы используем `kube-system`, чтобы сделать его системным. * `spec:` — спецификация сервиса. * `selector:` — селектор, который определяет, какие поды будут обслуживаться этим сервисом. * `app: shadowsocks-client-moscow` — поды с меткой `app=shadowsocks-client-moscow` (из нашего `deployment.yaml`) выше будут обслуживаться этим сервисом. Service автоматически находит все поды с такой меткой даже если их IP или хэш меняются. * `ports:` — порты, которые будут открыты для доступа к подам. * `name:` — уникальное имя для порта. Kubernetes требует `name` для портов в Service, если их больше одного, чтобы избежать путаницы при маршрутизации или логировании. * `protocol:` — протокол (TCP или UDP). * `port:` — порт, на котором будет доступен сервис. * `targetPort:` — порт, на который будет перенаправлен трафик внутри подов. * `type:` — тип сервиса. `ClusterIP` — это внутренний сервис, доступный только внутри кластера. Если нужно сделать его доступным извне, то можно использовать `NodePort` или `LoadBalancer`. В нашем случае `ClusterIP` достаточно. Применим сервис: ```bash sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml ``` Проверим, что сервис создался: ```bash sudo k3s kubectl get service -n kube-system ``` Увидим что-то типа: ```text NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE … ss-moscow-service ClusterIP 10.43.236.81 1081/TCP,1081/UDP 5m5s … ``` Теперь другие поды могут обращаться к `ss-moscow-service.kube-system.svc.cluster.local:1081` как к SOCKS5-прокси. ### Проверим как работает доступ к прокси из другого пода Создай тестовый под: (`test-pod`): ```bash sudo k3s kubectl run -n kube-system test-pod --image=alpine --restart=Never -- sh -c "sleep 3600" ``` Заходим в него: ```bash sudo k3s kubectl exec -it -n kube-system test-pod -- sh ``` Устанавливаем `curl` внутри пода: ```bash apk add curl ``` Проверяем доступ из `test-pod` к прокси на `ss-moscow-service` (не важно, полное имя или короткое): ```bash curl --socks5 ss-moscow-service:1081 http://ifconfig.me curl --socks5 ss-moscow-service.kube-system.svc.cluster.local:1081 http://ifconfig.me exit ``` Увидим, что запросы прошли и мы получили IP-адрес нашего VPS. ## Изменение конфигурации для доступа с хостов домашней сети (внешний доступ, не обязательно) Чтобы прокси был доступен из домашней сети, нужно "вывесить" SOCKS5-прокси изнутри пода наружу. Для этого в Kubernetes тоже можно использовать _Service_. Если использовать тип `NodePort`. NodePort — это тип сервиса в Kubernetes (и k3s), который делает порты пода доступными на всех узлах кластера (nodes) на определённом порту хоста. k3s использует _iptables_ (или _ipvs_) на каждом узле, чтобы перенаправлять трафик с NodePort (с порта IP узла) на внутренний IP пода (в нашем случае -- 10.42.x.x:1081) через CLUSTER-IP. Даже если под "живёт" только на одном узле, трафик с других узлов маршрутизируется к нему по внутренней сети k3s. Откроем `service.yaml` и изменим его: ```yaml apiVersion: v1 kind: Service metadata: name: ss-moscow-service namespace: kube-system spec: selector: app: shadowsocks-client-moscow ports: - name: tcp-1081 # Уникальное имя для TCP-порта protocol: TCP port: 1081 targetPort: 1081 nodePort: 31081 # Порт на хосте (TCP, будет доступен на всех нодах кластера) - name: udp-1081 # Уникальное имя для UDP-порта protocol: UDP port: 1081 targetPort: 1081 nodePort: 31081 # Порт на хосте (UDP, будет доступен на всех нодах кластера) # type: ClusterIP type: NodePort ``` Применим сервис: ```bash sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml ``` Можно, что теперь сервис доступен на любой ноде кластера по порту `31081` (TCP и UDP). Для этого с любого хоста домашней сети можно выполнить: ```bash curl --socks5 :31081 http://ifconfig.me ``` Увидим IP-адрес нашего VPS. ### Досутп из хостов домашней сети к прокси Так как мы уже [настроили Keepalived при установке k3s](../raspberry-and-orange-pi/k3s.md), а socks5-прокси доступен на хосте любого узла кластера, то автоматически socks5-прокси будет доступен и через VIP (виртуальный IP-адрес). Поэтому можно использовать VIP-адрес кластера, а не IP-адрес конкретного узла кластера. Проверим, что это сработало: ```bash curl --socks5 :31081 http://ifconfig.me ```