Skip to content

Instantly share code, notes, and snippets.

@dvygolov
Last active May 28, 2026 18:01
Show Gist options
  • Select an option

  • Save dvygolov/807b6e23c35b4387d4f547bccbf6615a to your computer and use it in GitHub Desktop.

Select an option

Save dvygolov/807b6e23c35b4387d4f547bccbf6615a to your computer and use it in GitHub Desktop.
YWB universal IPv6 proxy installer for Debian and Ubuntu

YWB Universal IPv6 Proxy Installer

This installer targets Debian and Ubuntu servers where:

  • the primary IPv6 connectivity already works;
  • you have either a routed IPv6 CIDR, a ready list of IPv6 addresses, or preconfigured IPv6 addresses on an interface;
  • you want HTTP and/or SOCKS5 proxies on top of 3proxy.

Tested matrix at this point:

  • Debian 12
  • Debian 13
  • Ubuntu 22.04
  • Ubuntu 24.04

Actions

  • install: install or re-apply the YWB-managed proxy setup.
  • show: print the current generated proxy list without reinstalling.
  • uninstall: remove the YWB-managed proxy setup, but leave the installed 3proxy package in place.

The installer can also run in interactive wizard mode:

  • --interactive: ask questions step by step instead of requiring you to edit example.env or type the full command by hand.

Modes

  • existing: use the global IPv6 addresses that already exist on the interface.
  • cidr: generate sequential IPv6 addresses from a given CIDR.
  • list: read IPv6 addresses from a file.

What the installer does

  • installs 3proxy from the official GitHub release package when possible;
  • falls back to building 3proxy from source on unsupported architectures;
  • writes a dedicated 3proxy configuration to /etc/ywb-ipv6-proxy;
  • creates a systemd service for 3proxy;
  • for cidr and list mode, creates a oneshot systemd service that re-adds IPv6 addresses on boot;
  • writes ready-to-use proxy lists to /root/ywb-ipv6-proxy;
  • runs a small self-test through the first proxies after installation.

Quick start: one-line interactive wizard

tmp=$(mktemp) && curl -fsSL https://gist.githubusercontent.com/dvygolov/807b6e23c35b4387d4f547bccbf6615a/raw/install.sh -o "$tmp" && bash "$tmp" --interactive; status=$?; rm -f "$tmp"; test $status -eq 0

This starts the wizard immediately and asks for:

  • action (install, show, uninstall);
  • mode (existing, cidr, list);
  • interface, proxy host, login, password mode;
  • protocol toggles (HTTP, SOCKS5);
  • ports, counts, CIDR or list file;
  • self-test and firewall options.

In the common case you can just press Enter for most of the wizard:

  • interface defaults to auto and is auto-detected from the default route;
  • proxy host defaults to auto and is auto-detected from the interface public IPv4;
  • HTTP starts at 30000;
  • SOCKS5 starts at 40000;
  • self-test count defaults to 2;
  • UFW management defaults to no.

In existing mode, the wizard also tries to detect how many global IPv6 addresses already exist on the interface and offers that number as the default proxy count.

Quick start: local files

cp example.env my.env
nano my.env
sudo ./install.sh --action install --config my.env

Quick start: local interactive wizard

sudo ./install.sh --interactive

Example: routed /64

sudo ./install.sh \
  --action install \
  --mode cidr \
  --interface eth0 \
  --login yellow \
  --password 'strong-pass' \
  --ipv6-cidr '2a01:4f8:abcd::/64' \
  --proxy-count 1000 \
  --ipv6-start-offset 1 \
  --assigned-prefixlen 64 \
  --http-port-start 30000 \
  --socks-port-start 40000

Example: use preconfigured addresses

sudo ./install.sh \
  --action install \
  --mode existing \
  --interface eth0 \
  --login yellow \
  --password 'strong-pass' \
  --proxy-count 15

Example: show current list

sudo ./install.sh --action show --show-file combined
sudo ./install.sh --action show --show-file http-urls

Example: uninstall

sudo ./install.sh --action uninstall

Output files

  • /root/ywb-ipv6-proxy/proxies-http.txt
  • /root/ywb-ipv6-proxy/proxies-http-urls.txt
  • /root/ywb-ipv6-proxy/proxies-socks5.txt
  • /root/ywb-ipv6-proxy/proxies-socks5-urls.txt
  • /root/ywb-ipv6-proxy/proxies-combined.csv

Notes

  • ASSIGNED_PREFIXLEN is separate from the generator CIDR. This matters on providers that only allow a smaller usable block but still expect addresses to be added with /64.
  • By default the installer does not touch your firewall. If UFW is already active, set MANAGE_FIREWALL=1.
  • The self-test uses https://ifconfig.co and validates that each tested proxy exits through the expected IPv6 address.
