diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..3e7dafd --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +OUTLINE_TRANSPORT=ss://secret@IP:PORT/?outline=1&prefix=POST%20 +SERVERURL=PUBLIC_IP +PEERS=3 +INTERNAL_SUBNET=10.13.13.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6d334b1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +* text=auto +*.sh text eol=lf +Dockerfile text eol=lf +docker-compose.yml text eol=lf +root/etc/** text eol=lf +root/defaults/** text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..01504de --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +wireguard-config +wireguard-config/peer* +wireguard-config/wg0.conf + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4229b72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +FROM golang:1.24-bookworm AS builder + +RUN apt-get update && apt-get install -y git + +# Клонируем репозиторий и собираем приложение +WORKDIR /outline-sdk +RUN git clone https://github.com/OutlineFoundation/outline-sdk.git . +WORKDIR /outline-sdk/x/examples/outline-cli + +RUN CGO_ENABLED=1 go build -o /outline-cli -ldflags="-extldflags=-static" . + +FROM debian:bookworm +RUN apt-get update && apt-get install -y curl wget +# Устанавливаем необходимые утилиты для работы сети и создаем пустой resolv.conf.head +RUN apt-get update && apt-get install -y iproute2 iptables && \ + rm -rf /var/lib/apt/lists/* && \ + touch /etc/resolv.conf.head + +# Копируем собранный бинарный файл из этапа сборки +COPY --from=builder /outline-cli /usr/local/bin/outline-cli + +# Обертка-энтрипоинт: готовит DNS и запускает outline-cli +COPY entrypoint.sh /usr/local/bin/outline-entrypoint.sh + +# Делаем бинарный файл исполняемым +RUN chmod +x /usr/local/bin/outline-cli /usr/local/bin/outline-entrypoint.sh + +# Точка входа +ENTRYPOINT ["/usr/local/bin/outline-entrypoint.sh"] + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..285bae0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +services: + outline-client: + build: . + container_name: outline-client + privileged: true + # WireGuard UDP port is published from this service because + # the wireguard container shares this network namespace. + ports: + - "51821:51820/udp" + environment: + # Replace with your Outline access key. + OUTLINE_TRANSPORT: "${OUTLINE_TRANSPORT}" + INTERNAL_SUBNET: "${INTERNAL_SUBNET}" + cap_add: + - NET_ADMIN + - SYS_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + security_opt: + - apparmor:unconfined + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.conf.all.rp_filter=0 + - net.ipv4.conf.default.rp_filter=0 + - net.ipv6.conf.all.forwarding=0 + - net.ipv6.conf.all.disable_ipv6=1 + - net.ipv6.conf.default.disable_ipv6=1 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" + + wireguard: + image: lscr.io/linuxserver/wireguard:latest + container_name: wg2outline + depends_on: + - outline-client + # Important: share network stack with outline-client so all + # traffic from WG peers leaves through Outline routing/policy. + network_mode: "service:outline-client" + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - PUID=1000 + - PGID=1000 + - TZ=Etc/UTC + # Public IP or domain of this server for peers. + - SERVERURL=${SERVERURL} + - SERVERPORT=51821 + - PEERS=${PEERS:-1} + - PEERDNS=1.1.1.1 + - INTERNAL_SUBNET=${INTERNAL_SUBNET} + # Keepalive helps on NAT/mobile networks. + - PERSISTENTKEEPALIVE_PEERS=25 + # Optional for enterprise networks that block MTU/fragmentation. + - MTU=1280 + # Firewall/NAT is handled by the host/namespace setup. + - LOG_CONFS=true + volumes: + - ./wireguard-config:/config + - /lib/modules:/lib/modules:ro + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + restart: unless-stopped + logging: + driver: json-file + options: + max-size: "50m" + max-file: "5" diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d019c72 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -eu + +if [ -z "${OUTLINE_TRANSPORT:-}" ]; then + echo "ERROR: OUTLINE_TRANSPORT is required (ss://...)" >&2 + exit 1 +fi + +# Some distros mount /etc/resolv.conf as a file from the host. +# We want to ensure DNS works reliably inside the VPN network namespace. +umount /etc/resolv.conf 2>/dev/null || true +[ -f /etc/resolv.conf ] || printf 'nameserver 1.1.1.1\n' > /etc/resolv.conf + +/usr/local/bin/outline-cli -transport "${OUTLINE_TRANSPORT}" & +OUTLINE_PID=$! + +# Allow local Docker and WireGuard subnets to bypass Outline policy routing. +# Without this, reply packets to WG peers can be forced into table 233. +for i in 1 2 3 4 5; do + if ip rule show | grep -q "table 233"; then + break + fi + sleep 1 +done + +WG_SUBNET="${INTERNAL_SUBNET}/24" +ip rule add pref 100 to "${WG_SUBNET}" lookup main 2>/dev/null || true + +# Docker/custom bridge subnets differ per host (e.g. 172.17.0.0/16, 10.200.0.0/16). +pref=101 +for cidr in $(ip -4 route show proto kernel scope link 2>/dev/null | awk '$2 == "dev" { print $1 }'); do + ip rule add pref "${pref}" to "${cidr}" lookup main 2>/dev/null || true + pref=$((pref + 1)) +done + +# Ensure exactly one NAT rule for traffic leaving via Outline interface. +if iptables -t nat -C POSTROUTING -o outline+ -j MASQUERADE 2>/dev/null; then + while iptables -t nat -C POSTROUTING -o outline+ -j MASQUERADE 2>/dev/null; do + iptables -t nat -D POSTROUTING -o outline+ -j MASQUERADE || true + done +fi +iptables -t nat -A POSTROUTING -o outline+ -j MASQUERADE + +wait "$OUTLINE_PID" + diff --git a/test-wg-client.sh b/test-wg-client.sh new file mode 100755 index 0000000..f88c174 --- /dev/null +++ b/test-wg-client.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Usage: +# ./test-wg-client.sh [path-to-peer-conf] +# +# Example: +# ./test-wg-client.sh ./wireguard-config/peer1/peer1.conf + +PEER_CONF="${1:-./wireguard-config/peer2/peer2.conf}" +CLIENT_NAME="${WG_TEST_CONTAINER_NAME:-wg-test-client}" +WAIT_SECONDS="${WG_TEST_WAIT_SECONDS:-15}" + +if [[ ! -f "${PEER_CONF}" ]]; then + echo "ERROR: peer config not found: ${PEER_CONF}" >&2 + exit 1 +fi + +cleanup() { + docker rm -f "${CLIENT_NAME}" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +echo "==> Preparing clean test container: ${CLIENT_NAME}" +cleanup + +echo "==> Starting temporary WireGuard client" +docker run -d \ + --name "${CLIENT_NAME}" \ + --privileged \ + --cap-add NET_ADMIN \ + --cap-add SYS_MODULE \ + --device /dev/net/tun:/dev/net/tun \ + -v "$(realpath "${PEER_CONF}"):/etc/wireguard/wg0.conf:ro" \ + --entrypoint sh \ + alpine:3.20 \ + -c "apk add --no-cache wireguard-tools iproute2 iptables ip6tables openresolv curl >/dev/null && wg-quick up wg0 && tail -f /dev/null" \ + >/dev/null + +echo "==> Waiting ${WAIT_SECONDS}s for tunnel to initialize" +sleep "${WAIT_SECONDS}" + +if [[ "$(docker inspect -f '{{.State.Running}}' "${CLIENT_NAME}")" != "true" ]]; then + echo "ERROR: test client container exited before tunnel check" >&2 + echo "==> Last container logs:" >&2 + docker logs "${CLIENT_NAME}" >&2 || true + exit 1 +fi + +echo "==> WireGuard status inside client" +docker exec "${CLIENT_NAME}" wg show || true + +echo "==> Test request through client namespace: https://web.telegram.org/" +docker run --rm \ + --network=container:"${CLIENT_NAME}" \ + curlimages/curl:8.7.1 \ + -sS -I --max-time 20 https://web.telegram.org/ + +echo "==> External IP seen via WG client namespace" +docker run --rm \ + --network=container:"${CLIENT_NAME}" \ + curlimages/curl:8.7.1 \ + -sS --max-time 20 https://ifconfig.me +echo + +echo "==> Test completed successfully" diff --git a/wireguard-config/templates/peer.conf b/wireguard-config/templates/peer.conf new file mode 100644 index 0000000..d987dba --- /dev/null +++ b/wireguard-config/templates/peer.conf @@ -0,0 +1,11 @@ +[Interface] +Address = ${CLIENT_IP} +PrivateKey = $(cat /config/${PEER_ID}/privatekey-${PEER_ID}) +ListenPort = 51820 +DNS = ${PEERDNS} + +[Peer] +PublicKey = $(cat /config/server/publickey-server) +PresharedKey = $(cat /config/${PEER_ID}/presharedkey-${PEER_ID}) +Endpoint = ${SERVERURL}:${SERVERPORT} +AllowedIPs = ${ALLOWEDIPS} diff --git a/wireguard-config/templates/server.conf b/wireguard-config/templates/server.conf new file mode 100644 index 0000000..757682d --- /dev/null +++ b/wireguard-config/templates/server.conf @@ -0,0 +1,6 @@ +[Interface] +Address = ${INTERFACE}.1 +ListenPort = 51820 +PrivateKey = $(cat /config/server/privatekey-server) +PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth+ -j MASQUERADE +PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth+ -j MASQUERADE