27 KiB
Создаём под с Shadowsocks
Для каждого VPN-сервера (локации) нужен отдельный клиентский под, который создаст SOCKS5-прокси внутри кластера. Другие поды будут подключаться к тому или иному SOCKS5-прокси в зависимости от их назначения.
Все конфиги и манифесты K3S хранит в etcd
и распространится по всем нодам кластера. Но создавать и вносить изменения
непосредственно в etcd
не удобно. Намного удобнее k3s-конфиги передавать через ConfigMap. К тому же это позволяет
иметь копии конфигов на каком-нибудь хосте и делать резервные копии и восстанавливать их в случае необходимости.
Не принципиально, где хранить конфиги и манифесты, так как после с помощью kubectl
они будут загружены в k3s. Но
лучше хранить их в одном месте, чтобы не искать по всему кластеру, где же они хранятся.
Предлагаемая структура каталогов для хранения конфигураций и манифестов Kubernetes:
~/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-клиента (Москва):
nano ~/k3s/vpn/client-shadowsocks--moscow/config.yaml
И вставляем в него следующее:
apiVersion: v1
kind: ConfigMap
metadata:
name: shadowsocks-client-moscow
namespace: kube-system # Ставим в kube-system, чтобы было системно
data:
config.json: |
{
"server": "<IP_ИЛИ_ИМЯ_СЕРВЕРА>",
"server_port": <ПОРТ>,
"local_address": "127.0.0.1",
"local_port": 1081,
"password": "<PASSWORD_FOR_SHADOWSOCKS_CLIENT>",
"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>@<IP_ИЛИ_ИМЯ_СЕРВЕРА>:<ПОРТ>?type=tcp#<ИМЯ-КЛИЕНТА>
mode: tcp_and_udp
— включает поддержку TCP и UDP.
Применим ConfigMap:
sudo k3s kubectl apply -f /home/<ПОЛЬЗОВАТЕЛЬ>/k3s/vpn/client-shadowsocks--moscow/config.yaml
Важно указывать полный путь к файлу, а не от домашнего каталога ~\
. Запуская kubectl
из под sudo
(или от имени
root
), мы исполняем команды k3s
от имени другого пользователя, а не от имени текущего.
Когда выполним команду, то увидим что-то вроде:
configmap/shadowsocks-config-moscow created
Теперь создадим Deployment
для Shadowsocks-клиента. Создаём файл deployment.yaml
:
nano ~/k3s/vpn/client-shadowsocks--moscow/deployment.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 это иногда нужно).
Применим под:
sudo k3s kubectl apply -f /home/opi/k3s/vpn/client-shadowsocks--moscow/deployment.yaml
Проверка
Проверяем, что под запустился, посмотрев статус:
sudo k3s kubectl get pods -n kube-system
Увидим что-то типа:
NAME READY STATUS RESTARTS AGE
…
…
shadowsocks-client-moscow-54d64bf5f4-trb6p 1/1 Running 0 24m
…
Можно проверь логи:
sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-54d64bf5f4-trb6p
Увидим, что клиент shadowsocks запустился:
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):
nano k3s/vpn/client-shadowsocks--moscow/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оберём образ:
sudo docker build -t shadowsocks-with-tools:latest ~/k3s/vpn/client-shadowsocks--moscow/
Увидим, что образ собрался:
[+] 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):
sudo docker save shadowsocks-with-tools:latest | sudo k3s ctr images import -
Здесь:
docker save
— экспортирует образ в tar-формат.k3s ctr
— вызывает ctr внутри k3s.images import -
— импортирует образ из stdin.
Увидим что-то вроде:
unpacking docker.io/library/shadowsocks-with-tools:latest (sha256:ae615618ce9d2aac7d3764ef735108452adf3fc30bb65f23f28c345798880c80)...done
Проверим, что образ появился в k3s:
sudo k3s ctr images ls | grep shadowsocks
Увидим что-то типа:
…
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"
Когда наш контейнер окажется на всех нодах, изменим deployment.yaml
Shadowsocks-клиента, чтобы использовать наш
новый образ. Закомментируем строку image: shadowsocks/shadowsocks-libev:latest
и вставим две строки после неё
(обратите внимание на заметки):
…
spec:
containers:
- name: shadowsocks-client
# image: shadowsocks/shadowsocks-libev:latest
image: shadowsocks-with-tools # Без :latest, чтобы k3s не "ходил" за контейнером в реестр (например, DockerHub)
imagePullPolicy: Never # Только локальный образ, не тянуть из реестра
…
…
Уберём старый под из deployment и удалим сам под из k3s:
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
Запустим новый под с нашим новым образом:
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--v/deployment.yaml
Проверим, что под запустился, посмотрев статус:
sudo k3s kubectl get pods -n kube-system
Увидим что-то типа:
NAME READY STATUS RESTARTS AGE
…
shadowsocks-client-moscow-6cf7b956b8-mtsg4 1/1 Running 0 9s
…
Проверка работы Shadowsocks
Посмотрим логи пода с Shadowsocks-клиентом:
sudo k3s kubectl logs -n kube-system -l app=shadowsocks-client-moscow
Увидим, что клиент shadowsocks запустился:
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-соединение. Зайдём в под:
sudo k3s kubectl exec -it -n kube-system shadowsocks-client-moscow-<hash> -- sh
И выполним внутри пода команду:
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. Ожидаемый результат: <VPS_IP>
(IP-адрес нашего VPS).
Выходим из пода:
exit
Проверим логи еще раз:
sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-<hash>
Увидим, что клиент shadowsocks отработал:
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: <VPS_IP>: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: <VPS_IP>:56553
Изменение конфигурации для доступа с других подов (внутри кластера)
Кстати, если нам понадобится внести изменения в конфиг, то можно просто отредактировать файл и применить его снова.
Старые данные автоматически заменятся на новые. "Умная" команда kubectl apply
сравнивает текущий объект в k3s
(в etcd
) с тем, что указан в файле. Если объект уже существует (по metadata.name
и namespace
), он обновляется.
Если объекта нет, он создаётся.
Сейчас SOCKS5
shadowsocks-контейнера доступен только внутри пода и для других контейнеров в том же поде (если
такие контейнеры появятся). Нр для моего проекта нужно чтобы shadowsocks-прокси были доступны из других подов
(поды-парсеры поисковика и сборщики данных). Для этого shadowsocks-контейнер должен слушать на 0.0.0.0
(внешний IP).
Для этого нужно изменить local_address в конфиге shadowsocks-клиента config.yaml
:
…
"server_port": <ПОРТ>,
# "local_address": "127.0.0.1",
"local_address": "0.0.0.0",
"local_port": 1081,
…
Применим конфиг:
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/config.yaml
И обновим под. Обратите внимание, что сам собой под не обновится. Он в памяти, исполняется и никак не может узнать, что конфиг изменился. Поэтому удалиv старый под и Deployment автоматически создаст его заново, но уже с новым конфигом:
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
:
nano ~/k3s/vpn/client-shadowsocks--moscow/service.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
достаточно.
Применим сервис:
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml
Проверим, что сервис создался:
sudo k3s kubectl get service -n kube-system
Увидим что-то типа:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
…
ss-moscow-service ClusterIP 10.43.236.81 <none> 1081/TCP,1081/UDP 5m5s
…
Теперь другие поды могут обращаться к ss-moscow-service.kube-system.svc.cluster.local:1081
как к SOCKS5-прокси.
Проверим как работает доступ к прокси из другого пода
Создай тестовый под: (test-pod
):
sudo k3s kubectl run -n kube-system test-pod --image=alpine --restart=Never -- sh -c "sleep 3600"
Заходим в него:
sudo k3s kubectl exec -it -n kube-system test-pod -- sh
Устанавливаем curl
внутри пода:
apk add curl
Проверяем доступ из test-pod
к прокси на ss-moscow-service
(не важно, полное имя или короткое):
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
и изменим его:
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
Применим сервис:
sudo k3s kubectl apply -f ~/k3s/vpn/client-shadowsocks--moscow/service.yaml
Можно, что теперь сервис доступен на любой ноде кластера по порту 31081
(TCP и UDP). Для этого с любого хоста
домашней сети можно выполнить:
curl --socks5 <IP_УЗЛА>:31081 http://ifconfig.me
Увидим IP-адрес нашего VPS.
Досутп из хостов домашней сети к прокси
Так как мы уже настроили Keepalived при установке k3s, а socks5-прокси доступен на хосте любого узла кластера, то автоматически socks5-прокси будет доступен и через VIP (виртуальный IP-адрес). Поэтому можно использовать VIP-адрес кластера, а не IP-адрес конкретного узла кластера.
Проверим, что это сработало:
curl --socks5 <VIP>:31081 http://ifconfig.me