# Common
ACTION="install"
MODE="cidr"
INTERFACE="eth0"
PROXY_HOST=""
PROXY_LOGIN="yellow"
PROXY_PASSWORD="change-me-now"
PROTOCOLS="http,socks5"
HTTP_PORT_START="30000"
SOCKS_PORT_START="40000"
PROXY_COUNT="100"
SELF_TEST_COUNT="2"
MANAGE_FIREWALL="0"
# CIDR mode
IPV6_CIDR="2a01:4f8:abcd::/64"
IPV6_START_OFFSET="1"
# list mode
# MODE="list"
# IPV6_LIST_FILE="/root/ipv6-addresses.txt"
# existing mode
# MODE="existing"
# Addresses are usually added to the interface with /64 even when the usable block is smaller.
ASSIGNED_PREFIXLEN="64"
#!/usr/bin/env bash
set -euo pipefail
INSTALLER_VERSION="0.2.8"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE=""
ACTION="install"
MODE="existing"
INTERFACE="auto"
PROXY_HOST=""
PROXY_LOGIN="proxy"
PROXY_PASSWORD=""
PROTOCOLS="http,socks5"
HTTP_PORT_START="30000"
SOCKS_PORT_START="40000"
PROXY_COUNT="10"
IPV6_CIDR=""
IPV6_LIST_FILE=""
IPV6_START_OFFSET="1"
ASSIGNED_PREFIXLEN="64"
THREEPROXY_VERSION="0.9.6"
CONFIG_DIR="/etc/ywb-ipv6-proxy"
STATE_DIR="/var/lib/ywb-ipv6-proxy"
OUTPUT_DIR="/root/ywb-ipv6-proxy"
LOG_DIR="/var/log/ywb-ipv6-proxy"
MANAGE_FIREWALL="0"
SELF_TEST_COUNT="2"
SHOW_FILE="combined"
INTERACTIVE="0"
PROMPT_INPUT=""
PROMPT_OUTPUT=""
usage() {
cat <<'EOF'
Usage:
sudo ./install.sh [--config example.env] [options]
sudo ./install.sh --interactive
One-line interactive start:
tmp=$(mktemp) && curl -fsSL https://gist.githubusercontent.com/dvygolov/807b6e23c35b4387d4f547bccbf6615a/raw/install.sh -o "$tmp" && bash "$tmp" --interactive; status=$?; rm -f "$tmp"; test $status -eq 0
Action:
--action install Install or re-apply the proxy setup.
--action uninstall Remove the YWB-managed proxy setup.
--action show Print the current proxy list without reinstalling.
--interactive Start the interactive setup wizard.
Required inputs for install:
--login USER
--password PASS
Address source for install:
--mode existing Use already configured global IPv6 addresses on the interface.
--mode cidr Generate sequential IPv6 addresses from --ipv6-cidr.
--mode list Read IPv6 addresses from --ipv6-list-file.
Common options:
--config FILE
--interface IFACE
--proxy-host HOST
--protocols http,socks5
--http-port-start 30000
--socks-port-start 40000
--self-test-count 2
--manage-firewall 1
--show-file combined
CIDR mode:
--ipv6-cidr 2a01:4f8:abcd::/64
--proxy-count 100
--ipv6-start-offset 1
--assigned-prefixlen 64
List mode:
--ipv6-list-file ./addresses.txt
--assigned-prefixlen 64
Example:
sudo ./install.sh \
--action install \
--mode cidr \
--interface eth0 \
--login yellow \
--password 'strong-pass' \
--ipv6-cidr '2a01:4f8:abcd::/64' \
--proxy-count 1000 \
--http-port-start 30000 \
--socks-port-start 40000
Show current proxy list:
sudo ./install.sh --action show --show-file combined
Interactive wizard:
sudo ./install.sh --interactive
tmp=$(mktemp) && curl -fsSL https://gist.githubusercontent.com/dvygolov/807b6e23c35b4387d4f547bccbf6615a/raw/install.sh -o "$tmp" && bash "$tmp" --interactive; status=$?; rm -f "$tmp"; test $status -eq 0
Uninstall:
sudo ./install.sh --action uninstall
EOF
}
log() {
printf '[*] %s\n' "$*"
}
die() {
printf '[!] %s\n' "$*" >&2
exit 1
}
require_root() {
[[ "${EUID}" -eq 0 ]] || die "Run this installer as root."
}
require_supported_os() {
[[ -r /etc/os-release ]] || die "/etc/os-release is missing."
# shellcheck disable=SC1091
source /etc/os-release
case "${ID:-}" in
debian|ubuntu) ;;
*) die "Supported only on Debian/Ubuntu. Detected: ${ID:-unknown}" ;;
esac
}
random_password() {
local password=""
password="$(
set +o pipefail
LC_ALL=C tr -dc 'A-Za-z0-9._-' </dev/urandom | head -c 20
)"
printf '%s' "${password}"
}
normalize_yes_no() {
local value="${1,,}"
case "${value}" in
y|yes|1|true) echo "1" ;;
n|no|0|false) echo "0" ;;
*) die "Expected yes/no value, got: ${1}" ;;
esac
}
init_prompt_io() {
if [[ -t 0 ]]; then
PROMPT_INPUT="/dev/stdin"
PROMPT_OUTPUT="/dev/stderr"
return
fi
if [[ -r /dev/tty && -w /dev/tty ]]; then
PROMPT_INPUT="/dev/tty"
PROMPT_OUTPUT="/dev/tty"
return
fi
die "Interactive mode requires a TTY."
}
prompt_with_default() {
local __var_name="$1"
local prompt_text="$2"
local default_value="${3-}"
local answer=""
while true; do
if [[ -n "${default_value}" ]]; then
printf '%s [%s]: ' "${prompt_text}" "${default_value}" > "${PROMPT_OUTPUT}"
else
printf '%s: ' "${prompt_text}" > "${PROMPT_OUTPUT}"
fi
IFS= read -r answer < "${PROMPT_INPUT}"
if [[ -z "${answer}" ]]; then
answer="${default_value}"
fi
[[ -n "${answer}" ]] && break
done
printf -v "${__var_name}" '%s' "${answer}"
}
prompt_secret() {
local __var_name="$1"
local prompt_text="$2"
local answer=""
while true; do
printf '%s: ' "${prompt_text}" > "${PROMPT_OUTPUT}"
IFS= read -r -s answer < "${PROMPT_INPUT}"
printf '\n' > "${PROMPT_OUTPUT}"
[[ -n "${answer}" ]] && break
done
printf -v "${__var_name}" '%s' "${answer}"
}
prompt_yes_no() {
local __var_name="$1"
local prompt_text="$2"
local default_value="${3:-1}"
local hint="Y/n"
local answer=""
if [[ "${default_value}" == "0" ]]; then
hint="y/N"
fi
while true; do
printf '%s [%s]: ' "${prompt_text}" "${hint}" > "${PROMPT_OUTPUT}"
IFS= read -r answer < "${PROMPT_INPUT}"
answer="${answer,,}"
if [[ -z "${answer}" ]]; then
answer="${default_value}"
fi
case "${answer}" in
y|yes|1|true)
printf -v "${__var_name}" '%s' "1"
return
;;
n|no|0|false)
printf -v "${__var_name}" '%s' "0"
return
;;
esac
done
}
prompt_choice() {
local __var_name="$1"
local prompt_text="$2"
local default_value="$3"
shift 3
local options=("$@")
local answer=""
local matched=""
local option=""
while true; do
printf '%s [%s]: ' "${prompt_text}" "${default_value}" > "${PROMPT_OUTPUT}"
IFS= read -r answer < "${PROMPT_INPUT}"
if [[ -z "${answer}" ]]; then
answer="${default_value}"
fi
matched=""
for option in "${options[@]}"; do
if [[ "${answer}" == "${option}" ]]; then
matched="${option}"
break
fi
done
if [[ -n "${matched}" ]]; then
printf -v "${__var_name}" '%s' "${matched}"
return
fi
done
}
configure_protocols_interactively() {
local enable_http="1"
local enable_socks5="1"
if [[ ",${PROTOCOLS}," != *",http,"* ]]; then
enable_http="0"
fi
if [[ ",${PROTOCOLS}," != *",socks5,"* ]]; then
enable_socks5="0"
fi
while true; do
prompt_yes_no enable_http "Enable HTTP proxies?" "${enable_http}"
prompt_yes_no enable_socks5 "Enable SOCKS5 proxies?" "${enable_socks5}"
if [[ "${enable_http}" == "1" || "${enable_socks5}" == "1" ]]; then
break
fi
printf 'Select at least one protocol.\n' > "${PROMPT_OUTPUT}"
done
PROTOCOLS=""
if [[ "${enable_http}" == "1" ]]; then
PROTOCOLS="http"
fi
if [[ "${enable_socks5}" == "1" ]]; then
if [[ -n "${PROTOCOLS}" ]]; then
PROTOCOLS="${PROTOCOLS},socks5"
else
PROTOCOLS="socks5"
fi
fi
}
interactive_configure() {
local password_mode="generate"
local confirm_install="1"
local confirm_uninstall="0"
local detected_interface=""
local count_default=""
local count_iface=""
local detected_count=""
init_prompt_io
printf '\nYWB IPv6 proxy installer %s\n\n' "${INSTALLER_VERSION}" > "${PROMPT_OUTPUT}"
prompt_choice ACTION "Action (install/show/uninstall)" "${ACTION}" install show uninstall
case "${ACTION}" in
show)
prompt_choice SHOW_FILE "Which list to show? (combined/http/http-urls/socks5/socks5-urls)" "${SHOW_FILE}" combined http http-urls socks5 socks5-urls
return
;;
uninstall)
prompt_yes_no confirm_uninstall "Remove the current YWB-managed setup?" "0"
[[ "${confirm_uninstall}" == "1" ]] || die "Uninstall cancelled."
return
;;
esac
prompt_choice MODE "Address source mode (existing/cidr/list)" "${MODE}" existing cidr list
if [[ "${INTERFACE}" == "auto" ]]; then
detected_interface="$(detect_interface_value 2>/dev/null || true)"
fi
if [[ -n "${detected_interface}" ]]; then
printf 'Auto-detected interface: %s\n' "${detected_interface}" > "${PROMPT_OUTPUT}"
fi
prompt_with_default INTERFACE "Network interface (Enter for auto-detect)" "${INTERFACE}"
prompt_with_default PROXY_HOST "Proxy host for clients (Enter for auto-detect public IPv4)" "${PROXY_HOST:-auto}"
[[ "${PROXY_HOST}" == "auto" ]] && PROXY_HOST=""
prompt_with_default PROXY_LOGIN "Proxy login" "${PROXY_LOGIN}"
if [[ -n "${PROXY_PASSWORD}" ]]; then
password_mode="manual"
fi
prompt_choice password_mode "Password mode (generate/manual)" "${password_mode}" generate manual
if [[ "${password_mode}" == "generate" ]]; then
PROXY_PASSWORD=""
else
prompt_secret PROXY_PASSWORD "Proxy password"
fi
configure_protocols_interactively
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
prompt_with_default HTTP_PORT_START "HTTP start port" "${HTTP_PORT_START}"
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
prompt_with_default SOCKS_PORT_START "SOCKS5 start port" "${SOCKS_PORT_START}"
fi
case "${MODE}" in
existing)
count_default="${PROXY_COUNT}"
count_iface="${INTERFACE}"
if [[ "${count_iface}" == "auto" ]]; then
count_iface="${detected_interface}"
fi
if [[ -n "${count_iface}" ]]; then
detected_count="$(count_existing_ipv6_on_interface "${count_iface}" 2>/dev/null || true)"
if [[ "${detected_count}" =~ ^[0-9]+$ ]] && [[ "${detected_count}" -gt 0 ]]; then
count_default="${detected_count}"
printf 'Found %s existing global IPv6 addresses on %s\n' "${detected_count}" "${count_iface}" > "${PROMPT_OUTPUT}"
fi
fi
prompt_with_default PROXY_COUNT "How many existing IPv6 addresses to use" "${count_default}"
;;
cidr)
prompt_with_default IPV6_CIDR "IPv6 CIDR to generate from" "${IPV6_CIDR}"
prompt_with_default PROXY_COUNT "How many IPv6 addresses to generate" "${PROXY_COUNT}"
prompt_with_default IPV6_START_OFFSET "Start offset inside CIDR" "${IPV6_START_OFFSET}"
prompt_with_default ASSIGNED_PREFIXLEN "Prefix length for addresses added on the interface" "${ASSIGNED_PREFIXLEN}"
;;
list)
prompt_with_default IPV6_LIST_FILE "Path to the IPv6 list file" "${IPV6_LIST_FILE}"
prompt_with_default ASSIGNED_PREFIXLEN "Prefix length for addresses added on the interface" "${ASSIGNED_PREFIXLEN}"
;;
esac
prompt_with_default SELF_TEST_COUNT "How many first addresses to self-test" "${SELF_TEST_COUNT}"
prompt_yes_no MANAGE_FIREWALL "Manage UFW rules automatically?" "$(normalize_yes_no "${MANAGE_FIREWALL}")"
printf '\nSummary:\n' > "${PROMPT_OUTPUT}"
printf ' Action: %s\n' "${ACTION}" > "${PROMPT_OUTPUT}"
printf ' Mode: %s\n' "${MODE}" > "${PROMPT_OUTPUT}"
printf ' Interface: %s\n' "${INTERFACE}" > "${PROMPT_OUTPUT}"
printf ' Proxy host: %s\n' "${PROXY_HOST:-auto}" > "${PROMPT_OUTPUT}"
printf ' Login: %s\n' "${PROXY_LOGIN}" > "${PROMPT_OUTPUT}"
printf ' Protocols: %s\n' "${PROTOCOLS}" > "${PROMPT_OUTPUT}"
if [[ "${MODE}" == "cidr" ]]; then
printf ' IPv6 CIDR: %s\n' "${IPV6_CIDR}" > "${PROMPT_OUTPUT}"
fi
if [[ "${MODE}" == "list" ]]; then
printf ' IPv6 list file: %s\n' "${IPV6_LIST_FILE}" > "${PROMPT_OUTPUT}"
fi
printf ' Firewall via UFW: %s\n' "$( [[ "${MANAGE_FIREWALL}" == "1" ]] && echo yes || echo no )" > "${PROMPT_OUTPUT}"
printf '\n' > "${PROMPT_OUTPUT}"
prompt_yes_no confirm_install "Start installation with these settings?" "1"
[[ "${confirm_install}" == "1" ]] || die "Installation cancelled."
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--config)
CONFIG_FILE="${2:?missing value for --config}"
shift 2
;;
--action)
ACTION="${2:?missing value for --action}"
shift 2
;;
--mode)
MODE="${2:?missing value for --mode}"
shift 2
;;
--interface)
INTERFACE="${2:?missing value for --interface}"
shift 2
;;
--proxy-host)
PROXY_HOST="${2:?missing value for --proxy-host}"
shift 2
;;
--login)
PROXY_LOGIN="${2:?missing value for --login}"
shift 2
;;
--password)
PROXY_PASSWORD="${2:?missing value for --password}"
shift 2
;;
--protocols)
PROTOCOLS="${2:?missing value for --protocols}"
shift 2
;;
--http-port-start)
HTTP_PORT_START="${2:?missing value for --http-port-start}"
shift 2
;;
--socks-port-start)
SOCKS_PORT_START="${2:?missing value for --socks-port-start}"
shift 2
;;
--proxy-count)
PROXY_COUNT="${2:?missing value for --proxy-count}"
shift 2
;;
--ipv6-cidr)
IPV6_CIDR="${2:?missing value for --ipv6-cidr}"
shift 2
;;
--ipv6-list-file)
IPV6_LIST_FILE="${2:?missing value for --ipv6-list-file}"
shift 2
;;
--ipv6-start-offset)
IPV6_START_OFFSET="${2:?missing value for --ipv6-start-offset}"
shift 2
;;
--assigned-prefixlen)
ASSIGNED_PREFIXLEN="${2:?missing value for --assigned-prefixlen}"
shift 2
;;
--3proxy-version)
THREEPROXY_VERSION="${2:?missing value for --3proxy-version}"
shift 2
;;
--config-dir)
CONFIG_DIR="${2:?missing value for --config-dir}"
shift 2
;;
--state-dir)
STATE_DIR="${2:?missing value for --state-dir}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:?missing value for --output-dir}"
shift 2
;;
--log-dir)
LOG_DIR="${2:?missing value for --log-dir}"
shift 2
;;
--manage-firewall)
MANAGE_FIREWALL="${2:?missing value for --manage-firewall}"
shift 2
;;
--self-test-count)
SELF_TEST_COUNT="${2:?missing value for --self-test-count}"
shift 2
;;
--show-file)
SHOW_FILE="${2:?missing value for --show-file}"
shift 2
;;
--interactive)
INTERACTIVE="1"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
}
load_config_if_needed() {
if [[ -n "${CONFIG_FILE}" ]]; then
[[ -f "${CONFIG_FILE}" ]] || die "Config file not found: ${CONFIG_FILE}"
# shellcheck disable=SC1090
source "${CONFIG_FILE}"
fi
}
validate_inputs() {
case "${ACTION}" in
install|uninstall|show) ;;
*) die "--action must be one of: install, uninstall, show" ;;
esac
case "${SHOW_FILE}" in
combined|http|http-urls|socks5|socks5-urls) ;;
*) die "--show-file must be one of: combined, http, http-urls, socks5, socks5-urls" ;;
esac
if [[ "${ACTION}" != "install" ]]; then
return
fi
[[ -n "${PROXY_LOGIN}" ]] || die "--login is required for install."
if [[ -z "${PROXY_PASSWORD}" ]]; then
PROXY_PASSWORD="$(random_password)"
log "Generated proxy password because --password was not provided."
fi
[[ "${PROXY_LOGIN}" =~ ^[A-Za-z0-9._-]+$ ]] || die "--login contains unsupported characters. Allowed: A-Z a-z 0-9 . _ -"
[[ "${PROXY_PASSWORD}" =~ ^[A-Za-z0-9._-]+$ ]] || die "--password contains unsupported characters. Allowed: A-Z a-z 0-9 . _ -"
case "${MODE}" in
existing|cidr|list) ;;
*) die "--mode must be one of: existing, cidr, list" ;;
esac
[[ "${HTTP_PORT_START}" =~ ^[0-9]+$ ]] || die "--http-port-start must be numeric."
[[ "${SOCKS_PORT_START}" =~ ^[0-9]+$ ]] || die "--socks-port-start must be numeric."
[[ "${PROXY_COUNT}" =~ ^[0-9]+$ ]] || die "--proxy-count must be numeric."
[[ "${IPV6_START_OFFSET}" =~ ^[0-9]+$ ]] || die "--ipv6-start-offset must be numeric."
[[ "${ASSIGNED_PREFIXLEN}" =~ ^[0-9]+$ ]] || die "--assigned-prefixlen must be numeric."
[[ "${SELF_TEST_COUNT}" =~ ^[0-9]+$ ]] || die "--self-test-count must be numeric."
case "${MODE}" in
cidr)
[[ -n "${IPV6_CIDR}" ]] || die "--ipv6-cidr is required in cidr mode."
;;
list)
[[ -n "${IPV6_LIST_FILE}" ]] || die "--ipv6-list-file is required in list mode."
[[ -f "${IPV6_LIST_FILE}" ]] || die "IPv6 list file not found: ${IPV6_LIST_FILE}"
;;
esac
}
detect_interface() {
if [[ "${INTERFACE}" != "auto" ]]; then
return
fi
INTERFACE="$(detect_interface_value)"
[[ -n "${INTERFACE}" ]] || die "Unable to auto-detect network interface."
}
detect_interface_value() {
local detected=""
detected="$(ip -6 route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
if [[ -z "${detected}" ]]; then
detected="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
fi
[[ -n "${detected}" ]] || return 1
printf '%s\n' "${detected}"
}
detect_proxy_host_value() {
local iface="$1"
local detected=""
detected="$(ip -4 -o addr show dev "${iface}" scope global | awk '{print $4}' | cut -d/ -f1 | head -n1)"
if [[ -z "${detected}" ]]; then
detected="$(hostname -f 2>/dev/null || true)"
fi
[[ -n "${detected}" ]] || return 1
printf '%s\n' "${detected}"
}
detect_proxy_host() {
if [[ -n "${PROXY_HOST}" ]]; then
return
fi
PROXY_HOST="$(detect_proxy_host_value "${INTERFACE}")"
[[ -n "${PROXY_HOST}" ]] || die "Unable to auto-detect proxy host."
}
count_existing_ipv6_on_interface() {
local iface="$1"
ip -6 -o addr show dev "${iface}" scope global 2>/dev/null \
| awk '{print $4}' \
| cut -d/ -f1 \
| sort -u \
| grep -v '^$' \
| wc -l \
| tr -d '[:space:]'
}
install_base_packages() {
export DEBIAN_FRONTEND=noninteractive
log "Installing base packages..."
apt-get update -y
apt-get install -y --no-install-recommends ca-certificates curl iproute2 gawk python3
}
disable_packaged_3proxy_service() {
if systemctl list-unit-files 2>/dev/null | grep -q '^3proxy\.service'; then
systemctl disable --now 3proxy.service >/dev/null 2>&1 || true
fi
}
build_3proxy_from_source() {
(
set -euo pipefail
local tmp_dir
local src_dir
tmp_dir="$(mktemp -d)"
trap "rm -rf -- '${tmp_dir}'" EXIT
log "Building 3proxy ${THREEPROXY_VERSION} from source..."
apt-get install -y build-essential gcc make libc6-dev libpcre2-dev
curl -fsSL "https://codeload.github.com/3proxy/3proxy/tar.gz/refs/tags/${THREEPROXY_VERSION}" -o "${tmp_dir}/3proxy.tar.gz"
tar -xzf "${tmp_dir}/3proxy.tar.gz" -C "${tmp_dir}"
src_dir="$(find "${tmp_dir}" -maxdepth 1 -type d -name '3proxy-*' | head -n1)"
[[ -n "${src_dir}" ]] || die "Failed to unpack 3proxy sources."
make -C "${src_dir}" -f Makefile.Linux
install -m 0755 "${src_dir}/bin/3proxy" /usr/local/bin/3proxy
)
}
install_3proxy() {
if command -v 3proxy >/dev/null 2>&1; then
log "3proxy already installed: $(command -v 3proxy)"
disable_packaged_3proxy_service
return
fi
local arch
local asset
local tmp_dir
local url
arch="$(dpkg --print-architecture)"
case "${arch}" in
amd64) asset="3proxy-${THREEPROXY_VERSION}.x86_64.deb" ;;
arm64) asset="3proxy-${THREEPROXY_VERSION}.arm64.deb" ;;
armhf|arm) asset="3proxy-${THREEPROXY_VERSION}.arm.deb" ;;
*)
asset=""
;;
esac
tmp_dir="$(mktemp -d)"
trap "rm -rf -- '${tmp_dir}'" RETURN
if [[ -n "${asset}" ]]; then
url="https://github.com/3proxy/3proxy/releases/download/${THREEPROXY_VERSION}/${asset}"
log "Installing 3proxy ${THREEPROXY_VERSION} from official release package..."
curl -fsSL "${url}" -o "${tmp_dir}/${asset}"
if dpkg -i "${tmp_dir}/${asset}"; then
disable_packaged_3proxy_service
return
fi
log "Official 3proxy package could not be installed cleanly on this host. Falling back to source build..."
apt-get install -f -y >/dev/null 2>&1 || true
apt-get purge -y 3proxy >/dev/null 2>&1 || true
else
log "No official DEB asset for architecture ${arch}. Falling back to source build..."
fi
build_3proxy_from_source
disable_packaged_3proxy_service
}
collect_existing_ipv6() {
python3 - "${INTERFACE}" <<'PY'
import ipaddress
import subprocess
import sys
addrs = []
seen = set()
iface = sys.argv[1]
output = subprocess.check_output(
["ip", "-6", "-o", "addr", "show", "dev", iface, "scope", "global"],
text=True,
)
for line in output.splitlines():
parts = line.split()
if len(parts) < 4:
continue
addr = parts[3].split("/", 1)[0].strip()
if not addr or addr in seen:
continue
seen.add(addr)
addrs.append(ipaddress.IPv6Address(addr))
for addr in sorted(addrs):
print(addr)
PY
}
generate_ipv6_from_cidr() {
python3 - "${IPV6_CIDR}" "${PROXY_COUNT}" "${IPV6_START_OFFSET}" <<'PY'
import ipaddress
import sys
network = ipaddress.IPv6Network(sys.argv[1], strict=False)
count = int(sys.argv[2])
start = int(sys.argv[3])
if count <= 0:
sys.exit("proxy count must be > 0")
available = network.num_addresses - start
if available < count:
sys.exit(f"requested {count} addresses but only {available} remain in {network}")
for offset in range(start, start + count):
print(network.network_address + offset)
PY
}
collect_list_ipv6() {
awk '
/^[[:space:]]*#/ { next }
/^[[:space:]]*$/ { next }
{
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
split($0, parts, "/")
print parts[1]
}
' "${IPV6_LIST_FILE}"
}
write_addresses_file() {
local source="${STATE_DIR}/ipv6-addresses.txt"
mkdir -p "${STATE_DIR}"
printf '%s\n' "${ADDRESSES[@]}" > "${source}"
}
write_metadata_file() {
mkdir -p "${STATE_DIR}"
cat > "${STATE_DIR}/installer.env" <<EOF
ACTION=install
MODE=${MODE}
INTERFACE=${INTERFACE}
PROXY_HOST=${PROXY_HOST}
PROXY_LOGIN=${PROXY_LOGIN}
PROTOCOLS=${PROTOCOLS}
HTTP_PORT_START=${HTTP_PORT_START}
SOCKS_PORT_START=${SOCKS_PORT_START}
ASSIGNED_PREFIXLEN=${ASSIGNED_PREFIXLEN}
CONFIG_DIR=${CONFIG_DIR}
STATE_DIR=${STATE_DIR}
OUTPUT_DIR=${OUTPUT_DIR}
LOG_DIR=${LOG_DIR}
EOF
}
load_metadata_file() {
local allow_missing="${1:-0}"
local metadata_file="${STATE_DIR}/installer.env"
if [[ ! -f "${metadata_file}" ]]; then
if [[ "${allow_missing}" == "1" ]]; then
return 1
fi
die "State file not found: ${metadata_file}. Run install first."
fi
# shellcheck disable=SC1090
source "${metadata_file}"
}
apply_addresses_now() {
local addr
if [[ "${MODE}" == "existing" ]]; then
return
fi
for addr in "${ADDRESSES[@]}"; do
if ip -6 addr show dev "${INTERFACE}" | grep -Fq "${addr}/${ASSIGNED_PREFIXLEN}"; then
continue
fi
ip -6 addr add "${addr}/${ASSIGNED_PREFIXLEN}" dev "${INTERFACE}"
done
}
write_address_helper() {
cat > /usr/local/sbin/ywb-ipv6-addresses <<EOF
#!/usr/bin/env bash
set -euo pipefail
INTERFACE="${INTERFACE}"
PREFIXLEN="${ASSIGNED_PREFIXLEN}"
ADDR_FILE="${STATE_DIR}/ipv6-addresses.txt"
cmd="\${1:-apply}"
[[ -f "\${ADDR_FILE}" ]] || exit 0
while IFS= read -r addr; do
[[ -n "\${addr}" ]] || continue
case "\${cmd}" in
apply)
if ! ip -6 addr show dev "\${INTERFACE}" | grep -Fq "\${addr}/\${PREFIXLEN}"; then
ip -6 addr add "\${addr}/\${PREFIXLEN}" dev "\${INTERFACE}"
fi
;;
remove)
if ip -6 addr show dev "\${INTERFACE}" | grep -Fq "\${addr}/\${PREFIXLEN}"; then
ip -6 addr del "\${addr}/\${PREFIXLEN}" dev "\${INTERFACE}"
fi
;;
*)
echo "Unknown command: \${cmd}" >&2
exit 1
;;
esac
done < "\${ADDR_FILE}"
EOF
chmod 0755 /usr/local/sbin/ywb-ipv6-addresses
}
write_address_service() {
if [[ "${MODE}" == "existing" ]]; then
return
fi
cat > /etc/systemd/system/ywb-ipv6-addresses.service <<EOF
[Unit]
Description=Apply IPv6 addresses for YWB proxy
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/ywb-ipv6-addresses apply
[Install]
WantedBy=multi-user.target
EOF
}
write_3proxy_config() {
mkdir -p "${CONFIG_DIR}" "${OUTPUT_DIR}" "${LOG_DIR}"
local cfg="${CONFIG_DIR}/3proxy.cfg"
{
echo "pidfile ${STATE_DIR}/3proxy.pid"
echo "log ${LOG_DIR}/3proxy.log D"
echo "rotate 30"
echo "maxconn 4096"
echo "nscache 65536"
echo "nserver 1.1.1.1"
echo "nserver 1.0.0.1"
echo "nserver 2606:4700:4700::1111"
echo "nserver 2606:4700:4700::1001"
echo "timeouts 1 5 30 60 180 1800 15 60"
echo "auth strong"
echo "users ${PROXY_LOGIN}:CL:${PROXY_PASSWORD}"
echo
local idx=0
local addr=""
local http_port=0
local socks_port=0
for addr in "${ADDRESSES[@]}"; do
http_port=$((HTTP_PORT_START + idx))
socks_port=$((SOCKS_PORT_START + idx))
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
echo "allow ${PROXY_LOGIN}"
echo "proxy -6 -n -a -p${http_port} -i0.0.0.0 -e${addr}"
echo "flush"
echo
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
echo "allow ${PROXY_LOGIN}"
echo "socks -6 -n -a -p${socks_port} -i0.0.0.0 -e${addr}"
echo "flush"
echo
fi
idx=$((idx + 1))
done
} > "${cfg}"
}
resolve_3proxy_binary() {
if command -v 3proxy >/dev/null 2>&1; then
command -v 3proxy
return
fi
[[ -x /usr/local/bin/3proxy ]] && { echo /usr/local/bin/3proxy; return; }
die "3proxy binary not found after installation."
}
write_3proxy_service() {
local bin_path
bin_path="$(resolve_3proxy_binary)"
cat > /etc/systemd/system/ywb-ipv6-proxy.service <<EOF
[Unit]
Description=YWB IPv6 3proxy service
After=network-online.target
Wants=network-online.target
$( [[ "${MODE}" != "existing" ]] && printf 'After=ywb-ipv6-addresses.service\nWants=ywb-ipv6-addresses.service\n' )
[Service]
Type=simple
ExecStart=${bin_path} ${CONFIG_DIR}/3proxy.cfg
Restart=always
RestartSec=3
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
}
write_proxy_lists() {
mkdir -p "${OUTPUT_DIR}"
: > "${OUTPUT_DIR}/proxies-http.txt"
: > "${OUTPUT_DIR}/proxies-http-urls.txt"
: > "${OUTPUT_DIR}/proxies-socks5.txt"
: > "${OUTPUT_DIR}/proxies-socks5-urls.txt"
: > "${OUTPUT_DIR}/proxies-combined.csv"
local idx=0
local addr=""
local http_port=0
local socks_port=0
printf 'protocol,host,port,username,password,ipv6\n' > "${OUTPUT_DIR}/proxies-combined.csv"
for addr in "${ADDRESSES[@]}"; do
http_port=$((HTTP_PORT_START + idx))
socks_port=$((SOCKS_PORT_START + idx))
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
printf '%s:%s:%s:%s\n' "${PROXY_HOST}" "${http_port}" "${PROXY_LOGIN}" "${PROXY_PASSWORD}" >> "${OUTPUT_DIR}/proxies-http.txt"
printf 'http://%s:%s@%s:%s\n' "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${PROXY_HOST}" "${http_port}" >> "${OUTPUT_DIR}/proxies-http-urls.txt"
printf 'http,%s,%s,%s,%s,%s\n' "${PROXY_HOST}" "${http_port}" "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${addr}" >> "${OUTPUT_DIR}/proxies-combined.csv"
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
printf '%s:%s:%s:%s\n' "${PROXY_HOST}" "${socks_port}" "${PROXY_LOGIN}" "${PROXY_PASSWORD}" >> "${OUTPUT_DIR}/proxies-socks5.txt"
printf 'socks5://%s:%s@%s:%s\n' "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${PROXY_HOST}" "${socks_port}" >> "${OUTPUT_DIR}/proxies-socks5-urls.txt"
printf 'socks5,%s,%s,%s,%s,%s\n' "${PROXY_HOST}" "${socks_port}" "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${addr}" >> "${OUTPUT_DIR}/proxies-combined.csv"
fi
idx=$((idx + 1))
done
}
manage_firewall_if_needed() {
if [[ "${MANAGE_FIREWALL}" != "1" ]]; then
return
fi
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q "Status: active"; then
log "Adding UFW rules for proxy ports..."
local port
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
for ((port = HTTP_PORT_START; port < HTTP_PORT_START + ${#ADDRESSES[@]}; port++)); do
ufw allow "${port}/tcp" >/dev/null
done
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
for ((port = SOCKS_PORT_START; port < SOCKS_PORT_START + ${#ADDRESSES[@]}; port++)); do
ufw allow "${port}/tcp" >/dev/null
done
fi
else
log "Firewall management requested, but no active UFW was found. Skipping firewall changes."
fi
}
enable_and_start_services() {
systemctl daemon-reload
if [[ "${MODE}" != "existing" ]]; then
systemctl enable ywb-ipv6-addresses.service >/dev/null 2>&1 || true
systemctl restart ywb-ipv6-addresses.service
fi
systemctl enable ywb-ipv6-proxy.service >/dev/null 2>&1 || true
if systemctl is-active --quiet ywb-ipv6-proxy.service; then
systemctl restart ywb-ipv6-proxy.service
else
systemctl start ywb-ipv6-proxy.service
fi
}
wait_for_local_listener() {
local port="$1"
local attempts="${2:-40}"
local sleep_seconds="${3:-0.25}"
local attempt=0
while (( attempt < attempts )); do
if ss -ltn "( sport = :${port} )" 2>/dev/null | grep -Fq ":${port}"; then
return 0
fi
sleep "${sleep_seconds}"
attempt=$((attempt + 1))
done
return 1
}
run_self_test() {
local max="${SELF_TEST_COUNT}"
local tested=0
local idx=0
local addr=""
local port=0
local result=""
[[ "${max}" -gt 0 ]] || return
log "Running self-test against the first ${max} address(es)..."
for addr in "${ADDRESSES[@]}"; do
if [[ "${tested}" -ge "${max}" ]]; then
break
fi
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
port=$((HTTP_PORT_START + idx))
wait_for_local_listener "${port}" || die "HTTP proxy listener did not start on port ${port} before self-test."
result="$(curl -4 -fsS --noproxy '' -x "http://${PROXY_LOGIN}:${PROXY_PASSWORD}@127.0.0.1:${port}" --max-time 20 https://ifconfig.co/ip | tr -d '\r\n')"
[[ "${result}" == "${addr}" ]] || die "HTTP proxy self-test failed on port ${port}. Expected ${addr}, got ${result}."
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
port=$((SOCKS_PORT_START + idx))
wait_for_local_listener "${port}" || die "SOCKS5 proxy listener did not start on port ${port} before self-test."
result="$(curl -4 -fsS --noproxy '' --socks5-hostname "127.0.0.1:${port}" --proxy-user "${PROXY_LOGIN}:${PROXY_PASSWORD}" --max-time 20 https://ifconfig.co/ip | tr -d '\r\n')"
[[ "${result}" == "${addr}" ]] || die "SOCKS5 proxy self-test failed on port ${port}. Expected ${addr}, got ${result}."
fi
tested=$((tested + 1))
idx=$((idx + 1))
done
}
print_summary() {
cat <<EOF
YWB IPv6 proxy installer ${INSTALLER_VERSION} completed successfully.
Interface: ${INTERFACE}
Proxy host: ${PROXY_HOST}
Mode: ${MODE}
Address count: ${#ADDRESSES[@]}
Protocols: ${PROTOCOLS}
HTTP start port: ${HTTP_PORT_START}
SOCKS start port: ${SOCKS_PORT_START}
Config directory: ${CONFIG_DIR}
State directory: ${STATE_DIR}
Output directory: ${OUTPUT_DIR}
Log file: ${LOG_DIR}/3proxy.log
Example proxy:
EOF
if [[ ",${PROTOCOLS}," == *",http,"* ]]; then
printf ' http://%s:%s@%s:%s\n' "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${PROXY_HOST}" "${HTTP_PORT_START}"
fi
if [[ ",${PROTOCOLS}," == *",socks5,"* ]]; then
printf ' socks5://%s:%s@%s:%s\n' "${PROXY_LOGIN}" "${PROXY_PASSWORD}" "${PROXY_HOST}" "${SOCKS_PORT_START}"
fi
cat <<EOF
Generated files:
${OUTPUT_DIR}/proxies-http.txt
${OUTPUT_DIR}/proxies-http-urls.txt
${OUTPUT_DIR}/proxies-socks5.txt
${OUTPUT_DIR}/proxies-socks5-urls.txt
${OUTPUT_DIR}/proxies-combined.csv
EOF
}
show_current_list() {
local target_file=""
load_metadata_file
case "${SHOW_FILE}" in
combined) target_file="${OUTPUT_DIR}/proxies-combined.csv" ;;
http) target_file="${OUTPUT_DIR}/proxies-http.txt" ;;
http-urls) target_file="${OUTPUT_DIR}/proxies-http-urls.txt" ;;
socks5) target_file="${OUTPUT_DIR}/proxies-socks5.txt" ;;
socks5-urls) target_file="${OUTPUT_DIR}/proxies-socks5-urls.txt" ;;
esac
[[ -f "${target_file}" ]] || die "Requested proxy list file not found: ${target_file}"
cat <<EOF
YWB IPv6 proxy list
Mode: ${MODE}
Interface: ${INTERFACE}
Proxy host: ${PROXY_HOST}
Protocols: ${PROTOCOLS}
Source file: ${target_file}
EOF
cat "${target_file}"
}
uninstall_setup() {
load_metadata_file 1 || true
if [[ -f /usr/local/sbin/ywb-ipv6-addresses && "${MODE}" != "existing" ]]; then
/usr/local/sbin/ywb-ipv6-addresses remove || true
fi
systemctl disable --now ywb-ipv6-proxy.service >/dev/null 2>&1 || true
systemctl disable --now ywb-ipv6-addresses.service >/dev/null 2>&1 || true
systemctl daemon-reload
rm -f /etc/systemd/system/ywb-ipv6-proxy.service
rm -f /etc/systemd/system/ywb-ipv6-addresses.service
rm -f /usr/local/sbin/ywb-ipv6-addresses
rm -rf "${CONFIG_DIR}"
rm -rf "${STATE_DIR}"
rm -rf "${OUTPUT_DIR}"
rm -rf "${LOG_DIR}"
systemctl daemon-reload
log "YWB-managed proxy setup was removed. Installed 3proxy package was left in place."
}
install_setup() {
detect_interface
detect_proxy_host
install_base_packages
install_3proxy
mapfile -t ADDRESSES < <(
case "${MODE}" in
existing) collect_existing_ipv6 ;;
cidr) generate_ipv6_from_cidr ;;
list) collect_list_ipv6 ;;
esac
)
[[ "${#ADDRESSES[@]}" -gt 0 ]] || die "No IPv6 addresses were collected."
if [[ "${MODE}" == "existing" ]] && [[ "${PROXY_COUNT}" -gt 0 ]] && [[ "${PROXY_COUNT}" -lt "${#ADDRESSES[@]}" ]]; then
ADDRESSES=("${ADDRESSES[@]:0:${PROXY_COUNT}}")
fi
write_addresses_file
write_metadata_file
write_address_helper
apply_addresses_now
write_address_service
write_3proxy_config
write_3proxy_service
write_proxy_lists
manage_firewall_if_needed
enable_and_start_services
run_self_test
print_summary
}
main() {
local original_argc="$#"
parse_args "$@"
if [[ "${original_argc}" -eq 0 && -t 0 ]]; then
INTERACTIVE="1"
fi
load_config_if_needed
if [[ "${INTERACTIVE}" == "1" ]]; then
interactive_configure
fi
validate_inputs
require_root
require_supported_os
case "${ACTION}" in
install) install_setup ;;
uninstall) uninstall_setup ;;
show) show_current_list ;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment