# Создаём под с 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": "<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:
```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-<hash> -- 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. Ожидаемый результат: `<VPS_IP>` (IP-адрес нашего VPS).

Выходим из пода:
```bash
exit
```

Проверим логи еще раз:
```bash
sudo k3s kubectl logs -n kube-system shadowsocks-client-moscow-<hash>
```

Увидим, что клиент 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: <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`:

```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    <none>                                   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 <IP_УЗЛА>:31081 http://ifconfig.me
```

Увидим IP-адрес нашего VPS.

### Досутп из хостов домашней сети к прокси

Так как мы уже [настроили Keepalived при установке k3s](../raspberry-and-orange-pi/k3s.md), а socks5-прокси
доступен на хосте любого узла кластера, то автоматически socks5-прокси будет доступен и через VIP
(виртуальный IP-адрес). Поэтому можно использовать VIP-адрес кластера, а не IP-адрес конкретного узла кластера.

Проверим, что это сработало:
```bash
curl --socks5 <VIP>:31081 http://ifconfig.me
```