Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21718042b6 | |||
| b6d49549fe | |||
| ec4aff6cfe | |||
| 4bbc8284d0 | |||
| 77a376c3e0 | |||
| 58dcb6d9a6 | |||
| 2bccb692a2 | |||
| 6d0cad85f5 | |||
| 1ec1aa6307 | |||
| 74fe9ed02c | |||
| 22e16773ef | |||
| 2ced93a971 | |||
| ec3c2d92cf | |||
| 8174346537 | |||
| f95545040a | |||
| 0728862fbe | |||
| e4fd2074e2 | |||
| 2e56306a4e | |||
| 4cae0783a6 | |||
| 9f86f78db5 | |||
| a13d26f0eb | |||
| 771293f309 | |||
| c485e7c8b6 | |||
| 7a2a7b2f48 |
@@ -5,3 +5,7 @@ tor/data/
|
|||||||
secrets/
|
secrets/
|
||||||
|
|
||||||
generated.yml
|
generated.yml
|
||||||
|
|
||||||
|
# local runtime state
|
||||||
|
pihole/etc-pihole/
|
||||||
|
# tailscale source files are versioned; runtime state is stored in Docker volume
|
||||||
|
|||||||
+65
-51
@@ -1,74 +1,88 @@
|
|||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM --platform=$BUILDPLATFORM ubuntu:22.04 AS builder
|
FROM --platform=$TARGETPLATFORM ubuntu:22.04 AS builder
|
||||||
|
|
||||||
ARG BUILDPLATFORM
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# Install build dependencies
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
RUN apt-get update && apt-get install -y \
|
wget \
|
||||||
qtbase5-dev \
|
ca-certificates \
|
||||||
qtchooser \
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
qt5-qmake \
|
|
||||||
qtbase5-dev-tools \
|
|
||||||
wget \
|
|
||||||
git \
|
|
||||||
build-essential \
|
|
||||||
libqt5core5a \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Download appropriate 3proxy binary based on architecture
|
# Download and extract appropriate 3proxy package based on architecture.
|
||||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
RUN set -eux; \
|
||||||
wget -O 3proxy.deb https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.x86_64.deb; \
|
if [ "$TARGETARCH" = "amd64" ]; then \
|
||||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
pkg_url="https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.x86_64.deb"; \
|
||||||
wget -O 3proxy.deb https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.aarch64.deb || \
|
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||||
{ echo "ARM64 binary not available, using x86_64"; wget -O 3proxy.deb https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.x86_64.deb; }; \
|
pkg_url="https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.aarch64.deb"; \
|
||||||
elif [ "$TARGETARCH" = "arm" ]; then \
|
elif [ "$TARGETARCH" = "arm" ]; then \
|
||||||
wget -O 3proxy.deb https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.armv7l.deb || \
|
pkg_url="https://github.com/3proxy/3proxy/releases/download/0.9.4/3proxy-0.9.4.armv7l.deb"; \
|
||||||
{ echo "ARMv7 binary not available"; exit 1; }; \
|
else \
|
||||||
fi && \
|
echo "Unsupported architecture: $TARGETARCH"; exit 1; \
|
||||||
dpkg -i 3proxy.deb || true
|
fi; \
|
||||||
|
wget -O /tmp/3proxy.deb "$pkg_url"; \
|
||||||
|
dpkg-deb -x /tmp/3proxy.deb /tmp/3proxy-root; \
|
||||||
|
test -x /tmp/3proxy-root/bin/3proxy; \
|
||||||
|
install -m 0755 /tmp/3proxy-root/bin/3proxy /usr/local/bin/3proxy; \
|
||||||
|
mkdir -p /usr/local/3proxy; \
|
||||||
|
cp -a /tmp/3proxy-root/usr/local/3proxy/. /usr/local/3proxy/ || true
|
||||||
|
|
||||||
# Build 3proxy-eagle in a dedicated source directory
|
# Stage 1b: Build Yggdrasil tooling for target arch
|
||||||
RUN git clone https://gitea.darkness.services/acetone/3proxy-eagle.git /src/3proxy-eagle \
|
FROM --platform=$BUILDPLATFORM golang:1.22-bookworm AS builder_yggdrasil
|
||||||
&& cd /src/3proxy-eagle \
|
|
||||||
&& find . -name "*.cpp" -o -name "*.h" | xargs sed -i 's/127\.0\.0\.1/0.0.0.0/g' \
|
ARG TARGETARCH
|
||||||
&& cd /src/3proxy-eagle/src \
|
ARG YGGDRASIL_VERSION=v0.5.12
|
||||||
&& qmake 3proxy-eagle.pro \
|
|
||||||
&& make -j$(nproc)
|
RUN git clone https://github.com/yggdrasil-network/yggdrasil-go.git /src/yggdrasil-go \
|
||||||
|
&& cd /src/yggdrasil-go \
|
||||||
|
&& git checkout ${YGGDRASIL_VERSION}
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
case "${TARGETARCH}" in \
|
||||||
|
amd64) goarch=amd64 ;; \
|
||||||
|
arm64) goarch=arm64 ;; \
|
||||||
|
arm) goarch=arm ;; \
|
||||||
|
*) echo "Unsupported architecture: ${TARGETARCH}"; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
cd /src/yggdrasil-go; \
|
||||||
|
mkdir -p /out; \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=${goarch} go build -o /out/yggdrasil ./cmd/yggdrasil; \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=${goarch} go build -o /out/yggdrasilctl ./cmd/yggdrasilctl; \
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=${goarch} go build -o /out/genkeys ./cmd/genkeys
|
||||||
|
|
||||||
# Stage 2: Runtime
|
# Stage 2: Runtime
|
||||||
FROM ubuntu:22.04
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
ARG TARGETARCH
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
libqt5core5a \
|
|
||||||
libqt5network5 \
|
|
||||||
libqt5xml5 \
|
|
||||||
libqt5sql5 \
|
|
||||||
libqt5sql5-sqlite \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
iproute2 \
|
||||||
|
iputils-ping \
|
||||||
|
python3 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy binaries using the correct absolute paths from Stage 1
|
# Copy binaries using the correct absolute paths from Stage 1
|
||||||
COPY --from=builder /usr/bin/3proxy /usr/bin/3proxy
|
COPY --from=builder /usr/local/bin/3proxy /usr/bin/3proxy
|
||||||
COPY --from=builder /src/3proxy-eagle/src/3proxy-eagle /usr/bin/3proxy-eagle
|
COPY --from=builder /usr/local/3proxy /usr/local/3proxy
|
||||||
|
COPY --from=builder_yggdrasil /out/yggdrasil /usr/bin/yggdrasil
|
||||||
|
COPY --from=builder_yggdrasil /out/yggdrasilctl /usr/bin/yggdrasilctl
|
||||||
|
COPY --from=builder_yggdrasil /out/genkeys /usr/bin/genkeys
|
||||||
|
|
||||||
|
# Copy local runtime helpers/config
|
||||||
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
COPY ./socks_router.py /usr/local/bin/socks_router.py
|
||||||
|
COPY ./yggdrasil.conf /etc/yggdrasil/yggdrasil.conf
|
||||||
|
|
||||||
# Setup config directories
|
# Setup config directories
|
||||||
RUN mkdir -p /etc/3proxy /etc/3proxy-eagle/data
|
RUN mkdir -p /etc/3proxy /etc/yggdrasil \
|
||||||
|
&& chmod +x /usr/local/bin/socks_router.py \
|
||||||
|
&& chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Ensure these files exist in your local directory where you run 'docker build'
|
# Ensure these files exist in your local directory where you run 'docker build'
|
||||||
#COPY first-instanse.cfg /etc/3proxy/
|
#COPY first-instanse.cfg /etc/3proxy/
|
||||||
#COPY second-instanse.cfg /etc/3proxy/
|
#COPY second-instanse.cfg /etc/3proxy/
|
||||||
|
|
||||||
EXPOSE 8161
|
EXPOSE 1080 3128 8161
|
||||||
|
|
||||||
# Run in foreground
|
# Start Yggdrasil first, then run plain 3proxy instances from entrypoint.
|
||||||
CMD ["/usr/bin/3proxy-eagle", \
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
"-i", "/usr/bin/3proxy,/etc/3proxy/first-instanse.cfg", \
|
CMD []
|
||||||
"-i", "/usr/bin/3proxy,/etc/3proxy/second-instanse.cfg", \
|
|
||||||
"-w", "/etc/3proxy-eagle/data", \
|
|
||||||
"-t", "DarkProxy"]
|
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
YGG_CONF="${YGG_CONF:-/etc/yggdrasil/yggdrasil.conf}"
|
||||||
|
YGG_CONNECT_WAIT_SECONDS="${YGG_CONNECT_WAIT_SECONDS:-8}"
|
||||||
|
YGG_RUNTIME_CONF="${YGG_RUNTIME_CONF:-/run/yggdrasil.conf}"
|
||||||
|
PROXY_CFG_PRIMARY="${PROXY_CFG_PRIMARY:-/etc/3proxy/first-instanse.cfg}"
|
||||||
|
PROXY_CFG_SECONDARY="${PROXY_CFG_SECONDARY:-/etc/3proxy/second-instanse.cfg}"
|
||||||
|
SOCKS_ROUTER_BIN="${SOCKS_ROUTER_BIN:-/usr/local/bin/socks_router.py}"
|
||||||
|
|
||||||
|
ygg_pid=""
|
||||||
|
proxy_primary_pid=""
|
||||||
|
proxy_secondary_pid=""
|
||||||
|
router_pid=""
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [[ -n "${proxy_primary_pid}" ]]; then
|
||||||
|
kill -TERM "${proxy_primary_pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${proxy_secondary_pid}" ]]; then
|
||||||
|
kill -TERM "${proxy_secondary_pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${router_pid}" ]]; then
|
||||||
|
kill -TERM "${router_pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if [[ -n "${ygg_pid}" ]]; then
|
||||||
|
kill -TERM "${ygg_pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
wait || true
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup TERM INT
|
||||||
|
|
||||||
|
if [[ ! -f "${YGG_CONF}" ]]; then
|
||||||
|
log "Missing Yggdrasil config at ${YGG_CONF}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${PROXY_CFG_PRIMARY}" ]]; then
|
||||||
|
log "Missing primary 3proxy config at ${PROXY_CFG_PRIMARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${PROXY_CFG_SECONDARY}" ]]; then
|
||||||
|
log "Missing secondary 3proxy config at ${PROXY_CFG_SECONDARY}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${SOCKS_ROUTER_BIN}" ]]; then
|
||||||
|
log "Missing SOCKS router at ${SOCKS_ROUTER_BIN}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "${YGG_CONF}" "${YGG_RUNTIME_CONF}"
|
||||||
|
|
||||||
|
# Populate keys once if config still has empty key fields.
|
||||||
|
if ! grep -Eq '^[[:space:]]*PrivateKey:[[:space:]]+[0-9a-fA-F]+' "${YGG_RUNTIME_CONF}"; then
|
||||||
|
log "Generating Yggdrasil keys for dark3proxy"
|
||||||
|
key_output="$(timeout 8 /usr/bin/genkeys 2>/dev/null || true)"
|
||||||
|
priv_key="$(printf '%s\n' "${key_output}" | awk '/^Priv:/{print $2; exit}')"
|
||||||
|
pub_key="$(printf '%s\n' "${key_output}" | awk '/^Pub:/{print $2; exit}')"
|
||||||
|
|
||||||
|
if [[ -z "${priv_key}" || -z "${pub_key}" ]]; then
|
||||||
|
log "Failed to parse generated Yggdrasil keys"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i -E \
|
||||||
|
-e "s|^([[:space:]]*PublicKey:).*|\1 ${pub_key}|" \
|
||||||
|
-e "s|^([[:space:]]*PrivateKey:).*|\1 ${priv_key}|" \
|
||||||
|
"${YGG_RUNTIME_CONF}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sysctl net.ipv6.conf.all.disable_ipv6=0 >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
log "Starting Yggdrasil"
|
||||||
|
/usr/bin/yggdrasil -useconffile "${YGG_RUNTIME_CONF}" &
|
||||||
|
ygg_pid=$!
|
||||||
|
|
||||||
|
sleep "${YGG_CONNECT_WAIT_SECONDS}"
|
||||||
|
if ! kill -0 "${ygg_pid}" 2>/dev/null; then
|
||||||
|
log "Yggdrasil exited during startup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting secondary 3proxy instance (${PROXY_CFG_SECONDARY})"
|
||||||
|
/usr/bin/3proxy "${PROXY_CFG_SECONDARY}" &
|
||||||
|
proxy_secondary_pid=$!
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if ! kill -0 "${proxy_secondary_pid}" 2>/dev/null; then
|
||||||
|
log "Secondary 3proxy instance exited during startup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting hostname-preserving SOCKS router (${SOCKS_ROUTER_BIN})"
|
||||||
|
python3 "${SOCKS_ROUTER_BIN}" &
|
||||||
|
router_pid=$!
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if ! kill -0 "${router_pid}" 2>/dev/null; then
|
||||||
|
log "SOCKS router exited during startup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Starting primary 3proxy instance (${PROXY_CFG_PRIMARY})"
|
||||||
|
/usr/bin/3proxy "${PROXY_CFG_PRIMARY}" &
|
||||||
|
proxy_primary_pid=$!
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if ! kill -0 "${proxy_primary_pid}" 2>/dev/null; then
|
||||||
|
log "Primary 3proxy instance exited during startup"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait -n "${proxy_secondary_pid}" "${proxy_primary_pid}" "${router_pid}" "${ygg_pid}"
|
||||||
|
exit_code=$?
|
||||||
|
log "A managed process exited, shutting down"
|
||||||
|
cleanup
|
||||||
|
exit "${exit_code}"
|
||||||
+27
-17
@@ -1,46 +1,56 @@
|
|||||||
# regular 3proxy configuration
|
# regular 3proxy configuration
|
||||||
fakeresolve
|
|
||||||
flush
|
flush
|
||||||
auth strong
|
nserver 10.5.0.7/9053
|
||||||
users $/run/secrets/PROXY_USERS
|
nserver 10.5.0.6
|
||||||
admin -p8161 -a127.0.0.1
|
nscache 65536
|
||||||
# strict required by 3proxy-eagle log lines
|
auth none
|
||||||
|
# Expose admin interface to host via docker port mapping (2002 -> 8161).
|
||||||
|
admin -p8161
|
||||||
|
# Keep compact per-request accounting in container logs.
|
||||||
log
|
log
|
||||||
logformat " type=%N destination=%n to=%O from=%I"
|
logformat " type=%N destination=%n to=%O from=%I"
|
||||||
|
|
||||||
# 3proxy-eagle black list format
|
# Blocklist (plain 3proxy syntax).
|
||||||
{{vk.com,ok.ru,mail.ru,gosuslugi.ru,127.0.0.1}}
|
deny * * vk.com
|
||||||
|
deny * * ok.ru
|
||||||
|
deny * * mail.ru
|
||||||
|
deny * * gosuslugi.ru
|
||||||
|
|
||||||
# regular 3proxy configuration again
|
# regular 3proxy configuration again
|
||||||
|
|
||||||
# expose a lightweight status socket for Prometheus scraping
|
|
||||||
# the exporter will connect here and convert counters to metrics
|
|
||||||
monitor -p6800
|
|
||||||
|
|
||||||
## i2p sites
|
## i2p sites
|
||||||
allow * * *.i2p
|
allow * * .i2p
|
||||||
#parent 1000 http 127.0.0.1 4444
|
#parent 1000 http 127.0.0.1 4444
|
||||||
parent 1000 socks5+ 10.5.0.2 4447
|
parent 1000 socks5+ 10.5.0.2 4447
|
||||||
|
flush
|
||||||
|
|
||||||
## yggdrasil (127.0.0.1 1085 is second instanse)
|
## yggdrasil (127.0.0.1 1085 is second instanse)
|
||||||
allow * * 200::/7
|
allow * * 200::/7
|
||||||
parent 1000 socks5+ 127.0.0.1 1085
|
parent 1000 socks5+ 127.0.0.1 1085
|
||||||
|
flush
|
||||||
|
|
||||||
## ygg domains resolving by alfis
|
## ygg domains resolving by alfis
|
||||||
allow * * *.ygg
|
allow * * .ygg
|
||||||
parent 1000 socks5+ 127.0.0.1 1085
|
parent 1000 socks5+ 127.0.0.1 1085
|
||||||
|
flush
|
||||||
|
|
||||||
## meshnames domains resolving by meshnamed
|
## meshnames domains resolving by meshnamed
|
||||||
allow * * *.meshname
|
allow * * .meshname
|
||||||
parent 1000 socks5+ 127.0.0.1 1085
|
parent 1000 socks5+ 127.0.0.1 1085
|
||||||
allow * * *.meship
|
flush
|
||||||
|
|
||||||
|
allow * * .meship
|
||||||
parent 1000 socks5+ 127.0.0.1 1085
|
parent 1000 socks5+ 127.0.0.1 1085
|
||||||
|
flush
|
||||||
|
|
||||||
## onion sites
|
## onion sites
|
||||||
allow * * *.onion
|
allow * * .onion
|
||||||
parent 1000 socks5+ 10.5.0.7 9050
|
parent 1000 socks5+ 10.5.0.7 9050
|
||||||
|
flush
|
||||||
|
|
||||||
## clearnet via another proxy or change to tor (9050)
|
## clearnet via another proxy or change to tor (9050)
|
||||||
allow *
|
allow *
|
||||||
parent 1000 socks5+ 10.5.0.7 9050
|
parent 1000 socks5+ 10.5.0.7 9050
|
||||||
|
|
||||||
socks -olSO_REUSEADDR,SO_REUSEPORT -ocTCP_TIMESTAMPS,TCP_NODELAY -osTCP_NODELAY -46 -i0.0.0.0 -p1080
|
socks -olSO_REUSEADDR,SO_REUSEPORT -ocTCP_TIMESTAMPS,TCP_NODELAY -osTCP_NODELAY -46 -i0.0.0.0 -p1088
|
||||||
proxy -olSO_REUSEADDR,SO_REUSEPORT -ocTCP_TIMESTAMPS,TCP_NODELAY -osTCP_NODELAY -46 -a1 -i0.0.0.0 -p3128
|
proxy -olSO_REUSEADDR,SO_REUSEPORT -ocTCP_TIMESTAMPS,TCP_NODELAY -osTCP_NODELAY -46 -a1 -i0.0.0.0 -p3128
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
SOCKS_VERSION = 5
|
||||||
|
CMD_CONNECT = 1
|
||||||
|
ATYP_IPV4 = 1
|
||||||
|
ATYP_DOMAIN = 3
|
||||||
|
ATYP_IPV6 = 4
|
||||||
|
|
||||||
|
REP_SUCCEEDED = 0
|
||||||
|
REP_GENERAL_FAILURE = 1
|
||||||
|
REP_CONNECTION_NOT_ALLOWED = 2
|
||||||
|
REP_NETWORK_UNREACHABLE = 3
|
||||||
|
REP_HOST_UNREACHABLE = 4
|
||||||
|
REP_COMMAND_NOT_SUPPORTED = 7
|
||||||
|
REP_ADDRESS_TYPE_NOT_SUPPORTED = 8
|
||||||
|
|
||||||
|
YGG_NET = ipaddress.ip_network("200::/7")
|
||||||
|
|
||||||
|
|
||||||
|
def env_endpoint(var_name: str, default: str) -> Tuple[str, int]:
|
||||||
|
raw = os.getenv(var_name, default).strip()
|
||||||
|
host, sep, port_s = raw.rpartition(":")
|
||||||
|
if not sep:
|
||||||
|
raise ValueError(f"Invalid endpoint for {var_name}: {raw}")
|
||||||
|
return host, int(port_s)
|
||||||
|
|
||||||
|
|
||||||
|
LISTEN_HOST = os.getenv("ROUTER_LISTEN_HOST", "0.0.0.0")
|
||||||
|
LISTEN_PORT = int(os.getenv("ROUTER_LISTEN_PORT", "1080"))
|
||||||
|
UPSTREAM_TOR = env_endpoint("ROUTER_UPSTREAM_TOR", "10.5.0.7:9050")
|
||||||
|
UPSTREAM_I2P = env_endpoint("ROUTER_UPSTREAM_I2P", "10.5.0.2:4447")
|
||||||
|
UPSTREAM_YGG = env_endpoint("ROUTER_UPSTREAM_YGG", "127.0.0.1:1085")
|
||||||
|
I2P_MAP_FILE = os.getenv("ROUTER_I2P_MAP_FILE", "").strip()
|
||||||
|
I2P_POOL_CIDR = os.getenv("ROUTER_I2P_POOL_CIDR", "172.31.0.0/16")
|
||||||
|
I2P_POOL = ipaddress.ip_network(I2P_POOL_CIDR)
|
||||||
|
|
||||||
|
_i2p_map_mtime = -1.0
|
||||||
|
_i2p_map_last_check = 0.0
|
||||||
|
_i2p_ip_to_host = {}
|
||||||
|
|
||||||
|
BLOCKLIST = [
|
||||||
|
item.strip().lower()
|
||||||
|
for item in os.getenv("ROUTER_BLOCKLIST", "vk.com,ok.ru,mail.ru,gosuslugi.ru").split(",")
|
||||||
|
if item.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg: str) -> None:
|
||||||
|
ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
print(f"[{ts}] [socks-router] {msg}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def is_blocked_domain(hostname: str) -> bool:
|
||||||
|
host = hostname.rstrip(".").lower()
|
||||||
|
for blocked in BLOCKLIST:
|
||||||
|
if host == blocked or host.endswith("." + blocked):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_i2p_map_if_needed() -> None:
|
||||||
|
global _i2p_map_mtime, _i2p_map_last_check, _i2p_ip_to_host
|
||||||
|
|
||||||
|
if not I2P_MAP_FILE:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
if now - _i2p_map_last_check < 1.0:
|
||||||
|
return
|
||||||
|
_i2p_map_last_check = now
|
||||||
|
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(I2P_MAP_FILE)
|
||||||
|
except OSError:
|
||||||
|
_i2p_ip_to_host = {}
|
||||||
|
_i2p_map_mtime = -1.0
|
||||||
|
return
|
||||||
|
|
||||||
|
if mtime == _i2p_map_mtime:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(I2P_MAP_FILE, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
mapping = data.get("ip_to_host", {}) if isinstance(data, dict) else {}
|
||||||
|
if isinstance(mapping, dict):
|
||||||
|
_i2p_ip_to_host = {str(k): str(v) for k, v in mapping.items()}
|
||||||
|
else:
|
||||||
|
_i2p_ip_to_host = {}
|
||||||
|
_i2p_map_mtime = mtime
|
||||||
|
except Exception:
|
||||||
|
_i2p_ip_to_host = {}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_i2p_hostname_for_virtual_ip(ip_str: str) -> Optional[str]:
|
||||||
|
if not I2P_MAP_FILE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(ip_str)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if ip.version != 4 or ip not in I2P_POOL:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_reload_i2p_map_if_needed()
|
||||||
|
host = _i2p_ip_to_host.get(str(ip))
|
||||||
|
if host and host.endswith(".i2p"):
|
||||||
|
return host
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pick_upstream(host: str, atyp: int) -> Optional[Tuple[str, int]]:
|
||||||
|
if atyp == ATYP_DOMAIN:
|
||||||
|
dhost = host.rstrip(".").lower()
|
||||||
|
if is_blocked_domain(dhost):
|
||||||
|
return None
|
||||||
|
if dhost.endswith(".onion"):
|
||||||
|
return UPSTREAM_TOR
|
||||||
|
if dhost.endswith(".i2p"):
|
||||||
|
return UPSTREAM_I2P
|
||||||
|
if dhost.endswith(".ygg") or dhost.endswith(".meshname") or dhost.endswith(".meship"):
|
||||||
|
return UPSTREAM_YGG
|
||||||
|
return UPSTREAM_TOR
|
||||||
|
|
||||||
|
if atyp == ATYP_IPV6:
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(host)
|
||||||
|
if ip in YGG_NET:
|
||||||
|
return UPSTREAM_YGG
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
return UPSTREAM_TOR
|
||||||
|
|
||||||
|
if atyp == ATYP_IPV4:
|
||||||
|
return UPSTREAM_TOR
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def read_exact(reader: asyncio.StreamReader, n: int) -> bytes:
|
||||||
|
data = await reader.readexactly(n)
|
||||||
|
if len(data) != n:
|
||||||
|
raise asyncio.IncompleteReadError(partial=data, expected=n)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def send_reply(writer: asyncio.StreamWriter, rep: int) -> None:
|
||||||
|
writer.write(b"\x05" + bytes([rep]) + b"\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||||
|
await writer.drain()
|
||||||
|
|
||||||
|
|
||||||
|
async def pipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = await reader.read(65536)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
writer.write(chunk)
|
||||||
|
await writer.drain()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_client(client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter) -> None:
|
||||||
|
peer = client_writer.get_extra_info("peername")
|
||||||
|
upstream_writer = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
header = await read_exact(client_reader, 2)
|
||||||
|
ver, n_methods = header[0], header[1]
|
||||||
|
if ver != SOCKS_VERSION:
|
||||||
|
client_writer.close()
|
||||||
|
await client_writer.wait_closed()
|
||||||
|
return
|
||||||
|
|
||||||
|
_ = await read_exact(client_reader, n_methods)
|
||||||
|
client_writer.write(b"\x05\x00")
|
||||||
|
await client_writer.drain()
|
||||||
|
|
||||||
|
req = await read_exact(client_reader, 4)
|
||||||
|
ver, cmd, _rsv, atyp = req
|
||||||
|
if ver != SOCKS_VERSION or cmd != CMD_CONNECT:
|
||||||
|
await send_reply(client_writer, REP_COMMAND_NOT_SUPPORTED)
|
||||||
|
return
|
||||||
|
|
||||||
|
if atyp == ATYP_IPV4:
|
||||||
|
raw_addr = await read_exact(client_reader, 4)
|
||||||
|
dst_host = str(ipaddress.IPv4Address(raw_addr))
|
||||||
|
elif atyp == ATYP_IPV6:
|
||||||
|
raw_addr = await read_exact(client_reader, 16)
|
||||||
|
dst_host = str(ipaddress.IPv6Address(raw_addr))
|
||||||
|
elif atyp == ATYP_DOMAIN:
|
||||||
|
dlen = (await read_exact(client_reader, 1))[0]
|
||||||
|
raw_addr = await read_exact(client_reader, dlen)
|
||||||
|
dst_host = raw_addr.decode("ascii", errors="ignore")
|
||||||
|
else:
|
||||||
|
await send_reply(client_writer, REP_ADDRESS_TYPE_NOT_SUPPORTED)
|
||||||
|
return
|
||||||
|
|
||||||
|
dst_port = struct.unpack("!H", await read_exact(client_reader, 2))[0]
|
||||||
|
|
||||||
|
connect_atyp = atyp
|
||||||
|
connect_host = dst_host
|
||||||
|
connect_raw_addr = raw_addr
|
||||||
|
|
||||||
|
if atyp == ATYP_IPV4:
|
||||||
|
mapped_i2p = lookup_i2p_hostname_for_virtual_ip(dst_host)
|
||||||
|
if mapped_i2p:
|
||||||
|
connect_atyp = ATYP_DOMAIN
|
||||||
|
connect_host = mapped_i2p
|
||||||
|
connect_raw_addr = None
|
||||||
|
|
||||||
|
upstream = pick_upstream(connect_host, connect_atyp)
|
||||||
|
if upstream is None:
|
||||||
|
await send_reply(client_writer, REP_CONNECTION_NOT_ALLOWED)
|
||||||
|
log(f"deny peer={peer} dst={dst_host}:{dst_port}")
|
||||||
|
return
|
||||||
|
|
||||||
|
up_host, up_port = upstream
|
||||||
|
if connect_host != dst_host:
|
||||||
|
log(
|
||||||
|
f"route peer={peer} dst={dst_host}:{dst_port} mapped={connect_host} via={up_host}:{up_port}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log(f"route peer={peer} dst={dst_host}:{dst_port} via={up_host}:{up_port}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
upstream_reader, upstream_writer = await asyncio.open_connection(up_host, up_port)
|
||||||
|
except Exception:
|
||||||
|
await send_reply(client_writer, REP_NETWORK_UNREACHABLE)
|
||||||
|
return
|
||||||
|
|
||||||
|
# SOCKS5 greeting to upstream.
|
||||||
|
upstream_writer.write(b"\x05\x01\x00")
|
||||||
|
await upstream_writer.drain()
|
||||||
|
up_auth = await read_exact(upstream_reader, 2)
|
||||||
|
if up_auth[0] != SOCKS_VERSION or up_auth[1] != 0x00:
|
||||||
|
await send_reply(client_writer, REP_GENERAL_FAILURE)
|
||||||
|
return
|
||||||
|
|
||||||
|
connect_req = bytearray(b"\x05\x01\x00")
|
||||||
|
connect_req.append(connect_atyp)
|
||||||
|
if connect_atyp == ATYP_DOMAIN:
|
||||||
|
try:
|
||||||
|
host_bytes = connect_host.encode("idna")
|
||||||
|
except UnicodeError:
|
||||||
|
host_bytes = connect_host.encode("ascii", errors="ignore")
|
||||||
|
if len(host_bytes) > 255:
|
||||||
|
await send_reply(client_writer, REP_HOST_UNREACHABLE)
|
||||||
|
return
|
||||||
|
connect_req.append(len(host_bytes))
|
||||||
|
connect_req.extend(host_bytes)
|
||||||
|
else:
|
||||||
|
connect_req.extend(connect_raw_addr)
|
||||||
|
connect_req.extend(struct.pack("!H", dst_port))
|
||||||
|
|
||||||
|
upstream_writer.write(connect_req)
|
||||||
|
await upstream_writer.drain()
|
||||||
|
|
||||||
|
up_resp = await read_exact(upstream_reader, 4)
|
||||||
|
if up_resp[0] != SOCKS_VERSION:
|
||||||
|
await send_reply(client_writer, REP_GENERAL_FAILURE)
|
||||||
|
return
|
||||||
|
|
||||||
|
up_rep = up_resp[1]
|
||||||
|
up_atyp = up_resp[3]
|
||||||
|
if up_atyp == ATYP_IPV4:
|
||||||
|
_ = await read_exact(upstream_reader, 4)
|
||||||
|
elif up_atyp == ATYP_IPV6:
|
||||||
|
_ = await read_exact(upstream_reader, 16)
|
||||||
|
elif up_atyp == ATYP_DOMAIN:
|
||||||
|
ln = (await read_exact(upstream_reader, 1))[0]
|
||||||
|
_ = await read_exact(upstream_reader, ln)
|
||||||
|
else:
|
||||||
|
await send_reply(client_writer, REP_GENERAL_FAILURE)
|
||||||
|
return
|
||||||
|
_ = await read_exact(upstream_reader, 2)
|
||||||
|
|
||||||
|
if up_rep != REP_SUCCEEDED:
|
||||||
|
await send_reply(client_writer, up_rep if up_rep <= REP_ADDRESS_TYPE_NOT_SUPPORTED else REP_GENERAL_FAILURE)
|
||||||
|
return
|
||||||
|
|
||||||
|
await send_reply(client_writer, REP_SUCCEEDED)
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
pipe(client_reader, upstream_writer),
|
||||||
|
pipe(upstream_reader, client_writer),
|
||||||
|
)
|
||||||
|
|
||||||
|
except asyncio.IncompleteReadError:
|
||||||
|
pass
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"error peer={peer} err={exc}")
|
||||||
|
try:
|
||||||
|
await send_reply(client_writer, REP_GENERAL_FAILURE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
client_writer.close()
|
||||||
|
await client_writer.wait_closed()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> int:
|
||||||
|
server = await asyncio.start_server(handle_client, LISTEN_HOST, LISTEN_PORT)
|
||||||
|
sockets = ", ".join(str(s.getsockname()) for s in (server.sockets or []))
|
||||||
|
log(f"listening on {sockets}")
|
||||||
|
|
||||||
|
stop_event = asyncio.Event()
|
||||||
|
|
||||||
|
def _stop() -> None:
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
try:
|
||||||
|
loop.add_signal_handler(sig, _stop)
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await stop_event.wait()
|
||||||
|
server.close()
|
||||||
|
await server.wait_closed()
|
||||||
|
log("shutdown complete")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
raise SystemExit(asyncio.run(main()))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
raise SystemExit(0)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
# Keep an explicit peer to darki2p so dark3proxy always has a Ygg path.
|
||||||
|
Peers: [
|
||||||
|
tls://10.5.0.2:10654
|
||||||
|
]
|
||||||
|
|
||||||
|
InterfacePeers: {}
|
||||||
|
|
||||||
|
# No public listener needed for dark3proxy use-case.
|
||||||
|
Listen: []
|
||||||
|
|
||||||
|
AdminListen: unix:///var/run/yggdrasil.sock
|
||||||
|
|
||||||
|
# Disable multicast discovery for deterministic container behavior.
|
||||||
|
MulticastInterfaces: []
|
||||||
|
|
||||||
|
AllowedPublicKeys: []
|
||||||
|
|
||||||
|
PublicKey:
|
||||||
|
PrivateKey:
|
||||||
|
|
||||||
|
IfName: auto
|
||||||
|
IfMTU: 65535
|
||||||
|
|
||||||
|
NodeInfoPrivacy: false
|
||||||
|
NodeInfo: {}
|
||||||
|
}
|
||||||
+1
-1
Submodule PopuraDNS updated: f76f02bb72...350f6d3b5d
@@ -4,24 +4,53 @@ Welcome to darkproxy...
|
|||||||
|
|
||||||
The stack now uses safer defaults for production:
|
The stack now uses safer defaults for production:
|
||||||
|
|
||||||
* Sensitive passwords are loaded from Docker secrets instead of inline values.
|
* Service admin/web passwords are loaded from Docker secrets instead of inline values.
|
||||||
* Admin/monitoring ports are bound to `127.0.0.1` on the host.
|
* Admin/monitoring ports are bound to `127.0.0.1` on the host.
|
||||||
* 3proxy now requires authentication (`auth strong`).
|
* The published SOCKS entrypoint now requires credentials from a Docker secret.
|
||||||
|
* CoreDNS recursive resolution for the catch-all zone is restricted to trusted/internal networks.
|
||||||
|
* Pi-hole ARP cache parsing is disabled in Docker (`FTLCONF_database_network_parseARPcache=false`) to prevent recurring netlink `neigh`/ARP errors.
|
||||||
|
* Fixed `10.5.0.x` service IPs are retained for the stack's internal routing/DNS design, but host/LAN-specific values now default to portable settings and can be overridden with environment variables.
|
||||||
|
* Optional integrations that need operator-specific credentials, such as Tailscale, are no longer enabled with placeholder defaults in the base stack.
|
||||||
|
|
||||||
Before starting in production, set these secret files:
|
Before starting in production, set these secret files:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
printf 'proxyuser:CL:YOUR_STRONG_PROXY_PASSWORD\n' > secrets/3proxy_users.txt
|
|
||||||
printf 'YOUR_STRONG_PIHOLE_PASSWORD\n' > secrets/pihole_webpassword.txt
|
printf 'YOUR_STRONG_PIHOLE_PASSWORD\n' > secrets/pihole_webpassword.txt
|
||||||
printf 'YOUR_STRONG_GRAFANA_PASSWORD\n' > secrets/grafana_admin_password.txt
|
printf 'proxyuser:REPLACE_WITH_A_LONG_RANDOM_PASSWORD\n' > secrets/3proxy_users.txt
|
||||||
|
printf 'REPLACE_WITH_A_LONG_RANDOM_NAMECOIN_RPC_PASSWORD\n' > secrets/namecoin_rpc_password.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
SOCKS clients must now authenticate to 3proxy with credentials from
|
Optional Tailscale note:
|
||||||
`secrets/3proxy_users.txt`.
|
|
||||||
|
* The `tailscale` service is now behind the Compose `tailscale` profile.
|
||||||
|
* It is not started by default.
|
||||||
|
* To enable it, export a real auth key and start that profile explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export TS_AUTHKEY=tskey-your-real-key
|
||||||
|
docker compose --profile tailscale up -d tailscale
|
||||||
|
```
|
||||||
|
|
||||||
|
Release default note for `.zil`:
|
||||||
|
|
||||||
|
* `zildns` now uses explicit public resolvers inside its container so its Zilliqa RPC lookups do not depend on Docker's embedded DNS path.
|
||||||
|
* This change is limited to the `zildns` container; the stack's internal fixed `10.5.0.x` service IP design remains unchanged.
|
||||||
|
|
||||||
|
SOCKS auth note:
|
||||||
|
|
||||||
|
* `dark3proxy` now reads credentials from `secrets/3proxy_users.txt`.
|
||||||
|
* Each non-comment line in that secret must be `username:password`.
|
||||||
|
* If you intentionally want an internal-only unauthenticated proxy, set `PROXY_REQUIRE_AUTH=false` for `dark3proxy` and remove the public `2000:1080` port mapping.
|
||||||
|
* The router now defaults to **no auth bypass CIDRs**. If you want trusted subnets to skip SOCKS auth, set `ROUTER_NO_AUTH_CIDRS` in your shell or `.env`, for example `ROUTER_NO_AUTH_CIDRS=192.168.1.0/24`.
|
||||||
|
|
||||||
|
Pi-hole note: disabling ARP parsing avoids noisy `Failed to read ARP cache`
|
||||||
|
messages in containerized setups where neighbor-table netlink operations are not
|
||||||
|
supported. This only affects Pi-hole's network-table enrichment, not DNS
|
||||||
|
blocking/forwarding behavior.
|
||||||
|
|
||||||
This container runs the following vontainers in the stack.
|
This container runs the following vontainers in the stack.
|
||||||
|
|
||||||
3proxy-eagle - Socks5/http proxy
|
dark3proxy - SOCKS5/http proxy with hostname-preserving router
|
||||||
- DNS
|
- DNS
|
||||||
- pihole - ad blocking and metrics
|
- pihole - ad blocking and metrics
|
||||||
- PopuraDNS - non-recursive DNS forwards requests to the following:
|
- PopuraDNS - non-recursive DNS forwards requests to the following:
|
||||||
@@ -34,13 +63,67 @@ This container runs the following vontainers in the stack.
|
|||||||
* Socks5 server for I2P
|
* Socks5 server for I2P
|
||||||
- Tor container
|
- Tor container
|
||||||
|
|
||||||
|
## SOCKS ports on host
|
||||||
|
|
||||||
|
Public proxy access is exposed on a single host port:
|
||||||
|
|
||||||
|
* `<host-ip>:2000` -> `dark3proxy` SOCKS (`dark3proxy:1080`), externally reachable.
|
||||||
|
- Intended as the single client entrypoint for clearnet, onion, and Ygg/I2P routing rules.
|
||||||
|
|
||||||
|
Local admin endpoint:
|
||||||
|
|
||||||
|
* `127.0.0.1:2002` -> 3proxy admin (`dark3proxy:8161`), host-local only.
|
||||||
|
* `127.0.0.1:2003` -> Pi-hole web UI (`darkpihole:80`), host-local by default.
|
||||||
|
* `127.0.0.1:2004` -> status dashboard (`darkstatus:8080`), host-local by default.
|
||||||
|
|
||||||
|
These host-local admin bindings can be overridden with environment variables when
|
||||||
|
needed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export PIHOLE_WEB_BIND_HOST=192.168.1.31
|
||||||
|
export PIHOLE_WEB_BIND_PORT=2003
|
||||||
|
export STATUS_DASHBOARD_BIND_HOST=192.168.1.31
|
||||||
|
export STATUS_DASHBOARD_BIND_PORT=2004
|
||||||
|
```
|
||||||
|
|
||||||
|
Router internals for `:2000`:
|
||||||
|
|
||||||
|
* Front-end process: `3proxy/socks_router.py` (SOCKS5), keeps destination hostnames intact.
|
||||||
|
* Upstream dispatch:
|
||||||
|
- `*.onion` -> Tor SOCKS (`10.5.0.7:9050`)
|
||||||
|
- `*.i2p` -> i2pd SOCKS (`10.5.0.2:4447`)
|
||||||
|
- `*.ygg`, `*.meshname`, `*.meship`, and `200::/7` -> local Ygg-guarded SOCKS (`127.0.0.1:1085`)
|
||||||
|
- all other targets -> Tor SOCKS (`10.5.0.7:9050`)
|
||||||
|
* The primary 3proxy instance still enforces ACL/parent rules internally, with SOCKS moved to internal port `1088` to avoid collision with the router front-end.
|
||||||
|
|
||||||
|
Additional non-proxy endpoint:
|
||||||
|
|
||||||
|
* `127.0.0.1:2006/udp` -> Tor DNSPort (`darktor:9053`).
|
||||||
|
|
||||||
|
Quick checks:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Onion over the only published proxy port
|
||||||
|
curl --proxy-user proxyuser:REPLACE_WITH_A_LONG_RANDOM_PASSWORD --socks5-hostname <host-ip>:2000 -I http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion
|
||||||
|
|
||||||
|
# Clearnet over the only published proxy port
|
||||||
|
curl --proxy-user proxyuser:REPLACE_WITH_A_LONG_RANDOM_PASSWORD --socks5-hostname <host-ip>:2000 -I https://example.com
|
||||||
|
```
|
||||||
|
|
||||||
The system offers a DNS that resolves many darknet and clearnet IP's. The
|
The system offers a DNS that resolves many darknet and clearnet IP's. The
|
||||||
stack now handles:
|
stack now handles:
|
||||||
|
|
||||||
|
Local resolution model (important):
|
||||||
|
|
||||||
|
* DNS queries are resolved inside this Docker stack on your host (`pihole -> CoreDNS -> local resolver services`).
|
||||||
|
* The stack does not forward DNS queries to public DNS providers for supported darknet/blockchain TLDs.
|
||||||
|
* Some namespaces still require blockchain RPC data sources for authoritative records (see limitations below).
|
||||||
|
|
||||||
* `*.onion` – forwarded to Tor's DNSPort running in the `darktor` container at
|
* `*.onion` – forwarded to Tor's DNSPort running in the `darktor` container at
|
||||||
10.5.0.7:9053.
|
10.5.0.7:9053.
|
||||||
* `*.i2p` – served by the built-in DNS server inside the `darki2p` container
|
* `*.i2p` – resolved by local `i2pdns` bridge service (`10.5.0.16:53`).
|
||||||
(10.5.0.2:53) which is enabled via the `[dns]` section in `i2pd.conf`.
|
- `i2pdns` synthesizes deterministic private A records and writes IP<->hostname mappings.
|
||||||
|
- `dark3proxy` consumes that map so synthetic IP requests are translated back to `.i2p` hostnames and routed to i2pd SOCKS (`10.5.0.2:4447`).
|
||||||
|
|
||||||
You can query these directly from any container on the `darkproxy` network, for
|
You can query these directly from any container on the `darkproxy` network, for
|
||||||
example:
|
example:
|
||||||
@@ -62,10 +145,9 @@ docker run --rm --network darkproxy infoblox/dig dig @10.5.0.4 TXT vitalik.eth
|
|||||||
docker run --rm --network darkproxy infoblox/dig dig @10.5.0.4 example.bit
|
docker run --rm --network darkproxy infoblox/dig dig @10.5.0.4 example.bit
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** the CoreDNS service at `10.5.0.4` no longer enforces an ACL for
|
> **Note:** the CoreDNS service at `10.5.0.4` enforces an ACL on the root zone.
|
||||||
> the root zone – it will happily perform recursive lookups for any client.
|
> Recursive lookups are limited to loopback, RFC1918/private ranges, and the
|
||||||
> Queries from your host should now work without the “refused to do a recursive
|
> Yggdrasil/internal subnets used by this stack.
|
||||||
> query” error. If you later reintroduce an ACL, update this note accordingly.
|
|
||||||
|
|
||||||
### Host-side quick tests (no extra images)
|
### Host-side quick tests (no extra images)
|
||||||
|
|
||||||
@@ -90,19 +172,37 @@ The stack now provides DNS resolution for a comprehensive set of alternative TLD
|
|||||||
| TLD | Resolver | Purpose |
|
| TLD | Resolver | Purpose |
|
||||||
|-----|----------|---------|
|
|-----|----------|---------|
|
||||||
| `.onion`, `.exit`, `.onion4`, `.onion6` | Tor DNSPort (10.5.0.7:9053) | Tor hidden services |
|
| `.onion`, `.exit`, `.onion4`, `.onion6` | Tor DNSPort (10.5.0.7:9053) | Tor hidden services |
|
||||||
| `.i2p` | i2pd DNS (10.5.0.2:53) | I2P eepsite names |
|
| `.i2p` | i2pdns bridge (10.5.0.16:53) + i2pd SOCKS (10.5.0.2:4447) | I2P eepsite names |
|
||||||
| `.eth` | ensdns (10.5.0.11) via CoreDNS | Ethereum Name Service (local resolver) |
|
| `.eth` | ensdns (10.5.0.11) via CoreDNS | Ethereum Name Service (local resolver) |
|
||||||
| `.bit` | namecoindns (10.5.0.12) via CoreDNS | Namecoin names (local resolver service) |
|
| `.bit` | namecoindns (10.5.0.12) via CoreDNS | Namecoin names (local resolver service) |
|
||||||
| `.alt` | alt-root 185.121.177.177 | Alternative DNS root |
|
| `.alt` | local unbound (10.5.0.5) via CoreDNS | Alternative DNS root (local-only fallback mode) |
|
||||||
| `.loki` | Cloudflare 1.1.1.1 | Lokinet privacy network |
|
| `.loki` | local lokinet (10.5.0.14) via CoreDNS | Lokinet privacy network (no Cloudflare/public DNS forwarder dependency) |
|
||||||
| `.zil` | Cloudflare 1.1.1.1 | Zilliqa blockchain |
|
| `.zil` | zildns (10.5.0.15) via CoreDNS | Zilliqa blockchain (local resolver service; no Cloudflare DNS dependency) |
|
||||||
| `.web3` | Cloudflare 1.1.1.1 | General blockchain namespace |
|
| `.web3` | local unbound (10.5.0.5) via CoreDNS | General blockchain namespace (local-only fallback mode) |
|
||||||
| Alfis TLDs | Alfis (10.5.0.3) | `.anon`, `.btn`, `.conf`, `.index`, `.merch`, `.mirror`, `.mob`, `.screen`, `.srv`, `.ygg` |
|
| Alfis TLDs | Alfis (10.5.0.3) | `.anon`, `.btn`, `.conf`, `.index`, `.merch`, `.mirror`, `.mob`, `.screen`, `.srv`, `.ygg` |
|
||||||
| EmerCoin TLDs | EmerCoin (10.5.0.9) | `.emc`, `.coin`, `.lib`, `.bazar`, `.enum` |
|
| EmerCoin TLDs | EmerCoin (10.5.0.9) | `.emc`, `.coin`, `.lib`, `.bazar`, `.enum` |
|
||||||
| OpenNIC TLDs | OpenNIC public resolvers | `.bbs`, `.chan`, `.cyb`, `.dyn`, `.epic`, `.geek`, `.gopher`, `.indy`, `.libre`, `.neo`, `.null`, `.o`, `.oss`, `.oz`, `.parody`, `.pirate`, `.fur`, `.ku`, `.rm`, `.te`, `.ti`, `.uu`, `.ko` |
|
| OpenNIC TLDs | local unbound (10.5.0.5) via CoreDNS | `.bbs`, `.chan`, `.cyb`, `.dyn`, `.epic`, `.geek`, `.gopher`, `.indy`, `.libre`, `.neo`, `.null`, `.o`, `.oss`, `.oz`, `.parody`, `.pirate`, `.fur`, `.ku`, `.rm`, `.te`, `.ti`, `.uu`, `.ko` (local-only fallback mode) |
|
||||||
| Clearnet | Unbound (10.5.0.5) | All other domains via recursive resolution |
|
| Clearnet | Unbound (10.5.0.5) | All other domains via recursive resolution |
|
||||||
|
|
||||||
|
|
||||||
|
## Local-only DNS mode limitations
|
||||||
|
|
||||||
|
Darkproxy now avoids direct public DNS forwarders for `.alt`, `.zil`, `.web3`, OpenNIC TLDs, and Lokinet fallback DNS.
|
||||||
|
|
||||||
|
Resolution still happens locally in this stack. The remaining external dependency
|
||||||
|
surface is blockchain RPC/data access for record lookup, not public DNS
|
||||||
|
forwarding.
|
||||||
|
|
||||||
|
Important limitations still apply for authoritative data:
|
||||||
|
|
||||||
|
* `.eth` requires an Ethereum JSON-RPC backend. By default this stack uses `https://ethereum-rpc.publicnode.com` in `ensdns`.
|
||||||
|
* `.alt`, OpenNIC TLDs, and `.web3` are not in the ICANN root. In local-only fallback mode they are sent to local Unbound, so they usually return `NXDOMAIN` unless you provide your own authoritative/local resolver backend for those namespaces.
|
||||||
|
* `.zil` is resolved through local `zildns`, which queries ZNS using `https://api.zilliqa.com` by default. This is a Zilliqa RPC lookup path (on-chain resolution), not a Cloudflare DNS forwarder path. You can override the RPC endpoint with `ZILDNS_ZNS_URL` in `docker-compose.yml`.
|
||||||
|
* `zildns` supports `ZILDNS_CACHE_SECONDS` (record cache duration) and `ZILDNS_PREWARM_DOMAIN` (startup warm-up query) to reduce first-query latency.
|
||||||
|
* For browser convenience, `A` lookups may return a CNAME to a public IPFS gateway when a `.zil` content hash exists. That gateway alias is separate from DNS resolution itself.
|
||||||
|
* `.loki` is resolved by local Lokinet, and Lokinet fallback DNS points to local Unbound (no Cloudflare/public DNS forwarder dependency).
|
||||||
|
|
||||||
|
|
||||||
## Configuration validation and drift tests
|
## Configuration validation and drift tests
|
||||||
|
|
||||||
A simple automation is included to help detect accidental changes to the
|
A simple automation is included to help detect accidental changes to the
|
||||||
@@ -110,11 +210,13 @@ compose and DNS configuration:
|
|||||||
|
|
||||||
1. **Local check** – run `scripts/validate-config.sh` (or its PowerShell equivalent).
|
1. **Local check** – run `scripts/validate-config.sh` (or its PowerShell equivalent).
|
||||||
- The script requires `docker` to be installed; it will generate a normalized
|
- The script requires `docker` to be installed; it will generate a normalized
|
||||||
manifest and compare it against `docker-compose.lock.yml`.
|
manifest and compare it against `docker-compose.lock.yml`.
|
||||||
- If the baseline file does not exist it will be created; commit the file after
|
- If the baseline file does not exist it will be created; commit the file after
|
||||||
review.
|
review.
|
||||||
- CoreDNS syntax is also validated using either a local `coredns` binary or a
|
- CoreDNS syntax is validated with the repo's compiled CoreDNS image so the
|
||||||
container.
|
custom `meshname` and `meship` plugins are available during the check.
|
||||||
|
- Unbound syntax is validated with the repo's Unbound image so the chrooted
|
||||||
|
runtime layout matches the stack.
|
||||||
|
|
||||||
2. **CI workflow** – `.github/workflows/config-validation.yml` is triggered on
|
2. **CI workflow** – `.github/workflows/config-validation.yml` is triggered on
|
||||||
pushes or pull requests affecting `docker-compose.yml` or `PopuraDNS/`.
|
pushes or pull requests affecting `docker-compose.yml` or `PopuraDNS/`.
|
||||||
@@ -125,8 +227,8 @@ compose and DNS configuration:
|
|||||||
compose file, regenerate the baseline:
|
compose file, regenerate the baseline:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
scripts/validate-config.sh # fails with drift
|
docker compose config > docker-compose.lock.yml
|
||||||
cp generated.yml docker-compose.lock.yml
|
scripts/validate-config.sh
|
||||||
git add docker-compose.lock.yml
|
git add docker-compose.lock.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -180,10 +282,34 @@ records in Namecoin itself and wait for local `namecoind` sync completion.
|
|||||||
> `.bit` responses may be empty or incomplete.
|
> `.bit` responses may be empty or incomplete.
|
||||||
|
|
||||||
|
|
||||||
## Monitoring the proxy
|
## Local Lokinet (.loki) resolver
|
||||||
|
|
||||||
A lightweight Prometheus/Grafana stack is included to expose 3proxy metrics
|
`.loki` lookups are resolved by a local Lokinet daemon path:
|
||||||
and give you visibility into traffic volumes, connected sessions, etc.
|
|
||||||
|
* CoreDNS forwards `loki.:53` to `10.5.0.14:53` (`lokinet`).
|
||||||
|
* `lokinet` resolves `.loki` names through the Lokinet network.
|
||||||
|
|
||||||
|
Quick test:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LOKI_ADDR="$(docker logs darklokinet 2>&1 | sed -n 's/.*endpoint:\([a-z0-9]\{52\}\.loki\).*/\1/p' | tail -n1)"
|
||||||
|
docker exec darkpihole dig @10.5.0.4 A "$LOKI_ADDR"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Lokinet may need a short bootstrap period after startup before
|
||||||
|
> `.loki` names resolve. Older sample names such as `oxen.loki` are no longer a
|
||||||
|
> reliable health check; resolving the container's self-published `.loki`
|
||||||
|
> address is a better validation of the local Lokinet path.
|
||||||
|
|
||||||
|
|
||||||
|
## Monitoring and stack status
|
||||||
|
|
||||||
|
Two lightweight tools are included for visibility:
|
||||||
|
|
||||||
|
* `3proxy_exporter` exposes 3proxy counters in Prometheus format for external
|
||||||
|
scraping.
|
||||||
|
* `status_dashboard` serves a local-only status page for the whole stack,
|
||||||
|
including container state, probe results, proxy usernames, and recent logs.
|
||||||
|
|
||||||
### how it works
|
### how it works
|
||||||
|
|
||||||
@@ -191,34 +317,39 @@ and give you visibility into traffic volumes, connected sessions, etc.
|
|||||||
status socket. Samples look like `PROXY CONNS 12` or `SOCKS IN 345`.
|
status socket. Samples look like `PROXY CONNS 12` or `SOCKS IN 345`.
|
||||||
* `monitor/exporter.py` polls that socket every few seconds and exports
|
* `monitor/exporter.py` polls that socket every few seconds and exports
|
||||||
the counters on HTTP port **9100** in Prometheus format.
|
the counters on HTTP port **9100** in Prometheus format.
|
||||||
* The `docker-compose.yml` file now defines three new services:
|
* `monitor/status_dashboard.py` talks to the local Docker socket, runs targeted
|
||||||
`3proxy_exporter`, `prometheus` and `grafana`.
|
DNS and SOCKS checks, and serves both HTML and JSON for stack status.
|
||||||
Prometheus scrapes the exporter and Grafana points at Prometheus as a data
|
|
||||||
source.
|
|
||||||
|
|
||||||
### building & running
|
### building & running
|
||||||
|
|
||||||
The exporter lives in `monitor/`; build the image and start the stack:
|
The monitoring utilities live in `monitor/`; build the images and start the
|
||||||
|
stack:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# build everything including the new services
|
# build the exporter and status dashboard
|
||||||
docker-compose build 3proxy_exporter prometheus grafana
|
docker-compose build 3proxy_exporter status_dashboard
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Prometheus will be accessible on port **9090**, Grafana on **3000** (admin
|
By default the status page is available on **http://127.0.0.1:2004/** and the
|
||||||
password `secret`). Use Grafana to create or import dashboards – a simple
|
raw JSON is available on **http://127.0.0.1:2004/api/status**. If you override
|
||||||
example JSON is provided in `monitor/3proxy-dashboard.json`. You can import
|
the bind host/port via `STATUS_DASHBOARD_BIND_HOST` or
|
||||||
that file directly or build your own panels using metrics such as
|
`STATUS_DASHBOARD_BIND_PORT`, use that address instead.
|
||||||
`proxy_conns` and `proxy_bytes_in`.
|
|
||||||
|
If you want external dashboards or alerts, point a Prometheus-compatible
|
||||||
|
collector at the exporter and use metrics such as `proxy_conns` and
|
||||||
|
`proxy_bytes_in`.
|
||||||
|
|
||||||
### tips
|
### tips
|
||||||
|
|
||||||
* Alerts can be added in Prometheus rules, e.g. fire when `proxy_conns`
|
* Alerts can be added in your external Prometheus rules, e.g. fire when
|
||||||
exceeds a threshold for several minutes.
|
`proxy_conns` exceeds a threshold for several minutes.
|
||||||
* If you don’t want the full stack, you can still query the monitor port
|
* You can still query the monitor port directly with `nc`; nothing in the
|
||||||
directly with `nc`; nothing in the proxy depends on the exporter.
|
proxy depends on the exporter.
|
||||||
|
* The status dashboard can show logs and configured proxy usernames, so keep it
|
||||||
|
behind a trusted admin network or add your own access controls before
|
||||||
|
exposing it broadly.
|
||||||
* The `scripts/validate-config.sh` script warns if a 3proxy config lacks a
|
* The `scripts/validate-config.sh` script warns if a 3proxy config lacks a
|
||||||
`monitor` line.
|
`monitor` line.
|
||||||
|
|
||||||
@@ -235,8 +366,12 @@ any warnings or errors. The script covers:
|
|||||||
* CoreDNS/PopuraDNS Corefile syntax
|
* CoreDNS/PopuraDNS Corefile syntax
|
||||||
* Unbound configuration syntax
|
* Unbound configuration syntax
|
||||||
* Presence of required secret files
|
* Presence of required secret files
|
||||||
|
* Portable host-local defaults for Pi-hole, the status dashboard, and router auth bypass CIDRs
|
||||||
* Simple 3proxy configuration sanity (presence of `socks`/`proxy` rules)
|
* Simple 3proxy configuration sanity (presence of `socks`/`proxy` rules)
|
||||||
* Detection of `:latest` image tags (pin to fixed versions)
|
* Detection of mutable `:latest` image tags in deployment compose files
|
||||||
|
* Verification that critical resolver/backend services keep explicit healthchecks
|
||||||
|
* Live CoreDNS smoke checks for `.onion`, `.i2p`, `.eth`, and `.zil`
|
||||||
|
* Verification that Tailscale stays opt-in and does not ship a placeholder auth key
|
||||||
* Verification that restart policies exist
|
* Verification that restart policies exist
|
||||||
|
|
||||||
The GitHub Actions workflow also includes a **smoke test** job that spins up
|
The GitHub Actions workflow also includes a **smoke test** job that spins up
|
||||||
@@ -252,5 +387,3 @@ running `docker run ...` commands to exercise the images, scanning the
|
|||||||
Run the script locally via `./scripts/validate-config.sh` (or
|
Run the script locally via `./scripts/validate-config.sh` (or
|
||||||
`.scripts/validate-config.ps1` on Windows) and review the output carefully.
|
`.scripts/validate-config.ps1` on Windows) and review the output carefully.
|
||||||
CI will execute the same validations automatically on pull requests.
|
CI will execute the same validations automatically on pull requests.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
FROM alpine:3.20 AS code-cloner
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
RUN git clone --depth 1 https://github.com/Revertron/Alfis.git
|
||||||
|
|
||||||
|
FROM rust:slim-bullseye AS builder
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends pkg-config libcairo2-dev ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=code-cloner /src/Alfis /src
|
||||||
|
WORKDIR /src
|
||||||
|
RUN cargo build --release --no-default-features --features="doh"
|
||||||
|
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates libcairo2 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /src/target/release/alfis /alfis
|
||||||
|
COPY --from=builder /src/alfis.toml /storage/alfis.toml
|
||||||
|
|
||||||
|
EXPOSE 4244/tcp
|
||||||
|
EXPOSE 53/tcp
|
||||||
|
EXPOSE 53/udp
|
||||||
|
|
||||||
|
WORKDIR /storage
|
||||||
|
VOLUME ["/storage"]
|
||||||
|
CMD ["/alfis", "-c", "/storage/alfis.toml"]
|
||||||
+59
-23
@@ -3,41 +3,77 @@
|
|||||||
#
|
#
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.arm.yml up --build
|
# docker compose -f docker-compose.yml -f docker-compose.arm.yml up --build
|
||||||
#
|
#
|
||||||
# This file adds `platform: linux/arm64` hints to services so the correct
|
# The base compose is AMD64-only. This override is the ARM-only mode.
|
||||||
# architecture is selected when running on an ARM host. It does **not** replace
|
# Apply this file on ARM hosts to switch services to linux/arm64 and ARM builds.
|
||||||
# any images; if an image is not available for ARM (e.g. alfis, unbound, the
|
# If an image/build does not support ARM, startup should fail rather than silently
|
||||||
# prebuilt tor_yggdrasil) Docker will attempt emulation via QEMU or fall back to
|
# using emulation.
|
||||||
# amd64.
|
|
||||||
#
|
|
||||||
# You can also modify this file to swap in ARM-specific images or build
|
|
||||||
# instructions if you have them.
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
dark3proxy:
|
dark3proxy:
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
i2pd_yggdrasil:
|
3proxy_exporter:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
i2pdns:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
i2pd_yggdrasil:
|
||||||
|
# Build from local ARM context in this repo to avoid read-only upstream mirror constraints.
|
||||||
|
build:
|
||||||
|
context: ./i2pd_yggdrasil_arm
|
||||||
|
dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
tor_yggdrasil:
|
tor_yggdrasil:
|
||||||
|
build:
|
||||||
|
context: ./tor_yggdrasil_docker
|
||||||
|
dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
alfis:
|
alfis:
|
||||||
|
image: darkproxy-alfis:arm64
|
||||||
|
build:
|
||||||
|
context: ./alfis_arm
|
||||||
|
dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
coredns:
|
coredns:
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
|
zildns:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
unbound:
|
unbound:
|
||||||
|
image: darkproxy-unbound:arm64
|
||||||
|
build:
|
||||||
|
context: ./unbound_arm
|
||||||
|
dockerfile: Dockerfile
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
pihole:
|
pihole:
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
emc:
|
emc:
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
geth:
|
ensdns:
|
||||||
platform: linux/arm64
|
platform: linux/arm64
|
||||||
|
|
||||||
# networks, volumes, and secrets are inherited from the base compose file
|
lokinet:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
namecoind:
|
||||||
|
image: sevenrats/namecoin-core@sha256:c5275f813f85f5e1b1f43e4ca044b35c8f622457710a87c1dcb51733e42b59d1
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
namecoindns:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
tailscale:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
platform: linux/arm64
|
||||||
|
|
||||||
|
# networks, volumes, and secrets are inherited from the base compose file
|
||||||
|
|||||||
+709
-269
@@ -1,269 +1,709 @@
|
|||||||
services:
|
name: darkproxy
|
||||||
dark3proxy:
|
services:
|
||||||
container_name: dark3proxy
|
3proxy_exporter:
|
||||||
image: host60/darkproxy-dark3proxy:v1.0
|
build:
|
||||||
build:
|
context: /home/blade/darkproxy/monitor
|
||||||
context: ./3proxy/.
|
dockerfile: Dockerfile
|
||||||
dns:
|
container_name: dark3proxy-exporter
|
||||||
- "10.5.0.6"
|
depends_on:
|
||||||
dns_search: internal.namespace #namespace used in internal DNS
|
dark3proxy:
|
||||||
volumes:
|
condition: service_healthy
|
||||||
- "./3proxy/first-instanse.cfg:/etc/3proxy/first-instanse.cfg"
|
required: true
|
||||||
- "./3proxy/second-instanse.cfg:/etc/3proxy/second-instanse.cfg"
|
image: darkproxy-3proxy-exporter:local
|
||||||
restart: unless-stopped
|
networks:
|
||||||
sysctls:
|
darkproxy:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
ipv4_address: 10.5.0.20
|
||||||
ports:
|
platform: linux/amd64
|
||||||
- 100.117.196.19:2000:1080 # socks port
|
security_opt:
|
||||||
- 100.117.196.19:2002:8161 # 3proxy-eagle web page
|
- no-new-privileges:true
|
||||||
networks:
|
alfis:
|
||||||
darkproxy:
|
container_name: darkalfis
|
||||||
ipv4_address: 10.5.0.8
|
dns:
|
||||||
|
- 10.5.0.6
|
||||||
i2pd_yggdrasil:
|
image: cofob/alfis@sha256:0c5788b1e409557bb814dc2d055d584db8c48b3d42b375bf13feb59583af0585
|
||||||
container_name: darki2p
|
networks:
|
||||||
# image: i2pd_yggdrasil:latest
|
darkproxy:
|
||||||
image: host60/i2pd_yggdrasil:v1.0
|
ipv4_address: 10.5.0.3
|
||||||
privileged: true
|
platform: linux/amd64
|
||||||
cap_add:
|
restart: unless-stopped
|
||||||
- NET_ADMIN
|
security_opt:
|
||||||
devices:
|
- no-new-privileges:true
|
||||||
- /dev/net/tun:/dev/net/tun
|
stop_grace_period: 10s
|
||||||
ports:
|
sysctls:
|
||||||
# I2P Service Ports
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
# - "2827:2827" # BOB Bridge
|
volumes:
|
||||||
# - "4444:4444" # HTTP Proxy
|
- type: volume
|
||||||
# - "4447:4447" # SOCKS Proxy
|
source: darkalfis_data
|
||||||
- "100.117.196.19:2001:7070" # Webconsole
|
target: /storage
|
||||||
# - "7650:7650" # I2PControl
|
volume: {}
|
||||||
# - "7654:7654" # I2CP
|
coredns:
|
||||||
# - "7656:7656" # SAM Bridge (TCP)
|
build:
|
||||||
- "10765:10765" # Main I2P Listener
|
context: /home/blade/darkproxy/PopuraDNS
|
||||||
# [dns] section enabled in i2pd.conf; DNS port is 53 (accessible internally at 10.5.0.2:53)
|
dockerfile: Dockerfile
|
||||||
# mapping to host is possible if desired, e.g. 10853:53/udp
|
cpus: 1
|
||||||
# Yggdrasil Ports
|
container_name: darkdns
|
||||||
- "10654:10654" # Yggdrasil Listener
|
logging:
|
||||||
environment:
|
driver: json-file
|
||||||
tz: /run/secrets/tz
|
options:
|
||||||
secrets:
|
max-file: "3"
|
||||||
- tz
|
max-size: 10m
|
||||||
restart: unless-stopped
|
mem_limit: "268435456"
|
||||||
healthcheck:
|
mem_reservation: "67108864"
|
||||||
test: ["CMD", "bash", "-c", "pgrep i2pd && pgrep yggdrasil"]
|
networks:
|
||||||
interval: 30s
|
darkproxy:
|
||||||
timeout: 10s
|
ipv4_address: 10.5.0.4
|
||||||
retries: 3
|
platform: linux/amd64
|
||||||
start_period: 60s
|
restart: unless-stopped
|
||||||
sysctls:
|
security_opt:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- no-new-privileges:true
|
||||||
mac_address: ce:22:b8:0e:6e:78
|
sysctls:
|
||||||
networks:
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
darkproxy:
|
dark3proxy:
|
||||||
ipv4_address: 10.5.0.2
|
build:
|
||||||
|
context: /home/blade/darkproxy/3proxy
|
||||||
tor_yggdrasil:
|
dockerfile: Dockerfile
|
||||||
image: host60/tor_yggdrasil:v1.0
|
cap_add:
|
||||||
# image: tor_yggdrasil:latest
|
- NET_ADMIN
|
||||||
# container_name: tor_over_yggdrasil
|
cpus: 1
|
||||||
container_name: darktor
|
container_name: dark3proxy
|
||||||
# platform removed; image is amd64-only
|
devices:
|
||||||
cap_add:
|
- source: /dev/net/tun
|
||||||
- NET_ADMIN # Required for network tunnel management
|
target: /dev/net/tun
|
||||||
security_opt:
|
permissions: rwm
|
||||||
- no-new-privileges:true # Security enhancement - prevent privilege esca>
|
dns:
|
||||||
devices:
|
- 10.5.0.6
|
||||||
- /dev/net/tun:/dev/net/tun # TUN device for Yggdrasil network interface
|
dns_search:
|
||||||
ports:
|
- internal.namespace
|
||||||
# - "127.0.0.1:9050:9050/tcp" # SOCKS5 proxy port (Tor) for application-l>
|
environment:
|
||||||
- "10655:10655/tcp" # Yggdrasil peer connections
|
PROXY_REQUIRE_AUTH: "true"
|
||||||
secrets:
|
PROXY_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
- YGGDRASIL_GENERATE_KEYS
|
ROUTER_I2P_MAP_FILE: /var/lib/i2pdns/map.json
|
||||||
environment:
|
ROUTER_I2P_POOL_CIDR: 172.31.0.0/16
|
||||||
YGGDRASIL_GENERATE_KEYS: /run/secrets/YGGDRASIL_GENERATE_KEYS # Generate new Yggdrasil keys on startup
|
ROUTER_NO_AUTH_CIDRS: ""
|
||||||
restart: unless-stopped # Auto-restart unless manually stopped
|
ROUTER_REQUIRE_AUTH: "true"
|
||||||
dns:
|
ROUTER_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
- "10.5.0.6"
|
YGG_CONNECT_WAIT_SECONDS: "8"
|
||||||
sysctls:
|
healthcheck:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
test:
|
||||||
networks:
|
- CMD
|
||||||
darkproxy:
|
- bash
|
||||||
ipv4_address: 10.5.0.7
|
- -lc
|
||||||
|
- pgrep -f 'yggdrasil -useconffile' >/dev/null && pgrep -f '3proxy /etc/3proxy/second-instanse.cfg' >/dev/null && pgrep -f '3proxy /etc/3proxy/first-instanse.cfg' >/dev/null && pgrep -f 'python3 /usr/local/bin/socks_router.py' >/dev/null
|
||||||
alfis:
|
timeout: 10s
|
||||||
image: cofob/alfis
|
interval: 30s
|
||||||
# platform removed; image is amd64 only
|
retries: 3
|
||||||
container_name: darkalfis
|
start_period: 45s
|
||||||
volumes:
|
mem_limit: "268435456"
|
||||||
- darkalfis_data:/storage
|
mem_reservation: "67108864"
|
||||||
restart: unless-stopped
|
networks:
|
||||||
stop_grace_period: 10s
|
darkproxy:
|
||||||
dns:
|
ipv4_address: 10.5.0.8
|
||||||
- "10.5.0.6"
|
platform: linux/amd64
|
||||||
sysctls:
|
ports:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- mode: ingress
|
||||||
networks:
|
target: 1080
|
||||||
darkproxy:
|
published: "2000"
|
||||||
ipv4_address: 10.5.0.3
|
protocol: tcp
|
||||||
|
- mode: ingress
|
||||||
coredns:
|
host_ip: 127.0.0.1
|
||||||
# image: darkdns
|
target: 8161
|
||||||
image: host60/darkdns:v1.0
|
published: "2002"
|
||||||
build:
|
protocol: tcp
|
||||||
context: ./PopuraDNS
|
restart: unless-stopped
|
||||||
dockerfile: Dockerfile
|
secrets:
|
||||||
container_name: darkdns
|
- source: PROXY_USERS
|
||||||
restart: unless-stopped
|
target: /run/secrets/PROXY_USERS
|
||||||
sysctls:
|
security_opt:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- no-new-privileges:true
|
||||||
logging:
|
sysctls:
|
||||||
driver: "json-file"
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
options:
|
volumes:
|
||||||
max-size: "10m" # Maximum log file size
|
- type: bind
|
||||||
max-file: "3" # Maximum of 3 rotating log files
|
source: /home/blade/darkproxy/3proxy/first-instanse.cfg
|
||||||
networks:
|
target: /etc/3proxy/first-instanse.cfg
|
||||||
darkproxy:
|
bind:
|
||||||
ipv4_address: 10.5.0.4
|
create_host_path: true
|
||||||
|
- type: bind
|
||||||
unbound:
|
source: /home/blade/darkproxy/3proxy/second-instanse.cfg
|
||||||
container_name: darkunbound
|
target: /etc/3proxy/second-instanse.cfg
|
||||||
image: mvance/unbound:latest
|
bind:
|
||||||
# platform removed; image is amd64-only
|
create_host_path: true
|
||||||
sysctls:
|
- type: bind
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
source: /home/blade/darkproxy/3proxy/yggdrasil.conf
|
||||||
networks:
|
target: /etc/yggdrasil/yggdrasil.conf
|
||||||
darkproxy:
|
bind:
|
||||||
ipv4_address: 10.5.0.5
|
create_host_path: true
|
||||||
volumes:
|
- type: volume
|
||||||
- type: bind
|
source: i2p_dns_map
|
||||||
read_only: true
|
target: /var/lib/i2pdns
|
||||||
source: ./unbound/unbound.conf
|
read_only: true
|
||||||
target: /opt/unbound/etc/unbound/unbound.conf
|
volume: {}
|
||||||
- "./unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf"
|
emc:
|
||||||
- "./unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf"
|
cpus: 1
|
||||||
# ports:
|
command:
|
||||||
# - "5053:5053/tcp"
|
- -datadir=/emc/data
|
||||||
# - "5053:5053/udp"
|
- -conf=/emc/emercoin.conf
|
||||||
# healthcheck:
|
- -printtoconsole
|
||||||
# disable: true
|
container_name: darkemer
|
||||||
restart: unless-stopped
|
image: wg00/emercoin:0.8.4@sha256:b890987fb4b158305040dc76b32cd24ed5173dd7bc4aba983141ea0fe82e2996
|
||||||
|
mem_limit: "1073741824"
|
||||||
pihole:
|
mem_reservation: "268435456"
|
||||||
container_name: darkpihole
|
networks:
|
||||||
image: pihole/pihole:latest
|
darkproxy:
|
||||||
platform: linux/arm64
|
ipv4_address: 10.5.0.9
|
||||||
# For DHCP it is recommended to remove these ports and instead add: network_mode: "host"
|
platform: linux/amd64
|
||||||
ports:
|
restart: unless-stopped
|
||||||
# DNS Ports
|
security_opt:
|
||||||
# - "53:53/tcp"
|
- no-new-privileges:true
|
||||||
# - "53:53/udp"
|
stop_grace_period: 30s
|
||||||
# Default HTTP Port
|
volumes:
|
||||||
# - "80:80/tcp"
|
- type: volume
|
||||||
# Default HTTPs Port. FTL will generate a self-signed certificate
|
source: emc_data
|
||||||
# - "443:443/tcp
|
target: /emc/data
|
||||||
# Uncomment the line below if you are using Pi-hole as your DHCP server
|
volume: {}
|
||||||
# - "67:67/udp"
|
- type: bind
|
||||||
# Uncomment the line below if you are using Pi-hole as your NTP server
|
source: /etc/localtime
|
||||||
# - "123:123/udp"
|
target: /etc/localtime
|
||||||
- "100.117.196.19:2003:80/tcp" # pihole web port
|
read_only: true
|
||||||
environment:
|
bind:
|
||||||
# Set the appropriate timezone for your location (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), e.g:
|
create_host_path: true
|
||||||
TZ: 'America/Detroit'
|
ensdns:
|
||||||
# WEBPASSWORD: 'set a secure password here or it will be random'
|
build:
|
||||||
# FTLCONF_webserver_api_password: 'darkproxy'
|
context: /home/blade/darkproxy/ensdns
|
||||||
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
|
dockerfile: Dockerfile
|
||||||
# FTLCONF_dns_listeningMode: 'all'
|
cpus: 0.5
|
||||||
PIHOLE_DNS_: 10.5.0.4
|
container_name: darkens
|
||||||
TEMPERATUREUNIT: f
|
dns:
|
||||||
WEBTHEME: lcars
|
- 10.5.0.4
|
||||||
WEBPASSWORD: darkproxy
|
environment:
|
||||||
FTLCONF_dns_upstreams: coredns
|
ENS_RPC_URL: https://ethereum-rpc.publicnode.com
|
||||||
# Volumes store your data between container upgrades
|
ENSDNS_TTL: "60"
|
||||||
volumes:
|
healthcheck:
|
||||||
# For persisting Pi-hole's databases and common configuration file
|
test:
|
||||||
- './pihole/etc-pihole:/etc/pihole'
|
- CMD
|
||||||
# Uncomment the below if you have custom dnsmasq config files that you want to persist. Not needed for most starting fresh with Pi-hole v6. If you're upgrading from v5 you and have used this directory before, you should keep it enabled for the first v6 container start to allow for a complete migration. It can be removed afterwards. Needs environment variable FTLCONF_misc_etc_dnsmasq_d: 'true'
|
- python
|
||||||
- './pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
|
- -c
|
||||||
cap_add:
|
- import socket; from dnslib import DNSRecord; q=DNSRecord.question('vitalik.eth','TXT'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)
|
||||||
# See https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
|
timeout: 10s
|
||||||
# Required if you are using Pi-hole as your DHCP server, else not needed
|
interval: 1m0s
|
||||||
- NET_ADMIN
|
retries: 3
|
||||||
# Required if you are using Pi-hole as your NTP client to be able to set the host's system time
|
start_period: 15s
|
||||||
# - SYS_TIME
|
mem_limit: "268435456"
|
||||||
# Optional, if Pi-hole should get some more processing time
|
mem_reservation: "67108864"
|
||||||
- SYS_NICE
|
networks:
|
||||||
restart: unless-stopped # Recommended but not required (DHCP needs NET_ADMIN)
|
darkproxy:
|
||||||
sysctls:
|
ipv4_address: 10.5.0.11
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
platform: linux/amd64
|
||||||
healthcheck:
|
restart: unless-stopped
|
||||||
test: ["CMD", "dig", "+short", "+norecurse", "+timeout=2", "@127.0.0.1", "google.com"]
|
security_opt:
|
||||||
interval: 1m
|
- no-new-privileges:true
|
||||||
timeout: 10s
|
sysctls:
|
||||||
retries: 3
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
start_period: 30s
|
i2pd_yggdrasil:
|
||||||
networks:
|
build:
|
||||||
darkproxy:
|
context: /home/blade/darkproxy/i2pd_yggdrasil_docker/src
|
||||||
ipv4_address: 10.5.0.6
|
dockerfile: Dockerfile
|
||||||
|
cap_add:
|
||||||
geth:
|
- NET_ADMIN
|
||||||
image: ethereum/client-go:latest
|
cpus: 4
|
||||||
platform: linux/arm64
|
container_name: darki2p
|
||||||
container_name: darkgeth
|
devices:
|
||||||
command:
|
- source: /dev/net/tun
|
||||||
- --syncmode=light
|
target: /dev/net/tun
|
||||||
- --http
|
permissions: rwm
|
||||||
- --http.addr=0.0.0.0
|
environment:
|
||||||
- --http.port=8545
|
tz: /run/secrets/tz
|
||||||
- --http.api=eth,net,web3
|
healthcheck:
|
||||||
- --cache=256
|
test:
|
||||||
networks:
|
- CMD
|
||||||
darkproxy:
|
- bash
|
||||||
ipv4_address: 10.5.0.10
|
- -lc
|
||||||
restart: unless-stopped
|
- pgrep i2pd >/dev/null && pgrep yggdrasil >/dev/null && [ -s /var/lib/i2pd/addressbook/addresses.csv ]
|
||||||
stop_grace_period: 30s
|
timeout: 10s
|
||||||
healthcheck:
|
interval: 30s
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8545"]
|
retries: 3
|
||||||
interval: 30s
|
start_period: 15m0s
|
||||||
timeout: 10s
|
mem_limit: "2147483648"
|
||||||
retries: 3
|
mem_reservation: "536870912"
|
||||||
start_period: 120s
|
mac_address: ce:22:b8:0e:6e:78
|
||||||
sysctls:
|
networks:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.2
|
||||||
emc:
|
platform: linux/amd64
|
||||||
image: wg00/emercoin:0.8.4
|
ports:
|
||||||
platform: linux/arm64
|
- mode: ingress
|
||||||
container_name: darkemer
|
target: 7070
|
||||||
volumes:
|
published: "2001"
|
||||||
- emc_data:/emc
|
protocol: tcp
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- mode: ingress
|
||||||
networks:
|
target: 10765
|
||||||
darkproxy:
|
published: "10765"
|
||||||
ipv4_address: 10.5.0.9
|
protocol: tcp
|
||||||
restart: unless-stopped
|
- mode: ingress
|
||||||
stop_grace_period: 30s
|
target: 10654
|
||||||
|
published: "10654"
|
||||||
volumes:
|
protocol: tcp
|
||||||
darkalfis_data:
|
privileged: true
|
||||||
name: darkalfis_data
|
restart: unless-stopped
|
||||||
emc_data:
|
secrets:
|
||||||
name: emc_data
|
- source: tz
|
||||||
tor-data:
|
target: /run/secrets/tz
|
||||||
driver: local
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
networks:
|
volumes:
|
||||||
darkproxy:
|
- type: volume
|
||||||
# name: darkproxy
|
source: i2pd_state
|
||||||
enable_ipv6: true
|
target: /var/lib/i2pd
|
||||||
driver: bridge
|
volume: {}
|
||||||
ipam:
|
- type: volume
|
||||||
config:
|
source: yggdrasil_state
|
||||||
- subnet: 10.5.0.0/16
|
target: /var/lib/yggdrasil
|
||||||
gateway: 10.5.0.1
|
volume: {}
|
||||||
- subnet: 2001:0BC5::/112
|
i2pdns:
|
||||||
gateway: 2001:0BC5::1
|
build:
|
||||||
|
context: /home/blade/darkproxy/i2pdns
|
||||||
secrets:
|
dockerfile: Dockerfile
|
||||||
tz:
|
cpus: 0.5
|
||||||
file: ./secrets/tz.txt
|
container_name: darki2pdns
|
||||||
YGGDRASIL_GENERATE_KEYS:
|
environment:
|
||||||
file: ./secrets/YGGDRASIL_GENERATE_KEYS.txt
|
I2PDNS_LISTEN_HOST: 0.0.0.0
|
||||||
|
I2PDNS_LISTEN_PORT: "53"
|
||||||
|
I2PDNS_MAP_FILE: /var/lib/i2pdns/map.json
|
||||||
|
I2PDNS_POOL_CIDR: 172.31.0.0/16
|
||||||
|
I2PDNS_TTL: "60"
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- import socket; from dnslib import DNSRecord; q=DNSRecord.question('stats.i2p','A'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
mem_limit: "134217728"
|
||||||
|
mem_reservation: "67108864"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.16
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: i2p_dns_map
|
||||||
|
target: /var/lib/i2pdns
|
||||||
|
volume: {}
|
||||||
|
lokinet:
|
||||||
|
build:
|
||||||
|
context: /home/blade/darkproxy/lokinet
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
cpus: 1
|
||||||
|
container_name: darklokinet
|
||||||
|
devices:
|
||||||
|
- source: /dev/net/tun
|
||||||
|
target: /dev/net/tun
|
||||||
|
permissions: rwm
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- pidof lokinet >/dev/null && ip link show lokinet0 >/dev/null 2>&1
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 45s
|
||||||
|
mem_limit: "268435456"
|
||||||
|
mem_reservation: "67108864"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.14
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
namecoind:
|
||||||
|
cpus: 2
|
||||||
|
container_name: darknamecoind
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
exec /usr/local/bin/namecoind \
|
||||||
|
-server=1 \
|
||||||
|
-daemon=0 \
|
||||||
|
-datadir=/data \
|
||||||
|
-rpcbind=0.0.0.0 \
|
||||||
|
-rpcallowip=10.5.0.0/16 \
|
||||||
|
-rpcuser=namecoinrpc \
|
||||||
|
"-rpcpassword=$$(cat /run/secrets/NAMECOIN_RPC_PASSWORD)" \
|
||||||
|
-printtoconsole \
|
||||||
|
-txindex=1 \
|
||||||
|
-dnsseed=1 \
|
||||||
|
-addnode=162.212.154.52:8334 \
|
||||||
|
-addnode=162.210.196.27:8334 \
|
||||||
|
-addnode=23.108.191.178:8334 \
|
||||||
|
-addnode=3.228.193.128:8334 \
|
||||||
|
-addnode=3.66.245.44:8334 \
|
||||||
|
-addnode=212.51.144.42:8334
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- 'curl -fsS --user "namecoinrpc:$$(cat /run/secrets/NAMECOIN_RPC_PASSWORD)" --data-binary ''{"jsonrpc":"1.0","id":"health","method":"getblockcount","params":[]}'' -H ''content-type: text/plain;'' http://127.0.0.1:8336/ >/dev/null'
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
image: ukd1/namecoind@sha256:6fdf63f61f687ee639893a8cdcd910bb24c5eb3a3493ff72142c911d7019200b
|
||||||
|
mem_limit: "2147483648"
|
||||||
|
mem_reservation: "536870912"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.13
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
secrets:
|
||||||
|
- source: NAMECOIN_RPC_PASSWORD
|
||||||
|
target: /run/secrets/NAMECOIN_RPC_PASSWORD
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: namecoin_data
|
||||||
|
target: /data
|
||||||
|
volume: {}
|
||||||
|
namecoindns:
|
||||||
|
build:
|
||||||
|
context: /home/blade/darkproxy/namecoindns
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
cpus: 0.5
|
||||||
|
container_name: darknamecoin
|
||||||
|
depends_on:
|
||||||
|
namecoind:
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
environment:
|
||||||
|
NAMECOIN_RPC_PASSWORD_FILE: /run/secrets/NAMECOIN_RPC_PASSWORD
|
||||||
|
NAMECOIN_RPC_TIMEOUT: "8"
|
||||||
|
NAMECOIN_RPC_URL: http://darknamecoind:8336/
|
||||||
|
NAMECOIN_RPC_USER: namecoinrpc
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- import socket; from dnslib import DNSRecord; q=DNSRecord.question('d.bit','TXT'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
mem_limit: "268435456"
|
||||||
|
mem_reservation: "67108864"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.12
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
secrets:
|
||||||
|
- source: NAMECOIN_RPC_PASSWORD
|
||||||
|
target: /run/secrets/NAMECOIN_RPC_PASSWORD
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
pihole:
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_NICE
|
||||||
|
cpus: 1.5
|
||||||
|
container_name: darkpihole
|
||||||
|
entrypoint:
|
||||||
|
- /usr/local/bin/darkproxy-pihole-entrypoint.sh
|
||||||
|
environment:
|
||||||
|
FTLCONF_database_network_parseARPcache: "false"
|
||||||
|
FTLCONF_dns_listeningMode: all
|
||||||
|
FTLCONF_dns_upstreams: 10.5.0.4#53
|
||||||
|
PIHOLE_DNS_1: 10.5.0.4
|
||||||
|
TEMPERATUREUNIT: f
|
||||||
|
TZ: America/Detroit
|
||||||
|
WEBPASSWORD_FILE: /run/secrets/PIHOLE_WEBPASSWORD
|
||||||
|
WEBTHEME: lcars
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- dig
|
||||||
|
- +short
|
||||||
|
- +norecurse
|
||||||
|
- +timeout=2
|
||||||
|
- '@127.0.0.1'
|
||||||
|
- google.com
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
image: pihole/pihole@sha256:712b39f1fdb55121cef509813dfbe02d2bdef9c28e07404fa1d422f5157323b2
|
||||||
|
mem_limit: "536870912"
|
||||||
|
mem_reservation: "134217728"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.6
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- mode: ingress
|
||||||
|
target: 53
|
||||||
|
published: "53"
|
||||||
|
protocol: tcp
|
||||||
|
- mode: ingress
|
||||||
|
target: 53
|
||||||
|
published: "53"
|
||||||
|
protocol: udp
|
||||||
|
- mode: ingress
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
target: 80
|
||||||
|
published: "2003"
|
||||||
|
protocol: tcp
|
||||||
|
restart: unless-stopped
|
||||||
|
secrets:
|
||||||
|
- source: PIHOLE_WEBPASSWORD
|
||||||
|
target: /run/secrets/PIHOLE_WEBPASSWORD
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/pihole/etc-pihole
|
||||||
|
target: /etc/pihole
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/pihole/etc-dnsmasq.d
|
||||||
|
target: /etc/dnsmasq.d
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/scripts/pihole-entrypoint.sh
|
||||||
|
target: /usr/local/bin/darkproxy-pihole-entrypoint.sh
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
status_dashboard:
|
||||||
|
build:
|
||||||
|
context: /home/blade/darkproxy/monitor
|
||||||
|
dockerfile: status.Dockerfile
|
||||||
|
container_name: darkstatus
|
||||||
|
depends_on:
|
||||||
|
dark3proxy:
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
pihole:
|
||||||
|
condition: service_healthy
|
||||||
|
required: true
|
||||||
|
environment:
|
||||||
|
DASHBOARD_PORT: "8080"
|
||||||
|
DNS_PORT: "53"
|
||||||
|
DNS_SERVER: darkpihole
|
||||||
|
DOCKER_PROJECT: darkproxy
|
||||||
|
DOCKER_SOCKET: /var/run/docker.sock
|
||||||
|
LOG_TAIL_LINES: "120"
|
||||||
|
PROXY_HOST: dark3proxy
|
||||||
|
PROXY_PORT: "1080"
|
||||||
|
PROXY_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
|
SOCKET_TIMEOUT: "5"
|
||||||
|
STATUS_CACHE_SECONDS: "15"
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=5).read()
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
image: darkproxy-status-dashboard:local
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.21
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- mode: ingress
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
target: 8080
|
||||||
|
published: "2004"
|
||||||
|
protocol: tcp
|
||||||
|
restart: unless-stopped
|
||||||
|
secrets:
|
||||||
|
- source: PROXY_USERS
|
||||||
|
target: /run/secrets/PROXY_USERS
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /var/run/docker.sock
|
||||||
|
target: /var/run/docker.sock
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
tor_yggdrasil:
|
||||||
|
build:
|
||||||
|
context: /home/blade/darkproxy/tor_yggdrasil_docker
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
cpus: 2
|
||||||
|
command:
|
||||||
|
- -f
|
||||||
|
- /etc/tor/torrc
|
||||||
|
container_name: darktor
|
||||||
|
dns:
|
||||||
|
- 10.5.0.6
|
||||||
|
entrypoint:
|
||||||
|
- tor
|
||||||
|
mem_limit: "1073741824"
|
||||||
|
mem_reservation: "268435456"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.7
|
||||||
|
platform: linux/amd64
|
||||||
|
ports:
|
||||||
|
- mode: ingress
|
||||||
|
host_ip: 127.0.0.1
|
||||||
|
target: 9053
|
||||||
|
published: "2006"
|
||||||
|
protocol: udp
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/tor_yggdrasil_docker/torrc
|
||||||
|
target: /etc/tor/torrc
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
unbound:
|
||||||
|
cpus: 4
|
||||||
|
container_name: darkunbound
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
- drill @127.0.0.1 google.com >/dev/null 2>&1
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
image: mvance/unbound@sha256:76906da36d1806f3387338f15dcf8b357c51ce6897fb6450d6ce010460927e90
|
||||||
|
mem_limit: "805306368"
|
||||||
|
mem_reservation: "268435456"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.5
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/unbound/unbound.conf
|
||||||
|
target: /opt/unbound/etc/unbound/unbound.conf
|
||||||
|
read_only: true
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/unbound/forward-records.conf
|
||||||
|
target: /opt/unbound/etc/unbound/forward-records.conf
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/unbound/a-records.conf
|
||||||
|
target: /opt/unbound/etc/unbound/a-records.conf
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
- type: bind
|
||||||
|
source: /home/blade/darkproxy/unbound/srv-records.conf
|
||||||
|
target: /opt/unbound/etc/unbound/srv-records.conf
|
||||||
|
bind:
|
||||||
|
create_host_path: true
|
||||||
|
zildns:
|
||||||
|
build:
|
||||||
|
context: /home/blade/darkproxy/zildns
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
cpus: 0.5
|
||||||
|
container_name: darkzil
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
environment:
|
||||||
|
ZILDNS_CACHE_SECONDS: "300"
|
||||||
|
ZILDNS_PREWARM_DOMAIN: brad.zil
|
||||||
|
ZILDNS_TTL: "120"
|
||||||
|
ZILDNS_ZNS_NETWORK: mainnet
|
||||||
|
ZILDNS_ZNS_URL: https://api.zilliqa.com
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- node
|
||||||
|
- -e
|
||||||
|
- const net=require('net'); const socket=net.createConnection({host:'127.0.0.1', port:53}); socket.setTimeout(2000); socket.on('connect',()=>{socket.end(); process.exit(0);}); socket.on('timeout',()=>{socket.destroy(); process.exit(1);}); socket.on('error',()=>process.exit(1));
|
||||||
|
timeout: 10s
|
||||||
|
interval: 1m0s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
mem_limit: "268435456"
|
||||||
|
mem_reservation: "67108864"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.15
|
||||||
|
platform: linux/amd64
|
||||||
|
restart: unless-stopped
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
net.ipv6.conf.all.disable_ipv6: "0"
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
name: darkproxy_darkproxy
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 10.5.0.0/16
|
||||||
|
gateway: 10.5.0.1
|
||||||
|
- subnet: 2001:0BC5::/112
|
||||||
|
gateway: 2001:0BC5::1
|
||||||
|
enable_ipv6: true
|
||||||
|
volumes:
|
||||||
|
darkalfis_data:
|
||||||
|
name: darkalfis_data
|
||||||
|
emc_data:
|
||||||
|
name: emc_data
|
||||||
|
i2p_dns_map:
|
||||||
|
name: i2p_dns_map
|
||||||
|
i2pd_state:
|
||||||
|
name: i2pd_state
|
||||||
|
namecoin_data:
|
||||||
|
name: namecoin_data
|
||||||
|
yggdrasil_state:
|
||||||
|
name: yggdrasil_state
|
||||||
|
secrets:
|
||||||
|
NAMECOIN_RPC_PASSWORD:
|
||||||
|
name: darkproxy_NAMECOIN_RPC_PASSWORD
|
||||||
|
file: /home/blade/darkproxy/secrets/namecoin_rpc_password.txt
|
||||||
|
PIHOLE_WEBPASSWORD:
|
||||||
|
name: darkproxy_PIHOLE_WEBPASSWORD
|
||||||
|
file: /home/blade/darkproxy/secrets/pihole_webpassword.txt
|
||||||
|
PROXY_USERS:
|
||||||
|
name: darkproxy_PROXY_USERS
|
||||||
|
file: /home/blade/darkproxy/secrets/3proxy_users.txt
|
||||||
|
tz:
|
||||||
|
name: darkproxy_tz
|
||||||
|
file: /home/blade/darkproxy/secrets/tz.txt
|
||||||
|
|||||||
+333
-80
@@ -1,31 +1,59 @@
|
|||||||
services:
|
services:
|
||||||
dark3proxy:
|
dark3proxy:
|
||||||
container_name: dark3proxy
|
container_name: dark3proxy
|
||||||
image: host60/darkproxy-dark3proxy:v1.0
|
|
||||||
build:
|
build:
|
||||||
context: ./3proxy/.
|
context: ./3proxy/.
|
||||||
|
platform: linux/amd64
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
dns:
|
dns:
|
||||||
- "10.5.0.6"
|
- "10.5.0.6"
|
||||||
dns_search: internal.namespace #namespace used in internal DNS
|
dns_search: internal.namespace #namespace used in internal DNS
|
||||||
|
environment:
|
||||||
|
YGG_CONNECT_WAIT_SECONDS: "8"
|
||||||
|
PROXY_REQUIRE_AUTH: "true"
|
||||||
|
PROXY_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
|
ROUTER_REQUIRE_AUTH: "true"
|
||||||
|
ROUTER_NO_AUTH_CIDRS: ${ROUTER_NO_AUTH_CIDRS:-}
|
||||||
|
ROUTER_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
|
ROUTER_I2P_MAP_FILE: /var/lib/i2pdns/map.json
|
||||||
|
ROUTER_I2P_POOL_CIDR: 172.31.0.0/16
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
secrets:
|
||||||
|
- PROXY_USERS
|
||||||
volumes:
|
volumes:
|
||||||
- "./3proxy/first-instanse.cfg:/etc/3proxy/first-instanse.cfg"
|
- "./3proxy/first-instanse.cfg:/etc/3proxy/first-instanse.cfg"
|
||||||
- "./3proxy/second-instanse.cfg:/etc/3proxy/second-instanse.cfg"
|
- "./3proxy/second-instanse.cfg:/etc/3proxy/second-instanse.cfg"
|
||||||
secrets:
|
- "./3proxy/yggdrasil.conf:/etc/yggdrasil/yggdrasil.conf"
|
||||||
- PROXY_USERS
|
- "i2p_dns_map:/var/lib/i2pdns:ro"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 256m
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "bash", "-lc", "pgrep -f 'yggdrasil -useconffile' >/dev/null && pgrep -f '3proxy /etc/3proxy/second-instanse.cfg' >/dev/null && pgrep -f '3proxy /etc/3proxy/first-instanse.cfg' >/dev/null && pgrep -f 'python3 /usr/local/bin/socks_router.py' >/dev/null"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 45s
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:2000:1080" # socks port (localhost only)
|
- "2000:1080" # primary host SOCKS via dark3proxy (externally reachable)
|
||||||
- "127.0.0.1:2002:8161" # 3proxy-eagle web page (localhost only)
|
- "127.0.0.1:2002:8161" # 3proxy admin page (host-local only)
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.8
|
ipv4_address: 10.5.0.8
|
||||||
|
|
||||||
i2pd_yggdrasil:
|
i2pd_yggdrasil:
|
||||||
container_name: darki2p
|
container_name: darki2p
|
||||||
image: host60/i2pd_yggdrasil:v1.3
|
build:
|
||||||
# image: i2pd_yggdrasil:latest
|
context: ./i2pd_yggdrasil_docker/src
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
privileged: true
|
privileged: true
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
@@ -52,13 +80,20 @@ services:
|
|||||||
tz: /run/secrets/tz
|
tz: /run/secrets/tz
|
||||||
secrets:
|
secrets:
|
||||||
- tz
|
- tz
|
||||||
|
volumes:
|
||||||
|
- i2pd_state:/var/lib/i2pd
|
||||||
|
- yggdrasil_state:/var/lib/yggdrasil
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "4.0"
|
||||||
|
mem_reservation: 512m
|
||||||
|
mem_limit: 2g
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "bash", "-c", "pgrep i2pd && pgrep yggdrasil"]
|
test: ["CMD", "bash", "-lc", "pgrep i2pd >/dev/null && pgrep yggdrasil >/dev/null && [ -s /var/lib/i2pd/addressbook/addresses.csv ]"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
# Key generation + peer discovery can take several minutes on emulated ARM.
|
||||||
|
start_period: 15m
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
mac_address: ce:22:b8:0e:6e:78
|
mac_address: ce:22:b8:0e:6e:78
|
||||||
@@ -66,26 +101,58 @@ services:
|
|||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.2
|
ipv4_address: 10.5.0.2
|
||||||
|
|
||||||
|
i2pdns:
|
||||||
|
container_name: darki2pdns
|
||||||
|
build:
|
||||||
|
context: ./i2pdns
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
restart: unless-stopped
|
||||||
|
cpus: "0.5"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 128m
|
||||||
|
environment:
|
||||||
|
I2PDNS_LISTEN_HOST: 0.0.0.0
|
||||||
|
I2PDNS_LISTEN_PORT: 53
|
||||||
|
I2PDNS_TTL: 60
|
||||||
|
I2PDNS_POOL_CIDR: 172.31.0.0/16
|
||||||
|
I2PDNS_MAP_FILE: /var/lib/i2pdns/map.json
|
||||||
|
volumes:
|
||||||
|
- "i2p_dns_map:/var/lib/i2pdns"
|
||||||
|
sysctls:
|
||||||
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import socket; from dnslib import DNSRecord; q=DNSRecord.question('stats.i2p','A'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.16
|
||||||
|
|
||||||
tor_yggdrasil:
|
tor_yggdrasil:
|
||||||
image: host60/tor_yggdrasil:v1.0
|
build:
|
||||||
# image: tor_yggdrasil:latest
|
context: ./tor_yggdrasil_docker
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
# container_name: tor_over_yggdrasil
|
# container_name: tor_over_yggdrasil
|
||||||
container_name: darktor
|
container_name: darktor
|
||||||
|
entrypoint: ["tor"]
|
||||||
|
command: ["-f", "/etc/tor/torrc"]
|
||||||
# platform removed; image is amd64-only
|
# platform removed; image is amd64-only
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN # Required for network tunnel management
|
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true # Security enhancement - prevent privilege esca>
|
- no-new-privileges:true # Security enhancement - prevent privilege esca>
|
||||||
devices:
|
volumes:
|
||||||
- /dev/net/tun:/dev/net/tun # TUN device for Yggdrasil network interface
|
- "./tor_yggdrasil_docker/torrc:/etc/tor/torrc:ro"
|
||||||
ports:
|
ports:
|
||||||
# - "127.0.0.1:9050:9050/tcp" # SOCKS5 proxy port (Tor) for application-l>
|
- "127.0.0.1:2006:9053/udp" # direct Tor DNSPort
|
||||||
- "10655:10655/tcp" # Yggdrasil peer connections
|
|
||||||
secrets:
|
|
||||||
- YGGDRASIL_GENERATE_KEYS
|
|
||||||
environment:
|
|
||||||
YGGDRASIL_GENERATE_KEYS: /run/secrets/YGGDRASIL_GENERATE_KEYS # Generate new Yggdrasil keys on startup
|
|
||||||
restart: unless-stopped # Auto-restart unless manually stopped
|
restart: unless-stopped # Auto-restart unless manually stopped
|
||||||
|
cpus: "2.0"
|
||||||
|
mem_reservation: 256m
|
||||||
|
mem_limit: 1g
|
||||||
dns:
|
dns:
|
||||||
- "10.5.0.6"
|
- "10.5.0.6"
|
||||||
sysctls:
|
sysctls:
|
||||||
@@ -95,9 +162,12 @@ services:
|
|||||||
ipv4_address: 10.5.0.7
|
ipv4_address: 10.5.0.7
|
||||||
|
|
||||||
alfis:
|
alfis:
|
||||||
image: cofob/alfis
|
image: cofob/alfis@sha256:0c5788b1e409557bb814dc2d055d584db8c48b3d42b375bf13feb59583af0585
|
||||||
|
platform: linux/amd64
|
||||||
# platform removed; image is amd64 only
|
# platform removed; image is amd64 only
|
||||||
container_name: darkalfis
|
container_name: darkalfis
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
volumes:
|
volumes:
|
||||||
- darkalfis_data:/storage
|
- darkalfis_data:/storage
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -111,13 +181,17 @@ services:
|
|||||||
ipv4_address: 10.5.0.3
|
ipv4_address: 10.5.0.3
|
||||||
|
|
||||||
coredns:
|
coredns:
|
||||||
# image: darkdns
|
|
||||||
image: host60/darkdns:v1.0
|
|
||||||
build:
|
build:
|
||||||
context: ./PopuraDNS
|
context: ./PopuraDNS
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
container_name: darkdns
|
container_name: darkdns
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 256m
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
logging:
|
logging:
|
||||||
@@ -131,8 +205,11 @@ services:
|
|||||||
|
|
||||||
unbound:
|
unbound:
|
||||||
container_name: darkunbound
|
container_name: darkunbound
|
||||||
image: mvance/unbound:latest
|
image: mvance/unbound@sha256:76906da36d1806f3387338f15dcf8b357c51ce6897fb6450d6ce010460927e90
|
||||||
|
platform: linux/amd64
|
||||||
# platform removed; image is amd64-only
|
# platform removed; image is amd64-only
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
networks:
|
networks:
|
||||||
@@ -145,22 +222,35 @@ services:
|
|||||||
target: /opt/unbound/etc/unbound/unbound.conf
|
target: /opt/unbound/etc/unbound/unbound.conf
|
||||||
- "./unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf"
|
- "./unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf"
|
||||||
- "./unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf"
|
- "./unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf"
|
||||||
|
- "./unbound/srv-records.conf:/opt/unbound/etc/unbound/srv-records.conf"
|
||||||
# ports:
|
# ports:
|
||||||
# - "5053:5053/tcp"
|
# - "5053:5053/tcp"
|
||||||
# - "5053:5053/udp"
|
# - "5053:5053/udp"
|
||||||
# healthcheck:
|
# healthcheck:
|
||||||
# disable: true
|
# disable: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "4.0"
|
||||||
|
mem_reservation: 256m
|
||||||
|
mem_limit: 768m
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "drill @127.0.0.1 google.com >/dev/null 2>&1"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
pihole:
|
pihole:
|
||||||
container_name: darkpihole
|
container_name: darkpihole
|
||||||
image: pihole/pihole:latest
|
image: pihole/pihole@sha256:712b39f1fdb55121cef509813dfbe02d2bdef9c28e07404fa1d422f5157323b2
|
||||||
platform: linux/arm64
|
platform: linux/amd64
|
||||||
|
entrypoint: ["/usr/local/bin/darkproxy-pihole-entrypoint.sh"]
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
# For DHCP it is recommended to remove these ports and instead add: network_mode: "host"
|
# For DHCP it is recommended to remove these ports and instead add: network_mode: "host"
|
||||||
ports:
|
ports:
|
||||||
# DNS Ports
|
# DNS Ports
|
||||||
# - "53:53/tcp"
|
- "53:53/tcp"
|
||||||
# - "53:53/udp"
|
- "53:53/udp"
|
||||||
# Default HTTP Port
|
# Default HTTP Port
|
||||||
# - "80:80/tcp"
|
# - "80:80/tcp"
|
||||||
# Default HTTPs Port. FTL will generate a self-signed certificate
|
# Default HTTPs Port. FTL will generate a self-signed certificate
|
||||||
@@ -169,19 +259,21 @@ services:
|
|||||||
# - "67:67/udp"
|
# - "67:67/udp"
|
||||||
# Uncomment the line below if you are using Pi-hole as your NTP server
|
# Uncomment the line below if you are using Pi-hole as your NTP server
|
||||||
# - "123:123/udp"
|
# - "123:123/udp"
|
||||||
- "127.0.0.1:2003:80/tcp" # pihole web port (localhost only)
|
- "${PIHOLE_WEB_BIND_HOST:-127.0.0.1}:${PIHOLE_WEB_BIND_PORT:-2003}:80/tcp" # pihole web port (host-local by default)
|
||||||
environment:
|
environment:
|
||||||
# Set the appropriate timezone for your location (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), e.g:
|
# Set the appropriate timezone for your location (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), e.g:
|
||||||
TZ: 'America/Detroit'
|
TZ: 'America/Detroit'
|
||||||
# WEBPASSWORD: 'set a secure password here or it will be random'
|
# WEBPASSWORD: 'set a secure password here or it will be random'
|
||||||
# FTLCONF_webserver_api_password: 'darkproxy'
|
# FTLCONF_webserver_api_password: 'darkproxy'
|
||||||
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
|
# If using Docker's default `bridge` network setting the dns listening mode should be set to 'all'
|
||||||
# FTLCONF_dns_listeningMode: 'all'
|
FTLCONF_dns_listeningMode: 'all'
|
||||||
PIHOLE_DNS_: 10.5.0.4
|
PIHOLE_DNS_1: 10.5.0.4
|
||||||
TEMPERATUREUNIT: f
|
TEMPERATUREUNIT: f
|
||||||
WEBTHEME: lcars
|
WEBTHEME: lcars
|
||||||
WEBPASSWORD_FILE: /run/secrets/PIHOLE_WEBPASSWORD
|
WEBPASSWORD_FILE: /run/secrets/PIHOLE_WEBPASSWORD
|
||||||
FTLCONF_dns_upstreams: coredns
|
# Pin upstream to IPv4 CoreDNS to avoid noisy IPv6 recursion-refused warnings.
|
||||||
|
FTLCONF_dns_upstreams: 10.5.0.4#53
|
||||||
|
FTLCONF_database_network_parseARPcache: 'false'
|
||||||
secrets:
|
secrets:
|
||||||
- PIHOLE_WEBPASSWORD
|
- PIHOLE_WEBPASSWORD
|
||||||
# Volumes store your data between container upgrades
|
# Volumes store your data between container upgrades
|
||||||
@@ -190,6 +282,7 @@ services:
|
|||||||
- './pihole/etc-pihole:/etc/pihole'
|
- './pihole/etc-pihole:/etc/pihole'
|
||||||
# Uncomment the below if you have custom dnsmasq config files that you want to persist. Not needed for most starting fresh with Pi-hole v6. If you're upgrading from v5 you and have used this directory before, you should keep it enabled for the first v6 container start to allow for a complete migration. It can be removed afterwards. Needs environment variable FTLCONF_misc_etc_dnsmasq_d: 'true'
|
# Uncomment the below if you have custom dnsmasq config files that you want to persist. Not needed for most starting fresh with Pi-hole v6. If you're upgrading from v5 you and have used this directory before, you should keep it enabled for the first v6 container start to allow for a complete migration. It can be removed afterwards. Needs environment variable FTLCONF_misc_etc_dnsmasq_d: 'true'
|
||||||
- './pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
|
- './pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
|
||||||
|
- './scripts/pihole-entrypoint.sh:/usr/local/bin/darkproxy-pihole-entrypoint.sh:ro'
|
||||||
cap_add:
|
cap_add:
|
||||||
# See https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
|
# See https://github.com/pi-hole/docker-pi-hole#note-on-capabilities
|
||||||
# Required if you are using Pi-hole as your DHCP server, else not needed
|
# Required if you are using Pi-hole as your DHCP server, else not needed
|
||||||
@@ -199,6 +292,9 @@ services:
|
|||||||
# Optional, if Pi-hole should get some more processing time
|
# Optional, if Pi-hole should get some more processing time
|
||||||
- SYS_NICE
|
- SYS_NICE
|
||||||
restart: unless-stopped # Recommended but not required (DHCP needs NET_ADMIN)
|
restart: unless-stopped # Recommended but not required (DHCP needs NET_ADMIN)
|
||||||
|
cpus: "1.5"
|
||||||
|
mem_reservation: 128m
|
||||||
|
mem_limit: 512m
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -212,49 +308,161 @@ services:
|
|||||||
ipv4_address: 10.5.0.6
|
ipv4_address: 10.5.0.6
|
||||||
|
|
||||||
emc:
|
emc:
|
||||||
image: wg00/emercoin:0.8.4
|
image: wg00/emercoin:0.8.4@sha256:b890987fb4b158305040dc76b32cd24ed5173dd7bc4aba983141ea0fe82e2996
|
||||||
platform: linux/arm64
|
platform: linux/amd64
|
||||||
container_name: darkemer
|
container_name: darkemer
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
volumes:
|
volumes:
|
||||||
- emc_data:/emc
|
- emc_data:/emc/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
command:
|
||||||
|
- -datadir=/emc/data
|
||||||
|
- -conf=/emc/emercoin.conf
|
||||||
|
- -printtoconsole
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.9
|
ipv4_address: 10.5.0.9
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: 256m
|
||||||
|
mem_limit: 1g
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
|
|
||||||
ensdns:
|
ensdns:
|
||||||
build:
|
build:
|
||||||
context: ./ensdns
|
context: ./ensdns
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
container_name: darkens
|
container_name: darkens
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "0.5"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 256m
|
||||||
environment:
|
environment:
|
||||||
ENS_RPC_URL: https://ethereum-rpc.publicnode.com
|
ENS_RPC_URL: https://ethereum-rpc.publicnode.com
|
||||||
ENSDNS_TTL: 60
|
ENSDNS_TTL: 60
|
||||||
|
dns:
|
||||||
|
- "10.5.0.4"
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import socket; from dnslib import DNSRecord; q=DNSRecord.question('vitalik.eth','TXT'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.11
|
ipv4_address: 10.5.0.11
|
||||||
|
|
||||||
namecoind:
|
zildns:
|
||||||
image: ukd1/namecoind:latest
|
build:
|
||||||
container_name: darknamecoind
|
context: ./zildns
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: darkzil
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
cpus: "0.5"
|
||||||
- "-server=1"
|
mem_reservation: 64m
|
||||||
- "-rpcbind=0.0.0.0"
|
mem_limit: 256m
|
||||||
- "-rpcallowip=10.5.0.0/16"
|
environment:
|
||||||
- "-rpcuser=namecoinrpc"
|
ZILDNS_ZNS_URL: https://api.zilliqa.com
|
||||||
- "-rpcpassword=CHANGE_ME_NAMECOIN_RPC_PASSWORD"
|
ZILDNS_ZNS_NETWORK: mainnet
|
||||||
- "-printtoconsole"
|
ZILDNS_TTL: 120
|
||||||
- "-txindex=1"
|
ZILDNS_CACHE_SECONDS: 300
|
||||||
volumes:
|
ZILDNS_PREWARM_DOMAIN: brad.zil
|
||||||
- namecoin_data:/namecoin
|
dns:
|
||||||
|
- "1.1.1.1"
|
||||||
|
- "8.8.8.8"
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "const net=require('net'); const socket=net.createConnection({host:'127.0.0.1', port:53}); socket.setTimeout(2000); socket.on('connect',()=>{socket.end(); process.exit(0);}); socket.on('timeout',()=>{socket.destroy(); process.exit(1);}); socket.on('error',()=>process.exit(1));"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.15
|
||||||
|
|
||||||
|
lokinet:
|
||||||
|
build:
|
||||||
|
context: ./lokinet
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: darklokinet
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
restart: unless-stopped
|
||||||
|
cpus: "1.0"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 256m
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- NET_BIND_SERVICE
|
||||||
|
devices:
|
||||||
|
- /dev/net/tun:/dev/net/tun
|
||||||
|
sysctls:
|
||||||
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pidof lokinet >/dev/null && ip link show lokinet0 >/dev/null 2>&1"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 45s
|
||||||
|
networks:
|
||||||
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.14
|
||||||
|
|
||||||
|
namecoind:
|
||||||
|
image: ukd1/namecoind@sha256:6fdf63f61f687ee639893a8cdcd910bb24c5eb3a3493ff72142c911d7019200b
|
||||||
|
platform: linux/amd64
|
||||||
|
container_name: darknamecoind
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -ec
|
||||||
|
- |
|
||||||
|
exec /usr/local/bin/namecoind \
|
||||||
|
-server=1 \
|
||||||
|
-daemon=0 \
|
||||||
|
-datadir=/data \
|
||||||
|
-rpcbind=0.0.0.0 \
|
||||||
|
-rpcallowip=10.5.0.0/16 \
|
||||||
|
-rpcuser=namecoinrpc \
|
||||||
|
"-rpcpassword=$(cat /run/secrets/NAMECOIN_RPC_PASSWORD)" \
|
||||||
|
-printtoconsole \
|
||||||
|
-txindex=1 \
|
||||||
|
-dnsseed=1 \
|
||||||
|
-addnode=162.212.154.52:8334 \
|
||||||
|
-addnode=162.210.196.27:8334 \
|
||||||
|
-addnode=23.108.191.178:8334 \
|
||||||
|
-addnode=3.228.193.128:8334 \
|
||||||
|
-addnode=3.66.245.44:8334 \
|
||||||
|
-addnode=212.51.144.42:8334
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
restart: unless-stopped
|
||||||
|
cpus: "2.0"
|
||||||
|
mem_reservation: 512m
|
||||||
|
mem_limit: 2g
|
||||||
|
secrets:
|
||||||
|
- NAMECOIN_RPC_PASSWORD
|
||||||
|
volumes:
|
||||||
|
- namecoin_data:/data
|
||||||
|
sysctls:
|
||||||
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS --user \"namecoinrpc:$(cat /run/secrets/NAMECOIN_RPC_PASSWORD)\" --data-binary '{\"jsonrpc\":\"1.0\",\"id\":\"health\",\"method\":\"getblockcount\",\"params\":[]}' -H 'content-type: text/plain;' http://127.0.0.1:8336/ >/dev/null"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.13
|
ipv4_address: 10.5.0.13
|
||||||
@@ -263,17 +471,32 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./namecoindns
|
context: ./namecoindns
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
container_name: darknamecoin
|
container_name: darknamecoin
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
cpus: "0.5"
|
||||||
|
mem_reservation: 64m
|
||||||
|
mem_limit: 256m
|
||||||
depends_on:
|
depends_on:
|
||||||
- namecoind
|
namecoind:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
NAMECOIN_RPC_URL: http://darknamecoind:8336/
|
NAMECOIN_RPC_URL: http://darknamecoind:8336/
|
||||||
NAMECOIN_RPC_USER: namecoinrpc
|
NAMECOIN_RPC_USER: namecoinrpc
|
||||||
NAMECOIN_RPC_PASSWORD: CHANGE_ME_NAMECOIN_RPC_PASSWORD
|
NAMECOIN_RPC_PASSWORD_FILE: /run/secrets/NAMECOIN_RPC_PASSWORD
|
||||||
NAMECOIN_RPC_TIMEOUT: 8
|
NAMECOIN_RPC_TIMEOUT: 8
|
||||||
|
secrets:
|
||||||
|
- NAMECOIN_RPC_PASSWORD
|
||||||
sysctls:
|
sysctls:
|
||||||
- "net.ipv6.conf.all.disable_ipv6=0"
|
- "net.ipv6.conf.all.disable_ipv6=0"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import socket; from dnslib import DNSRecord; q=DNSRecord.question('d.bit','TXT'); s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.settimeout(2); s.sendto(q.pack(), ('127.0.0.1', 53)); data, _ = s.recvfrom(4096); DNSRecord.parse(data)"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
ipv4_address: 10.5.0.12
|
ipv4_address: 10.5.0.12
|
||||||
@@ -282,16 +505,19 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./tailscale
|
context: ./tailscale
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
platform: linux/amd64
|
||||||
|
profiles: ["tailscale"]
|
||||||
container_name: darkscale
|
container_name: darkscale
|
||||||
hostname: darkproxy-exit
|
hostname: darkproxy-exit
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
devices:
|
devices:
|
||||||
- /dev/net/tun:/dev/net/tun
|
- /dev/net/tun:/dev/net/tun
|
||||||
environment:
|
environment:
|
||||||
# ⚠️ Get your own auth key from https://login.tailscale.com/admin/settings/keys
|
- TS_AUTHKEY=${TS_AUTHKEY:-}
|
||||||
- TS_AUTHKEY=${TS_AUTHKEY:-tskey-YOUR-AUTH-KEY-HERE}
|
|
||||||
- TS_EXTRA_ARGS=--accept-dns=false --advertise-exit-node
|
- TS_EXTRA_ARGS=--accept-dns=false --advertise-exit-node
|
||||||
- TS_STATE_DIR=/var/lib/tailscale
|
- TS_STATE_DIR=/var/lib/tailscale
|
||||||
volumes:
|
volumes:
|
||||||
@@ -309,37 +535,65 @@ services:
|
|||||||
container_name: dark3proxy-exporter
|
container_name: dark3proxy-exporter
|
||||||
build:
|
build:
|
||||||
context: ./monitor
|
context: ./monitor
|
||||||
image: darkproxy-3proxy-exporter:latest
|
image: darkproxy-3proxy-exporter:local
|
||||||
|
platform: linux/amd64
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
depends_on:
|
depends_on:
|
||||||
- dark3proxy
|
dark3proxy:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- darkproxy
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.20
|
||||||
|
|
||||||
prometheus:
|
status_dashboard:
|
||||||
image: prom/prometheus:latest
|
container_name: darkstatus
|
||||||
container_name: darkprom
|
build:
|
||||||
volumes:
|
context: ./monitor
|
||||||
- ./monitor/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
dockerfile: status.Dockerfile
|
||||||
ports:
|
image: darkproxy-status-dashboard:local
|
||||||
- "127.0.0.1:9090:9090"
|
platform: linux/amd64
|
||||||
networks:
|
security_opt:
|
||||||
- darkproxy
|
- no-new-privileges:true
|
||||||
|
depends_on:
|
||||||
grafana:
|
dark3proxy:
|
||||||
image: grafana/grafana:latest
|
condition: service_healthy
|
||||||
container_name: darkgraf
|
pihole:
|
||||||
ports:
|
condition: service_healthy
|
||||||
- "127.0.0.1:2005:3000"
|
|
||||||
environment:
|
environment:
|
||||||
- GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/GRAFANA_ADMIN_PASSWORD
|
DASHBOARD_PORT: 8080
|
||||||
|
DOCKER_PROJECT: darkproxy
|
||||||
|
DOCKER_SOCKET: /var/run/docker.sock
|
||||||
|
DNS_SERVER: darkpihole
|
||||||
|
DNS_PORT: 53
|
||||||
|
PROXY_HOST: dark3proxy
|
||||||
|
PROXY_PORT: 1080
|
||||||
|
PROXY_USERS_FILE: /run/secrets/PROXY_USERS
|
||||||
|
STATUS_CACHE_SECONDS: 15
|
||||||
|
SOCKET_TIMEOUT: 5
|
||||||
|
LOG_TAIL_LINES: 120
|
||||||
secrets:
|
secrets:
|
||||||
- GRAFANA_ADMIN_PASSWORD
|
- PROXY_USERS
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
ports:
|
||||||
|
- "${STATUS_DASHBOARD_BIND_HOST:-127.0.0.1}:${STATUS_DASHBOARD_BIND_PORT:-2004}:8080"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/healthz', timeout=5).read()"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
networks:
|
networks:
|
||||||
- darkproxy
|
darkproxy:
|
||||||
|
ipv4_address: 10.5.0.21
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
i2pd_state:
|
||||||
|
name: i2pd_state
|
||||||
|
yggdrasil_state:
|
||||||
|
name: yggdrasil_state
|
||||||
darkalfis_data:
|
darkalfis_data:
|
||||||
name: darkalfis_data
|
name: darkalfis_data
|
||||||
emc_data:
|
emc_data:
|
||||||
@@ -350,8 +604,8 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
tailscale_data:
|
tailscale_data:
|
||||||
name: tailscale_data
|
name: tailscale_data
|
||||||
grafana-data:
|
i2p_dns_map:
|
||||||
name: grafana-data
|
name: i2p_dns_map
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
darkproxy:
|
darkproxy:
|
||||||
@@ -372,8 +626,7 @@ secrets:
|
|||||||
file: ./secrets/YGGDRASIL_GENERATE_KEYS.txt
|
file: ./secrets/YGGDRASIL_GENERATE_KEYS.txt
|
||||||
PIHOLE_WEBPASSWORD:
|
PIHOLE_WEBPASSWORD:
|
||||||
file: ./secrets/pihole_webpassword.txt
|
file: ./secrets/pihole_webpassword.txt
|
||||||
GRAFANA_ADMIN_PASSWORD:
|
|
||||||
file: ./secrets/grafana_admin_password.txt
|
|
||||||
PROXY_USERS:
|
PROXY_USERS:
|
||||||
file: ./secrets/3proxy_users.txt
|
file: ./secrets/3proxy_users.txt
|
||||||
|
NAMECOIN_RPC_PASSWORD:
|
||||||
|
file: ./secrets/namecoin_rpc_password.txt
|
||||||
|
|||||||
+61
-8
@@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
from dnslib import A, AAAA, CNAME, QTYPE, RR, TXT, DNSHeader, DNSRecord, RCODE
|
from dnslib import A, AAAA, CNAME, QTYPE, RR, TXT, DNSHeader, DNSRecord, RCODE
|
||||||
from web3 import HTTPProvider, Web3
|
from web3 import HTTPProvider, Web3
|
||||||
@@ -11,6 +12,7 @@ RPC_URL = os.getenv("ENS_RPC_URL", "https://ethereum-rpc.publicnode.com")
|
|||||||
LISTEN_HOST = os.getenv("ENSDNS_LISTEN_HOST", "0.0.0.0")
|
LISTEN_HOST = os.getenv("ENSDNS_LISTEN_HOST", "0.0.0.0")
|
||||||
LISTEN_PORT = int(os.getenv("ENSDNS_LISTEN_PORT", "53"))
|
LISTEN_PORT = int(os.getenv("ENSDNS_LISTEN_PORT", "53"))
|
||||||
DEFAULT_TTL = int(os.getenv("ENSDNS_TTL", "60"))
|
DEFAULT_TTL = int(os.getenv("ENSDNS_TTL", "60"))
|
||||||
|
RECONNECT_INTERVAL = float(os.getenv("ENSDNS_RECONNECT_INTERVAL", "30"))
|
||||||
|
|
||||||
ENS_REGISTRY_ADDRESS = Web3.to_checksum_address("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e")
|
ENS_REGISTRY_ADDRESS = Web3.to_checksum_address("0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e")
|
||||||
|
|
||||||
@@ -44,6 +46,10 @@ PUBLIC_RESOLVER_ABI = [
|
|||||||
EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"
|
EMPTY_ADDRESS = "0x0000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
|
||||||
|
class ENSBackendUnavailable(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def namehash(name: str) -> bytes:
|
def namehash(name: str) -> bytes:
|
||||||
node = b"\x00" * 32
|
node = b"\x00" * 32
|
||||||
labels = [label for label in name.strip().lower().split(".") if label]
|
labels = [label for label in name.strip().lower().split(".") if label]
|
||||||
@@ -59,18 +65,58 @@ def normalize_qname(qname: str) -> str:
|
|||||||
|
|
||||||
class ENSResolver:
|
class ENSResolver:
|
||||||
def __init__(self, rpc_url: str):
|
def __init__(self, rpc_url: str):
|
||||||
self.web3 = Web3(HTTPProvider(rpc_url, request_kwargs={"timeout": 10}))
|
self.rpc_url = rpc_url
|
||||||
if not self.web3.is_connected():
|
self._lock = threading.Lock()
|
||||||
raise RuntimeError(f"Could not connect to Ethereum RPC: {rpc_url}")
|
self._web3 = None
|
||||||
self.registry = self.web3.eth.contract(address=ENS_REGISTRY_ADDRESS, abi=ENS_REGISTRY_ABI)
|
self._registry = None
|
||||||
|
self._last_connect_attempt = 0.0
|
||||||
|
self._last_error = None
|
||||||
|
|
||||||
|
def _connect(self, force: bool = False) -> bool:
|
||||||
|
now = time.monotonic()
|
||||||
|
with self._lock:
|
||||||
|
if self._web3 is not None and self._registry is not None and self._web3.is_connected():
|
||||||
|
return True
|
||||||
|
if not force and now - self._last_connect_attempt < RECONNECT_INTERVAL:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._last_connect_attempt = now
|
||||||
|
web3 = Web3(HTTPProvider(self.rpc_url, request_kwargs={"timeout": 10}))
|
||||||
|
if not web3.is_connected():
|
||||||
|
self._web3 = None
|
||||||
|
self._registry = None
|
||||||
|
self._last_error = f"Could not connect to Ethereum RPC: {self.rpc_url}"
|
||||||
|
print(f"[ensdns] {self._last_error}", flush=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._web3 = web3
|
||||||
|
self._registry = web3.eth.contract(address=ENS_REGISTRY_ADDRESS, abi=ENS_REGISTRY_ABI)
|
||||||
|
self._last_error = None
|
||||||
|
print(f"[ensdns] connected to {self.rpc_url}", flush=True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _ensure_backend(self):
|
||||||
|
if not self._connect():
|
||||||
|
detail = self._last_error or f"Could not connect to Ethereum RPC: {self.rpc_url}"
|
||||||
|
raise ENSBackendUnavailable(detail)
|
||||||
|
return self._web3, self._registry
|
||||||
|
|
||||||
def resolve(self, name: str):
|
def resolve(self, name: str):
|
||||||
|
web3, registry = self._ensure_backend()
|
||||||
node = namehash(name)
|
node = namehash(name)
|
||||||
resolver_addr = self.registry.functions.resolver(node).call()
|
try:
|
||||||
|
resolver_addr = registry.functions.resolver(node).call()
|
||||||
|
except Exception as exc:
|
||||||
|
with self._lock:
|
||||||
|
self._web3 = None
|
||||||
|
self._registry = None
|
||||||
|
self._last_error = f"ENS registry lookup failed: {exc}"
|
||||||
|
raise ENSBackendUnavailable(self._last_error) from exc
|
||||||
|
|
||||||
if not resolver_addr or resolver_addr == EMPTY_ADDRESS:
|
if not resolver_addr or resolver_addr == EMPTY_ADDRESS:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
resolver = self.web3.eth.contract(address=resolver_addr, abi=PUBLIC_RESOLVER_ABI)
|
resolver = web3.eth.contract(address=resolver_addr, abi=PUBLIC_RESOLVER_ABI)
|
||||||
|
|
||||||
addr = None
|
addr = None
|
||||||
contenthash = None
|
contenthash = None
|
||||||
@@ -105,7 +151,13 @@ def answer_query(record: DNSRecord, ens: ENSResolver) -> DNSRecord:
|
|||||||
reply.header.rcode = RCODE.NXDOMAIN
|
reply.header.rcode = RCODE.NXDOMAIN
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
resolved = ens.resolve(qname)
|
try:
|
||||||
|
resolved = ens.resolve(qname)
|
||||||
|
except ENSBackendUnavailable as exc:
|
||||||
|
print(f"[ensdns] backend unavailable while resolving {qname}: {exc}", flush=True)
|
||||||
|
reply.header.rcode = RCODE.SERVFAIL
|
||||||
|
return reply
|
||||||
|
|
||||||
if resolved is None:
|
if resolved is None:
|
||||||
reply.header.rcode = RCODE.NXDOMAIN
|
reply.header.rcode = RCODE.NXDOMAIN
|
||||||
return reply
|
return reply
|
||||||
@@ -163,8 +215,9 @@ def serve_tcp(sock: socket.socket, ens: ENSResolver):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
ens = ENSResolver(RPC_URL)
|
ens = ENSResolver(RPC_URL)
|
||||||
print(f"[ensdns] connected to {RPC_URL}", flush=True)
|
|
||||||
print(f"[ensdns] listening on {LISTEN_HOST}:{LISTEN_PORT} (udp/tcp)", flush=True)
|
print(f"[ensdns] listening on {LISTEN_HOST}:{LISTEN_PORT} (udp/tcp)", flush=True)
|
||||||
|
if not ens._connect(force=True):
|
||||||
|
print(f"[ensdns] starting degraded; RPC unavailable at {RPC_URL}", flush=True)
|
||||||
|
|
||||||
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
udp_sock.bind((LISTEN_HOST, LISTEN_PORT))
|
udp_sock.bind((LISTEN_HOST, LISTEN_PORT))
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
ARG UBUNTU_VERSION=24.04
|
||||||
|
|
||||||
|
# I2P build stage
|
||||||
|
# Build on target platform so produced binaries match runtime architecture.
|
||||||
|
FROM --platform=$TARGETPLATFORM ubuntu:${UBUNTU_VERSION} AS builder_i2pd
|
||||||
|
ARG I2PD_VERSION=2.58.0
|
||||||
|
ARG I2PD_COMPILER=gcc
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get -y upgrade && \
|
||||||
|
apt-get install -y \
|
||||||
|
$I2PD_COMPILER \
|
||||||
|
git make cmake debhelper \
|
||||||
|
libboost-date-time-dev \
|
||||||
|
libboost-filesystem-dev \
|
||||||
|
libboost-program-options-dev \
|
||||||
|
libboost-system-dev \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libminiupnpc-dev && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /BUILD_I2PD/
|
||||||
|
RUN git config --global advice.detachedHead false && \
|
||||||
|
git clone --depth 1 --branch $I2PD_VERSION https://github.com/PurpleI2P/i2pd.git
|
||||||
|
|
||||||
|
WORKDIR /BUILD_I2PD/i2pd/build
|
||||||
|
RUN cmake -DCMAKE_BUILD_TYPE=Release -DWITH_AESNI=${AESNI_SUPPORT:-OFF} -DWITH_UPNP=ON . && \
|
||||||
|
make -j$(nproc)
|
||||||
|
|
||||||
|
# Yggdrasil build stage
|
||||||
|
# Build on target platform so produced binaries match runtime architecture.
|
||||||
|
FROM --platform=$TARGETPLATFORM ubuntu:${UBUNTU_VERSION} AS builder_yggdrasil
|
||||||
|
ARG YGGDRASIL_VERSION=v0.5.12
|
||||||
|
ARG BUILDPLATFORM
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get -y upgrade && \
|
||||||
|
apt-get install -y git golang && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /BUILD_YGGDRASIL/
|
||||||
|
RUN git config --global advice.detachedHead false && \
|
||||||
|
git clone --depth 1 --branch $YGGDRASIL_VERSION https://github.com/yggdrasil-network/yggdrasil-go.git
|
||||||
|
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
WORKDIR /BUILD_YGGDRASIL/yggdrasil-go
|
||||||
|
# Add genkeys to build and build binaries
|
||||||
|
RUN sed -i 's/yggdrasil yggdrasilctl/yggdrasil yggdrasilctl genkeys/g' build && \
|
||||||
|
./build
|
||||||
|
|
||||||
|
# Utility scripts for enhanced Yggdrasil functionality
|
||||||
|
RUN git clone --depth 1 --branch v0 https://github.com/oldnick85/yggdrasil_get_keys.git /UTILS/yggdrasil_get_keys && \
|
||||||
|
git clone --depth 1 --branch v4 https://github.com/oldnick85/yggdrasil_find_public_peers.git /UTILS/yggdrasil_find_public_peers && \
|
||||||
|
rm -rf /UTILS/yggdrasil_get_keys/.git /UTILS/yggdrasil_find_public_peers/.git
|
||||||
|
|
||||||
|
# Final runtime image
|
||||||
|
FROM ubuntu:${UBUNTU_VERSION}
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
|
||||||
|
apt-get -y upgrade && \
|
||||||
|
apt-get install -y \
|
||||||
|
libboost-date-time-dev \
|
||||||
|
libboost-filesystem-dev \
|
||||||
|
libboost-program-options-dev \
|
||||||
|
libboost-system-dev \
|
||||||
|
libssl3 \
|
||||||
|
zlib1g \
|
||||||
|
libminiupnpc17 \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
iputils-ping \
|
||||||
|
ca-certificates && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
|
# ==== UTILS ====
|
||||||
|
WORKDIR /UTILS/
|
||||||
|
COPY --from=builder_yggdrasil /UTILS/ /UTILS/
|
||||||
|
|
||||||
|
# Install Python dependencies securely
|
||||||
|
RUN python3 -m pip install --no-cache-dir --break-system-packages \
|
||||||
|
-r /UTILS/yggdrasil_get_keys/requirements.txt \
|
||||||
|
-r /UTILS/yggdrasil_find_public_peers/requirements.txt
|
||||||
|
|
||||||
|
# Cache public peers during build for fallback
|
||||||
|
RUN python3 /UTILS/yggdrasil_find_public_peers/yggdrasil_find_public_peers.py \
|
||||||
|
--yggdrasil-conf="" \
|
||||||
|
--yggdrasil-peers-json="/UTILS/yggdrasil_find_public_peers/public_peers.json" || true
|
||||||
|
|
||||||
|
# ==== I2P CONFIGURATION ====
|
||||||
|
# I2P Service Ports
|
||||||
|
# Web console
|
||||||
|
EXPOSE 7070
|
||||||
|
# HTTP proxy
|
||||||
|
EXPOSE 4444
|
||||||
|
# SOCKS proxy
|
||||||
|
EXPOSE 4447
|
||||||
|
# SAM bridge
|
||||||
|
EXPOSE 7656
|
||||||
|
# BOB bridge
|
||||||
|
EXPOSE 2827
|
||||||
|
# I2CP
|
||||||
|
EXPOSE 7654
|
||||||
|
# I2PControl
|
||||||
|
EXPOSE 7650
|
||||||
|
# Main listener
|
||||||
|
EXPOSE 10765
|
||||||
|
|
||||||
|
WORKDIR /I2PD/
|
||||||
|
COPY --from=builder_i2pd /BUILD_I2PD/i2pd/build/i2pd .
|
||||||
|
COPY --from=builder_i2pd /BUILD_I2PD/i2pd/contrib/certificates ./certificates
|
||||||
|
COPY ./i2pd.conf .
|
||||||
|
|
||||||
|
# Increase file descriptor limit for better performance
|
||||||
|
RUN ulimit -n 4096
|
||||||
|
|
||||||
|
# ==== YGGDRASIL CONFIGURATION ====
|
||||||
|
# Yggdrasil listener
|
||||||
|
EXPOSE 10654
|
||||||
|
|
||||||
|
WORKDIR /YGGDRASIL/
|
||||||
|
COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/yggdrasil .
|
||||||
|
COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/yggdrasilctl .
|
||||||
|
COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/genkeys .
|
||||||
|
COPY ./yggdrasil.conf .
|
||||||
|
|
||||||
|
# ==== FINAL SETUP ====
|
||||||
|
WORKDIR /
|
||||||
|
COPY ./entrypoint.sh .
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Health check to ensure services are running
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
|
CMD bash -c 'pgrep i2pd && pgrep yggdrasil'
|
||||||
|
|
||||||
|
CMD ["bash", "/entrypoint.sh"]
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
echo "Starting I2P over Yggdrasil setup..."
|
||||||
|
|
||||||
|
# Function to handle graceful shutdown
|
||||||
|
cleanup() {
|
||||||
|
echo "Received shutdown signal, stopping services..."
|
||||||
|
kill -TERM $i2pd_pid $yggdrasil_pid 2>/dev/null
|
||||||
|
wait
|
||||||
|
echo "Services stopped gracefully."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup SIGTERM SIGINT
|
||||||
|
|
||||||
|
# Generate strong Yggdrasil address (https://yggdrasil-network.github.io/configuration.html#generating-stronger-addresses-and-prefixes)
|
||||||
|
echo "Generating Yggdrasil keys..."
|
||||||
|
python3 /UTILS/yggdrasil_get_keys/yggdrasil_get_keys.py \
|
||||||
|
--genkeys="/YGGDRASIL/genkeys" \
|
||||||
|
--yggdrasil-conf="/YGGDRASIL/yggdrasil.conf" \
|
||||||
|
--timeout=60 \
|
||||||
|
--environment
|
||||||
|
|
||||||
|
# Find and configure public Yggdrasil peers
|
||||||
|
echo "Discovering Yggdrasil public peers..."
|
||||||
|
python3 /UTILS/yggdrasil_find_public_peers/yggdrasil_find_public_peers.py \
|
||||||
|
--yggdrasil-conf="/YGGDRASIL/yggdrasil.conf" \
|
||||||
|
--yggdrasil-peers-json="/UTILS/yggdrasil_find_public_peers/public_peers.json" \
|
||||||
|
--parallel=4 \
|
||||||
|
--pings=10 \
|
||||||
|
--best=6 \
|
||||||
|
--max-from-country=2 \
|
||||||
|
--ping-interval=0.5
|
||||||
|
|
||||||
|
# Enable IPv6 (required for Yggdrasil)
|
||||||
|
echo "Enabling IPv6..."
|
||||||
|
sysctl net.ipv6.conf.all.disable_ipv6=0 || true
|
||||||
|
|
||||||
|
# Start Yggdrasil in background
|
||||||
|
echo "Starting Yggdrasil..."
|
||||||
|
/YGGDRASIL/yggdrasil -useconffile /YGGDRASIL/yggdrasil.conf &
|
||||||
|
yggdrasil_pid=$!
|
||||||
|
|
||||||
|
# Wait for Yggdrasil to establish connections
|
||||||
|
echo "Waiting for Yggdrasil network connectivity (60 seconds)..."
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# Start I2P in background
|
||||||
|
echo "Starting I2P..."
|
||||||
|
/I2PD/i2pd --datadir /I2PD --conf /I2PD/i2pd.conf &
|
||||||
|
i2pd_pid=$!
|
||||||
|
|
||||||
|
echo "Both services started successfully!"
|
||||||
|
echo "I2P Web Console: http://localhost:7070"
|
||||||
|
echo "Yggdrasil status: check container logs"
|
||||||
|
|
||||||
|
# Wait for any process to exit and handle restart if needed
|
||||||
|
wait -n
|
||||||
|
|
||||||
|
# If we reach here, one process died
|
||||||
|
echo "One of the services stopped unexpectedly, shutting down..."
|
||||||
|
kill -TERM $i2pd_pid $yggdrasil_pid 2>/dev/null
|
||||||
|
wait
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
## Configuration file for a typical i2pd user
|
||||||
|
## See https://i2pd.readthedocs.io/en/latest/user-guide/configuration/
|
||||||
|
## for more options you can use in this file.
|
||||||
|
|
||||||
|
## Lines that begin with "## " try to explain what's going on. Lines
|
||||||
|
## that begin with just "#" are disabled commands: you can enable them
|
||||||
|
## by removing the "#" symbol.
|
||||||
|
|
||||||
|
## Tunnels config file
|
||||||
|
## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf
|
||||||
|
# tunconf = /var/lib/i2pd/tunnels.conf
|
||||||
|
|
||||||
|
## Tunnels config files path
|
||||||
|
## Use that path to store separated tunnels in different config files.
|
||||||
|
## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d
|
||||||
|
# tunnelsdir = /var/lib/i2pd/tunnels.d
|
||||||
|
|
||||||
|
## Path to certificates used for verifying .su3, families
|
||||||
|
## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates
|
||||||
|
# certsdir = /var/lib/i2pd/certificates
|
||||||
|
|
||||||
|
## Where to write pidfile (default: i2pd.pid, not used in Windows)
|
||||||
|
# pidfile = /run/i2pd.pid
|
||||||
|
|
||||||
|
## Logging configuration section
|
||||||
|
## By default logs go to stdout with level 'info' and higher
|
||||||
|
## For Windows OS by default logs go to file with level 'warn' and higher
|
||||||
|
##
|
||||||
|
## Logs destination (valid values: stdout, file, syslog)
|
||||||
|
## * stdout - print log entries to stdout
|
||||||
|
## * file - log entries to a file
|
||||||
|
## * syslog - use syslog, see man 3 syslog
|
||||||
|
# log = file
|
||||||
|
## Path to logfile (default - autodetect)
|
||||||
|
# logfile = /var/log/i2pd/i2pd.log
|
||||||
|
## Log messages above this level (debug, info, *warn, error, none)
|
||||||
|
## If you set it to none, logging will be disabled
|
||||||
|
loglevel = info # show informational messages (including SOCKS startup)
|
||||||
|
## Write full CLF-formatted date and time to log (default: write only time)
|
||||||
|
# logclftime = true
|
||||||
|
|
||||||
|
## Daemon mode. Router will go to background after start. Ignored on Windows
|
||||||
|
# daemon = true
|
||||||
|
|
||||||
|
## Specify a family, router belongs to (default - none)
|
||||||
|
# family =
|
||||||
|
|
||||||
|
## Network interface to bind to
|
||||||
|
## Updates address4/6 options if they are not set
|
||||||
|
# ifname =
|
||||||
|
## You can specify different interfaces for IPv4 and IPv6
|
||||||
|
# ifname4 =
|
||||||
|
# ifname6 =
|
||||||
|
|
||||||
|
## Local address to bind transport sockets to
|
||||||
|
## Overrides host option if:
|
||||||
|
## For ipv4: if ipv4 = true and nat = false
|
||||||
|
## For ipv6: if 'host' is not set or ipv4 = true
|
||||||
|
# address4 =
|
||||||
|
# address6 =
|
||||||
|
|
||||||
|
## External IPv4 or IPv6 address to listen for connections
|
||||||
|
## By default i2pd sets IP automatically
|
||||||
|
## Sets published NTCP2v4/SSUv4 address to 'host' value if nat = true
|
||||||
|
## Sets published NTCP2v6/SSUv6 address to 'host' value if ipv4 = false
|
||||||
|
# host = 1.2.3.4
|
||||||
|
|
||||||
|
## Port to listen for connections
|
||||||
|
## By default i2pd picks random port. You MUST pick a random number too,
|
||||||
|
## don't just uncomment this
|
||||||
|
port = 10765
|
||||||
|
|
||||||
|
## Enable communication through ipv4
|
||||||
|
# IPv4 networking is required for I2P router connectivity and reseeding.
|
||||||
|
# Earlier we disabled it to restrict traffic to Yggdrasil only, but that
|
||||||
|
# prevented the router from ever contacting peers or downloading its
|
||||||
|
# initial NetDB. Enable at least for bootstrap; once the router has
|
||||||
|
# peers it will happily tunnel them over Yggdrasil if configured.
|
||||||
|
ipv4 = true
|
||||||
|
## Enable communication through ipv6
|
||||||
|
# This MUST be true so i2pd can talk over the Yggdrasil mesh – without it
|
||||||
|
# Yggdrasil networking is broken, and the container will not be able to
|
||||||
|
# reach the I2P network at all.
|
||||||
|
ipv6 = true
|
||||||
|
|
||||||
|
## Enable SSU transport (default = true)
|
||||||
|
ssu = false
|
||||||
|
|
||||||
|
## Bandwidth configuration
|
||||||
|
## L limit bandwidth to 32KBs/sec, O - to 256KBs/sec, P - to 2048KBs/sec,
|
||||||
|
## X - unlimited
|
||||||
|
## Default is L (regular node) and X if floodfill mode enabled. If you want to
|
||||||
|
## share more bandwidth without floodfill mode, uncomment that line and adjust
|
||||||
|
## value to your possibilities
|
||||||
|
# bandwidth = L
|
||||||
|
## Max % of bandwidth limit for transit. 0-100. 100 by default
|
||||||
|
# share = 100
|
||||||
|
|
||||||
|
## Router will not accept transit tunnels, disabling transit traffic completely
|
||||||
|
## (default = false)
|
||||||
|
# notransit = true
|
||||||
|
|
||||||
|
## Router will be floodfill
|
||||||
|
## Note: that mode uses much more network connections and CPU!
|
||||||
|
floodfill = true
|
||||||
|
|
||||||
|
[ntcp2]
|
||||||
|
## Enable NTCP2 transport (default = true)
|
||||||
|
# enabled = true
|
||||||
|
## Publish address in RouterInfo (default = true)
|
||||||
|
# published = true
|
||||||
|
## Port for incoming connections (default is global port option value)
|
||||||
|
# port = 4567
|
||||||
|
|
||||||
|
[ssu2]
|
||||||
|
## Enable SSU2 transport (default = false for 2.43.0)
|
||||||
|
enabled = true
|
||||||
|
## Publish address in RouterInfo (default = false for 2.43.0)
|
||||||
|
published = true
|
||||||
|
## Port for incoming connections (default is global port option value or port + 1 if SSU is enabled)
|
||||||
|
# port = 4567
|
||||||
|
|
||||||
|
[http]
|
||||||
|
## Web Console settings
|
||||||
|
## Uncomment and set to 'false' to disable Web Console
|
||||||
|
# enabled = true
|
||||||
|
## Address and port service will listen on
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = 7070
|
||||||
|
## Path to web console, default "/"
|
||||||
|
# webroot = /
|
||||||
|
## Uncomment following lines to enable Web Console authentication
|
||||||
|
# auth = true
|
||||||
|
# user = i2pd
|
||||||
|
# pass = changeme
|
||||||
|
## Select webconsole language
|
||||||
|
## Currently supported english (default), afrikaans, armenian, chinese, french,
|
||||||
|
## german, russian, turkmen, ukrainian and uzbek languages
|
||||||
|
# lang = english
|
||||||
|
hostname = localhost
|
||||||
|
#strictheaders = false
|
||||||
|
|
||||||
|
[httpproxy]
|
||||||
|
## Uncomment and set to 'false' to disable HTTP Proxy
|
||||||
|
# enabled = true
|
||||||
|
## Address and port service will listen on
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = 4444
|
||||||
|
## Optional keys file for proxy local destination
|
||||||
|
# keys = http-proxy-keys.dat
|
||||||
|
## Enable address helper for adding .i2p domains with "jump URLs" (default: true)
|
||||||
|
# addresshelper = true
|
||||||
|
## Address of a proxy server inside I2P, which is used to visit regular Internet
|
||||||
|
# outproxy = http://false.i2p
|
||||||
|
## httpproxy section also accepts I2CP parameters, like "inbound.length" etc.
|
||||||
|
|
||||||
|
# [dns] section removed because the I2PD version used in this image
|
||||||
|
# does not support a built-in DNS server. Name resolution for .i2p domains
|
||||||
|
# is performed internally by the SOCKS proxy, so the external DNS listener
|
||||||
|
# is unnecessary. If you upgrade to a future I2PD release that adds this
|
||||||
|
# feature, you can restore the section.
|
||||||
|
#
|
||||||
|
# [dns]
|
||||||
|
# ## enable DNS server for resolving *.i2p names
|
||||||
|
# enabled = true
|
||||||
|
# address = 0.0.0.0
|
||||||
|
# port = 53
|
||||||
|
|
||||||
|
|
||||||
|
[socksproxy]
|
||||||
|
## Uncomment and set to 'false' to disable SOCKS Proxy
|
||||||
|
enabled = true
|
||||||
|
## Address and port service will listen on
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = 4447
|
||||||
|
## Optional keys file for proxy local destination
|
||||||
|
# keys = socks-proxy-keys.dat
|
||||||
|
## Socks outproxy. Example below is set to use Tor for all connections except i2p
|
||||||
|
## Uncomment and set to 'true' to enable using of SOCKS outproxy
|
||||||
|
# outproxy.enabled = false
|
||||||
|
## Address and port of outproxy
|
||||||
|
# outproxy = 127.0.0.1
|
||||||
|
# outproxyport = 9050
|
||||||
|
## socksproxy section also accepts I2CP parameters, like "inbound.length" etc.
|
||||||
|
|
||||||
|
[sam]
|
||||||
|
## Comment or set to 'false' to disable SAM Bridge
|
||||||
|
enabled = true
|
||||||
|
## Address and port service will listen on
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = 7656
|
||||||
|
|
||||||
|
[bob]
|
||||||
|
## Uncomment and set to 'true' to enable BOB command channel
|
||||||
|
# enabled = false
|
||||||
|
## Address and port service will listen on
|
||||||
|
# address = 127.0.0.1
|
||||||
|
# port = 2827
|
||||||
|
|
||||||
|
[i2cp]
|
||||||
|
## Uncomment and set to 'true' to enable I2CP protocol
|
||||||
|
enabled = true
|
||||||
|
## Address and port service will listen on
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = 7654
|
||||||
|
|
||||||
|
[i2pcontrol]
|
||||||
|
## Uncomment and set to 'true' to enable I2PControl protocol
|
||||||
|
# enabled = false
|
||||||
|
## Address and port service will listen on
|
||||||
|
# address = 127.0.0.1
|
||||||
|
# port = 7650
|
||||||
|
## Authentication password. "itoopie" by default
|
||||||
|
# password = itoopie
|
||||||
|
|
||||||
|
[precomputation]
|
||||||
|
## Enable or disable elgamal precomputation table
|
||||||
|
## By default, enabled on i386 hosts
|
||||||
|
# elgamal = true
|
||||||
|
|
||||||
|
[upnp]
|
||||||
|
## Enable or disable UPnP: automatic port forwarding (enabled by default in WINDOWS, ANDROID)
|
||||||
|
# enabled = false
|
||||||
|
## Name i2pd appears in UPnP forwardings list (default = I2Pd)
|
||||||
|
# name = I2Pd
|
||||||
|
|
||||||
|
[meshnets]
|
||||||
|
## Enable connectivity over the Yggdrasil network
|
||||||
|
yggdrasil = true
|
||||||
|
## You can bind address from your Yggdrasil subnet 300::/64
|
||||||
|
## The address must first be added to the network interface
|
||||||
|
# yggaddress =
|
||||||
|
|
||||||
|
[reseed]
|
||||||
|
## Options for bootstrapping into I2P network, aka reseeding
|
||||||
|
## Enable or disable reseed data verification.
|
||||||
|
verify = true
|
||||||
|
## URLs to request reseed data from, separated by comma
|
||||||
|
## Default: "mainline" I2P Network reseeds
|
||||||
|
# urls = https://reseed.i2p-projekt.de/,https://i2p.mooo.com/netDb/,https://netdb.i2p2.no/
|
||||||
|
## Reseed URLs through the Yggdrasil, separated by comma
|
||||||
|
# yggurls = http://[324:9de3:fea4:f6ac::ace]:7070/
|
||||||
|
## Path to local reseed data file (.su3) for manual reseeding
|
||||||
|
# file = /path/to/i2pseeds.su3
|
||||||
|
## or HTTPS URL to reseed from
|
||||||
|
# file = https://legit-website.com/i2pseeds.su3
|
||||||
|
## Path to local ZIP file or HTTPS URL to reseed from
|
||||||
|
# zipfile = /path/to/netDb.zip
|
||||||
|
## If you run i2pd behind a proxy server, set proxy server for reseeding here
|
||||||
|
## Should be http://address:port or socks://address:port
|
||||||
|
# proxy = http://127.0.0.1:8118
|
||||||
|
## Minimum number of known routers, below which i2pd triggers reseeding. 25 by default
|
||||||
|
# threshold = 25
|
||||||
|
|
||||||
|
[addressbook]
|
||||||
|
## AddressBook subscription URL for initial setup
|
||||||
|
## Default: reg.i2p at "mainline" I2P Network
|
||||||
|
# defaulturl = http://shx5vqsw7usdaunyzr2qmes2fq37oumybpudrd4jjj4e4vk4uusa.b32.i2p/hosts.txt
|
||||||
|
## Optional subscriptions URLs, separated by comma
|
||||||
|
# subscriptions = http://reg.i2p/hosts.txt,http://identiguy.i2p/hosts.txt,http://stats.i2p/cgi-bin/newhosts.txt,http://rus.i2p/hosts.txt
|
||||||
|
|
||||||
|
[limits]
|
||||||
|
## Maximum active transit sessions (default:2500)
|
||||||
|
# transittunnels = 2500
|
||||||
|
## Limit number of open file descriptors (0 - use system limit)
|
||||||
|
# openfiles = 0
|
||||||
|
## Maximum size of corefile in Kb (0 - use system limit)
|
||||||
|
# coresize = 0
|
||||||
|
|
||||||
|
[trust]
|
||||||
|
## Enable explicit trust options. false by default
|
||||||
|
# enabled = true
|
||||||
|
## Make direct I2P connections only to routers in specified Family.
|
||||||
|
# family = MyFamily
|
||||||
|
## Make direct I2P connections only to routers specified here. Comma separated list of base64 identities.
|
||||||
|
# routers =
|
||||||
|
## Should we hide our router from other routers? false by default
|
||||||
|
# hidden = true
|
||||||
|
|
||||||
|
[exploratory]
|
||||||
|
## Exploratory tunnels settings with default values
|
||||||
|
# inbound.length = 2
|
||||||
|
# inbound.quantity = 3
|
||||||
|
# outbound.length = 2
|
||||||
|
# outbound.quantity = 3
|
||||||
|
|
||||||
|
[persist]
|
||||||
|
## Save peer profiles on disk (default: true)
|
||||||
|
# profiles = true
|
||||||
|
## Save full addresses on disk (default: true)
|
||||||
|
# addressbook = true
|
||||||
|
|
||||||
|
[cpuext]
|
||||||
|
## Use CPU AES-NI instructions set when work with cryptography when available (default: true)
|
||||||
|
# aesni = true
|
||||||
|
## Use CPU AVX instructions set when work with cryptography when available (default: true)
|
||||||
|
# avx = true
|
||||||
|
## Force usage of CPU instructions set, even if they not found
|
||||||
|
## DO NOT TOUCH that option if you really don't know what are you doing!
|
||||||
|
# force = false
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{
|
||||||
|
# List of connection strings for outbound peer connections in URI format,
|
||||||
|
# e.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections
|
||||||
|
# will obey the operating system routing table, therefore you should
|
||||||
|
# use this section when you may connect via different interfaces.
|
||||||
|
Peers: []
|
||||||
|
|
||||||
|
# List of connection strings for outbound peer connections in URI format,
|
||||||
|
# arranged by source interface, e.g. { "eth0": [ "tls://a.b.c.d:e" ] }.
|
||||||
|
# Note that SOCKS peerings will NOT be affected by this option and should
|
||||||
|
# go in the "Peers" section instead.
|
||||||
|
InterfacePeers: {}
|
||||||
|
|
||||||
|
# Listen addresses for incoming connections. You will need to add
|
||||||
|
# listeners in order to accept incoming peerings from non-local nodes.
|
||||||
|
# Multicast peer discovery will work regardless of any listeners set
|
||||||
|
# here. Each listener should be specified in URI format as above, e.g.
|
||||||
|
# tls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces.
|
||||||
|
Listen: [
|
||||||
|
tls://[::]:10654
|
||||||
|
]
|
||||||
|
|
||||||
|
# Listen address for admin connections. Default is to listen for local
|
||||||
|
# connections either on TCP/9001 or a UNIX socket depending on your
|
||||||
|
# platform. Use this value for yggdrasilctl -endpoint=X. To disable
|
||||||
|
# the admin socket, use the value "none" instead.
|
||||||
|
AdminListen: unix:///var/run/yggdrasil.sock
|
||||||
|
|
||||||
|
# Configuration for which interfaces multicast peer discovery should be
|
||||||
|
# enabled on. Each entry in the list should be a json object which may
|
||||||
|
# contain Regex, Beacon, Listen, and Port. Regex is a regular expression
|
||||||
|
# which is matched against an interface name, and interfaces use the
|
||||||
|
# first configuration that they match gainst. Beacon configures whether
|
||||||
|
# or not the node should send link-local multicast beacons to advertise
|
||||||
|
# their presence, while listening for incoming connections on Port.
|
||||||
|
# Listen controls whether or not the node listens for multicast beacons
|
||||||
|
# and opens outgoing connections.
|
||||||
|
MulticastInterfaces:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
Regex: .*
|
||||||
|
Beacon: true
|
||||||
|
Listen: true
|
||||||
|
Port: 0
|
||||||
|
Priority: 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# List of peer public keys to allow incoming peering connections
|
||||||
|
# from. If left empty/undefined then all connections will be allowed
|
||||||
|
# by default. This does not affect outgoing peerings, nor does it
|
||||||
|
# affect link-local peers discovered via multicast.
|
||||||
|
AllowedPublicKeys: []
|
||||||
|
|
||||||
|
# Your public key. Your peers may ask you for this to put
|
||||||
|
# into their AllowedPublicKeys configuration.
|
||||||
|
PublicKey:
|
||||||
|
|
||||||
|
# Your private key. DO NOT share this with anyone!
|
||||||
|
PrivateKey:
|
||||||
|
|
||||||
|
# Local network interface name for TUN adapter, or "auto" to select
|
||||||
|
# an interface automatically, or "none" to run without TUN.
|
||||||
|
IfName: auto
|
||||||
|
|
||||||
|
# Maximum Transmission Unit (MTU) size for your local TUN interface.
|
||||||
|
# Default is the largest supported size for your platform. The lowest
|
||||||
|
# possible value is 1280.
|
||||||
|
IfMTU: 65535
|
||||||
|
|
||||||
|
# By default, nodeinfo contains some defaults including the platform,
|
||||||
|
# architecture and Yggdrasil version. These can help when surveying
|
||||||
|
# the network and diagnosing network routing problems. Enabling
|
||||||
|
# nodeinfo privacy prevents this, so that only items specified in
|
||||||
|
# "NodeInfo" are sent back if specified.
|
||||||
|
NodeInfoPrivacy: false
|
||||||
|
|
||||||
|
# Optional node info. This must be a { "key": "value", ... } map
|
||||||
|
# or set as null. This is entirely optional but, if set, is visible
|
||||||
|
# to the whole network on request.
|
||||||
|
NodeInfo: {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.11-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir "dnslib>=0.9.24"
|
||||||
|
|
||||||
|
COPY server.py /app/server.py
|
||||||
|
|
||||||
|
EXPOSE 53/udp
|
||||||
|
EXPOSE 53/tcp
|
||||||
|
|
||||||
|
CMD ["python", "/app/server.py"]
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import hashlib
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
from dnslib import A, DNSHeader, DNSRecord, QTYPE, RCODE, RR
|
||||||
|
|
||||||
|
LISTEN_HOST = os.getenv("I2PDNS_LISTEN_HOST", "0.0.0.0")
|
||||||
|
LISTEN_PORT = int(os.getenv("I2PDNS_LISTEN_PORT", "53"))
|
||||||
|
DEFAULT_TTL = int(os.getenv("I2PDNS_TTL", "60"))
|
||||||
|
POOL_CIDR = os.getenv("I2PDNS_POOL_CIDR", "172.31.0.0/16")
|
||||||
|
MAP_FILE = os.getenv("I2PDNS_MAP_FILE", "/var/lib/i2pdns/map.json")
|
||||||
|
|
||||||
|
POOL = ipaddress.ip_network(POOL_CIDR, strict=True)
|
||||||
|
if POOL.version != 4 or POOL.num_addresses < 4:
|
||||||
|
raise RuntimeError(f"I2PDNS_POOL_CIDR must be an IPv4 network with >=4 addresses: {POOL_CIDR}")
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_map() -> Dict[str, Dict[str, str]]:
|
||||||
|
try:
|
||||||
|
with open(MAP_FILE, "r", encoding="utf-8") as fh:
|
||||||
|
data = json.load(fh)
|
||||||
|
except FileNotFoundError:
|
||||||
|
data = {}
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
host_to_ip = data.get("host_to_ip") if isinstance(data, dict) else None
|
||||||
|
ip_to_host = data.get("ip_to_host") if isinstance(data, dict) else None
|
||||||
|
if not isinstance(host_to_ip, dict):
|
||||||
|
host_to_ip = {}
|
||||||
|
if not isinstance(ip_to_host, dict):
|
||||||
|
ip_to_host = {}
|
||||||
|
return {"host_to_ip": host_to_ip, "ip_to_host": ip_to_host}
|
||||||
|
|
||||||
|
|
||||||
|
def _save_map(data: Dict[str, Dict[str, str]]) -> None:
|
||||||
|
os.makedirs(os.path.dirname(MAP_FILE), exist_ok=True)
|
||||||
|
tmp = MAP_FILE + ".tmp"
|
||||||
|
with open(tmp, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(data, fh, sort_keys=True)
|
||||||
|
os.replace(tmp, MAP_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def _candidate_index(host: str, host_space: int) -> int:
|
||||||
|
digest = hashlib.blake2b(host.encode("utf-8"), digest_size=8).digest()
|
||||||
|
return (int.from_bytes(digest, "big") % host_space) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _index_to_ip(idx: int) -> str:
|
||||||
|
return str(ipaddress.IPv4Address(int(POOL.network_address) + idx))
|
||||||
|
|
||||||
|
|
||||||
|
def _allocate_ip_for_host(host: str) -> str:
|
||||||
|
# Exclude network and broadcast addresses in the pool.
|
||||||
|
host_space = int(POOL.num_addresses) - 2
|
||||||
|
if host_space <= 0:
|
||||||
|
raise RuntimeError("I2P DNS pool has no usable host addresses")
|
||||||
|
|
||||||
|
with _lock:
|
||||||
|
data = _load_map()
|
||||||
|
host_to_ip = data["host_to_ip"]
|
||||||
|
ip_to_host = data["ip_to_host"]
|
||||||
|
|
||||||
|
existing = host_to_ip.get(host)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
|
||||||
|
start = _candidate_index(host, host_space)
|
||||||
|
idx = start
|
||||||
|
while True:
|
||||||
|
ip = _index_to_ip(idx)
|
||||||
|
current = ip_to_host.get(ip)
|
||||||
|
if current is None or current == host:
|
||||||
|
host_to_ip[host] = ip
|
||||||
|
ip_to_host[ip] = host
|
||||||
|
_save_map(data)
|
||||||
|
return ip
|
||||||
|
|
||||||
|
idx += 1
|
||||||
|
if idx > host_space:
|
||||||
|
idx = 1
|
||||||
|
if idx == start:
|
||||||
|
raise RuntimeError("I2P DNS pool exhausted")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_i2p_name(name: str) -> bool:
|
||||||
|
return name.endswith(".i2p")
|
||||||
|
|
||||||
|
|
||||||
|
def _answer(req: DNSRecord) -> DNSRecord:
|
||||||
|
reply = DNSRecord(DNSHeader(id=req.header.id, qr=1, aa=1, ra=1), q=req.q)
|
||||||
|
|
||||||
|
qname = str(req.q.qname).rstrip(".").lower()
|
||||||
|
qtype = QTYPE[req.q.qtype]
|
||||||
|
|
||||||
|
if not _is_i2p_name(qname):
|
||||||
|
reply.header.rcode = RCODE.NXDOMAIN
|
||||||
|
return reply
|
||||||
|
|
||||||
|
if qtype in ("A", "ANY"):
|
||||||
|
ip = _allocate_ip_for_host(qname)
|
||||||
|
reply.add_answer(RR(str(req.q.qname), QTYPE.A, rdata=A(ip), ttl=DEFAULT_TTL))
|
||||||
|
return reply
|
||||||
|
|
||||||
|
if qtype == "AAAA":
|
||||||
|
# We only synthesize IPv4 addresses for SOCKS routing compatibility.
|
||||||
|
return reply
|
||||||
|
|
||||||
|
# Unsupported RR types get NOERROR with empty answer section.
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
def _serve_udp(sock: socket.socket) -> None:
|
||||||
|
while True:
|
||||||
|
data, addr = sock.recvfrom(4096)
|
||||||
|
try:
|
||||||
|
req = DNSRecord.parse(data)
|
||||||
|
resp = _answer(req).pack()
|
||||||
|
sock.sendto(resp, addr)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_tcp_conn(conn: socket.socket) -> None:
|
||||||
|
try:
|
||||||
|
hdr = conn.recv(2)
|
||||||
|
if len(hdr) != 2:
|
||||||
|
return
|
||||||
|
size = int.from_bytes(hdr, "big")
|
||||||
|
payload = conn.recv(size)
|
||||||
|
if len(payload) != size:
|
||||||
|
return
|
||||||
|
req = DNSRecord.parse(payload)
|
||||||
|
resp = _answer(req).pack()
|
||||||
|
conn.sendall(len(resp).to_bytes(2, "big") + resp)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _serve_tcp(sock: socket.socket) -> None:
|
||||||
|
while True:
|
||||||
|
conn, _ = sock.accept()
|
||||||
|
threading.Thread(target=_handle_tcp_conn, args=(conn,), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print(f"[i2pdns] listening on {LISTEN_HOST}:{LISTEN_PORT}", flush=True)
|
||||||
|
print(f"[i2pdns] pool={POOL} map_file={MAP_FILE}", flush=True)
|
||||||
|
|
||||||
|
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
udp.bind((LISTEN_HOST, LISTEN_PORT))
|
||||||
|
|
||||||
|
tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
tcp.bind((LISTEN_HOST, LISTEN_PORT))
|
||||||
|
tcp.listen(128)
|
||||||
|
|
||||||
|
threading.Thread(target=_serve_udp, args=(udp,), daemon=True).start()
|
||||||
|
_serve_tcp(tcp)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg iproute2 iptables \
|
||||||
|
&& mkdir -p /usr/share/keyrings \
|
||||||
|
&& curl -fsSL https://deb.session.foundation/pub.gpg -o /usr/share/keyrings/session-foundation.gpg \
|
||||||
|
&& printf 'Types: deb\nURIs: https://deb.session.foundation\nSuites: bookworm\nComponents: main\nSigned-By: /usr/share/keyrings/session-foundation.gpg\n' > /etc/apt/sources.list.d/session.sources \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends lokinet-bin \
|
||||||
|
&& mkdir -p /etc/lokinet /var/lib/lokinet \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY lokinet.ini /etc/lokinet/lokinet.ini
|
||||||
|
|
||||||
|
CMD ["/usr/bin/lokinet", "--config", "/etc/lokinet/lokinet.ini"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[router]
|
||||||
|
data-dir=/var/lib/lokinet
|
||||||
|
|
||||||
|
[network]
|
||||||
|
auto-routing=false
|
||||||
|
blackhole-routes=false
|
||||||
|
ifname=lokinet0
|
||||||
|
|
||||||
|
[dns]
|
||||||
|
bind=0.0.0.0:53
|
||||||
|
upstream=10.5.0.5:53
|
||||||
|
no-resolvconf=1
|
||||||
|
|
||||||
|
[bootstrap]
|
||||||
|
add-node=/etc/lokinet/bootstrap.signed
|
||||||
|
|
||||||
|
[logging]
|
||||||
|
type=print
|
||||||
|
level=info
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"title": "3proxy overview",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"version": 1,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"type": "timeseries",
|
||||||
|
"title": "Active proxy connections",
|
||||||
|
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 },
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "proxy_proxy_conns",
|
||||||
|
"legendFormat": "connections",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY exporter.py /app/exporter.py
|
||||||
|
|
||||||
|
ENV EXPORTER_PORT=9100 \
|
||||||
|
MONITOR_HOST=dark3proxy \
|
||||||
|
MONITOR_PORT=6800 \
|
||||||
|
SOCKET_TIMEOUT=2
|
||||||
|
|
||||||
|
EXPOSE 9100
|
||||||
|
CMD ["python", "/app/exporter.py"]
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
|
||||||
|
MONITOR_HOST = os.getenv("MONITOR_HOST", "dark3proxy")
|
||||||
|
MONITOR_PORT = int(os.getenv("MONITOR_PORT", "6800"))
|
||||||
|
SOCKET_TIMEOUT = float(os.getenv("SOCKET_TIMEOUT", "2"))
|
||||||
|
EXPORTER_PORT = int(os.getenv("EXPORTER_PORT", "9100"))
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_monitor_data() -> str:
|
||||||
|
with socket.create_connection((MONITOR_HOST, MONITOR_PORT), timeout=SOCKET_TIMEOUT) as sock:
|
||||||
|
sock.settimeout(SOCKET_TIMEOUT)
|
||||||
|
chunks = []
|
||||||
|
while True:
|
||||||
|
data = sock.recv(4096)
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
chunks.append(data)
|
||||||
|
return b"".join(chunks).decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def metric_name(raw_parts: list[str]) -> str:
|
||||||
|
name = "_".join(raw_parts).lower()
|
||||||
|
name = re.sub(r"[^a-z0-9_]", "_", name)
|
||||||
|
name = re.sub(r"_+", "_", name).strip("_")
|
||||||
|
if not name or not name[0].isalpha():
|
||||||
|
name = f"m_{name}" if name else "m_value"
|
||||||
|
return f"proxy_{name}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_metrics(text: str) -> dict[str, float]:
|
||||||
|
metrics: dict[str, float] = {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
parts = line.strip().split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
value_token = parts[-1].replace(",", ".")
|
||||||
|
try:
|
||||||
|
value = float(value_token)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
name = metric_name(parts[:-1])
|
||||||
|
metrics[name] = value
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def render_metrics() -> bytes:
|
||||||
|
body_lines = []
|
||||||
|
try:
|
||||||
|
raw = fetch_monitor_data()
|
||||||
|
metrics = parse_metrics(raw)
|
||||||
|
body_lines.append("proxy_exporter_up 1")
|
||||||
|
for name in sorted(metrics):
|
||||||
|
body_lines.append(f"{name} {metrics[name]}")
|
||||||
|
except Exception:
|
||||||
|
body_lines.append("proxy_exporter_up 0")
|
||||||
|
|
||||||
|
body = "\n".join(body_lines) + "\n"
|
||||||
|
return body.encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path not in ("/metrics", "/"):
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = render_metrics()
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(payload)))
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(payload)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = HTTPServer(("0.0.0.0", EXPORTER_PORT), Handler)
|
||||||
|
server.serve_forever()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
global:
|
||||||
|
scrape_interval: 5s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 3proxy_exporter
|
||||||
|
static_configs:
|
||||||
|
- targets:
|
||||||
|
- dark3proxy-exporter:9100
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.12-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY status_dashboard.py /app/status_dashboard.py
|
||||||
|
|
||||||
|
ENV DASHBOARD_PORT=8080 \
|
||||||
|
DOCKER_PROJECT=darkproxy \
|
||||||
|
DOCKER_SOCKET=/var/run/docker.sock \
|
||||||
|
DNS_SERVER=darkpihole \
|
||||||
|
DNS_PORT=53 \
|
||||||
|
PROXY_HOST=dark3proxy \
|
||||||
|
PROXY_PORT=1080 \
|
||||||
|
PROXY_USERS_FILE=/run/secrets/PROXY_USERS \
|
||||||
|
STATUS_CACHE_SECONDS=15 \
|
||||||
|
SOCKET_TIMEOUT=5 \
|
||||||
|
LOG_TAIL_LINES=120
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["python", "/app/status_dashboard.py"]
|
||||||
@@ -0,0 +1,880 @@
|
|||||||
|
import http.client
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
|
||||||
|
DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "8080"))
|
||||||
|
DOCKER_SOCKET = os.getenv("DOCKER_SOCKET", "/var/run/docker.sock")
|
||||||
|
DOCKER_PROJECT = os.getenv("DOCKER_PROJECT", "darkproxy")
|
||||||
|
DNS_SERVER = os.getenv("DNS_SERVER", "darkpihole")
|
||||||
|
DNS_PORT = int(os.getenv("DNS_PORT", "53"))
|
||||||
|
PROXY_HOST = os.getenv("PROXY_HOST", "dark3proxy")
|
||||||
|
PROXY_PORT = int(os.getenv("PROXY_PORT", "1080"))
|
||||||
|
PROXY_USERS_FILE = os.getenv("PROXY_USERS_FILE", "/run/secrets/PROXY_USERS")
|
||||||
|
SOCKET_TIMEOUT = float(os.getenv("SOCKET_TIMEOUT", "5"))
|
||||||
|
STATUS_CACHE_SECONDS = int(os.getenv("STATUS_CACHE_SECONDS", "15"))
|
||||||
|
LOG_TAIL_LINES = int(os.getenv("LOG_TAIL_LINES", "120"))
|
||||||
|
|
||||||
|
ERROR_RE = re.compile(r"error|failed|not found|panic|traceback|unhealthy|refused|parse", re.IGNORECASE)
|
||||||
|
IGNORE_RE = {
|
||||||
|
"darkscale": re.compile(r"tpmrm0|Tailscale is stopped", re.IGNORECASE),
|
||||||
|
"darkpihole": re.compile(r"refused to do a recursive query", re.IGNORECASE),
|
||||||
|
"darki2p": re.compile(
|
||||||
|
r"SessionCreated read error: End of file|"
|
||||||
|
r"Connect error Operation canceled|"
|
||||||
|
r"Connect error Network is unreachable|"
|
||||||
|
r"RouterInfo for .* not found|"
|
||||||
|
r"RouterInfo not found, failed to send messages|"
|
||||||
|
r"NetDbReq: .* not found after 5 attempts",
|
||||||
|
re.IGNORECASE,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
RCODE_NAMES = {
|
||||||
|
0: "NOERROR",
|
||||||
|
1: "FORMERR",
|
||||||
|
2: "SERVFAIL",
|
||||||
|
3: "NXDOMAIN",
|
||||||
|
4: "NOTIMP",
|
||||||
|
5: "REFUSED",
|
||||||
|
}
|
||||||
|
DNS_CHECKS = [
|
||||||
|
{
|
||||||
|
"name": "google.com",
|
||||||
|
"record_type": "A",
|
||||||
|
"severity": "critical",
|
||||||
|
"label": "Public DNS path",
|
||||||
|
"note": "Pi-hole -> CoreDNS -> Unbound",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "facebookcorewwwi.onion",
|
||||||
|
"record_type": "A",
|
||||||
|
"severity": "critical",
|
||||||
|
"label": ".onion resolution",
|
||||||
|
"note": "Tor DNS path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stats.i2p",
|
||||||
|
"record_type": "A",
|
||||||
|
"severity": "critical",
|
||||||
|
"label": ".i2p resolution",
|
||||||
|
"note": "i2pdns bridge path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vitalik.eth",
|
||||||
|
"record_type": "TXT",
|
||||||
|
"severity": "warning",
|
||||||
|
"label": ".eth resolution",
|
||||||
|
"note": "ENS resolver path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "brad.zil",
|
||||||
|
"record_type": "TXT",
|
||||||
|
"severity": "warning",
|
||||||
|
"label": ".zil resolution",
|
||||||
|
"note": "Zilliqa resolver path",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "d.bit",
|
||||||
|
"record_type": "TXT",
|
||||||
|
"severity": "info",
|
||||||
|
"label": ".bit resolution",
|
||||||
|
"note": "Sync-dependent Namecoin path",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
STATUS_CACHE = {"expires_at": 0.0, "payload": None}
|
||||||
|
CACHE_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
class UnixSocketHTTPConnection(http.client.HTTPConnection):
|
||||||
|
def __init__(self, socket_path: str, timeout: float):
|
||||||
|
super().__init__("localhost", timeout=timeout)
|
||||||
|
self.socket_path = socket_path
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
|
self.sock.settimeout(self.timeout)
|
||||||
|
self.sock.connect(self.socket_path)
|
||||||
|
|
||||||
|
|
||||||
|
def now_iso() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def docker_request(path: str) -> tuple[int, bytes]:
|
||||||
|
conn = UnixSocketHTTPConnection(DOCKER_SOCKET, timeout=SOCKET_TIMEOUT)
|
||||||
|
try:
|
||||||
|
conn.request("GET", path)
|
||||||
|
response = conn.getresponse()
|
||||||
|
return response.status, response.read()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def docker_json(path: str):
|
||||||
|
status, payload = docker_request(path)
|
||||||
|
if status >= 400:
|
||||||
|
raise RuntimeError(f"Docker API request failed for {path}: HTTP {status}")
|
||||||
|
return json.loads(payload.decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def docker_bytes(path: str) -> bytes:
|
||||||
|
status, payload = docker_request(path)
|
||||||
|
if status >= 400:
|
||||||
|
raise RuntimeError(f"Docker API request failed for {path}: HTTP {status}")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def decode_docker_stream(payload: bytes) -> str:
|
||||||
|
# Docker may frame stdout/stderr for non-TTY containers using an 8-byte header.
|
||||||
|
chunks = []
|
||||||
|
offset = 0
|
||||||
|
while offset + 8 <= len(payload):
|
||||||
|
if payload[offset + 1 : offset + 4] != b"\x00\x00\x00":
|
||||||
|
return payload.decode("utf-8", errors="replace")
|
||||||
|
frame_len = struct.unpack(">I", payload[offset + 4 : offset + 8])[0]
|
||||||
|
frame_start = offset + 8
|
||||||
|
frame_end = frame_start + frame_len
|
||||||
|
if frame_end > len(payload):
|
||||||
|
return payload.decode("utf-8", errors="replace")
|
||||||
|
chunks.append(payload[frame_start:frame_end])
|
||||||
|
offset = frame_end
|
||||||
|
if offset == len(payload):
|
||||||
|
return b"".join(chunks).decode("utf-8", errors="replace")
|
||||||
|
return payload.decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def service_log_text(container_id: str, tail: int) -> str:
|
||||||
|
query = urllib.parse.urlencode({"stdout": 1, "stderr": 1, "tail": max(1, min(tail, 500))})
|
||||||
|
payload = docker_bytes(f"/containers/{container_id}/logs?{query}")
|
||||||
|
return decode_docker_stream(payload).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_error_excerpt(name: str, log_text: str) -> list[str]:
|
||||||
|
ignore = IGNORE_RE.get(name)
|
||||||
|
matches = []
|
||||||
|
for line in log_text.splitlines():
|
||||||
|
if not ERROR_RE.search(line):
|
||||||
|
continue
|
||||||
|
if ignore and ignore.search(line):
|
||||||
|
continue
|
||||||
|
matches.append(line)
|
||||||
|
return matches[-6:]
|
||||||
|
|
||||||
|
|
||||||
|
def read_proxy_users() -> list[dict[str, str]]:
|
||||||
|
users = []
|
||||||
|
try:
|
||||||
|
with open(PROXY_USERS_FILE, "r", encoding="utf-8") as handle:
|
||||||
|
for raw_line in handle:
|
||||||
|
line = raw_line.strip()
|
||||||
|
if not line or line.startswith("#") or ":" not in line:
|
||||||
|
continue
|
||||||
|
username, password = line.split(":", 1)
|
||||||
|
if username:
|
||||||
|
users.append({"username": username, "password": password})
|
||||||
|
except FileNotFoundError:
|
||||||
|
return []
|
||||||
|
return users
|
||||||
|
|
||||||
|
|
||||||
|
def encode_dns_name(name: str) -> bytes:
|
||||||
|
parts = name.rstrip(".").split(".")
|
||||||
|
encoded = bytearray()
|
||||||
|
for part in parts:
|
||||||
|
label = part.encode("idna")
|
||||||
|
encoded.append(len(label))
|
||||||
|
encoded.extend(label)
|
||||||
|
encoded.append(0)
|
||||||
|
return bytes(encoded)
|
||||||
|
|
||||||
|
|
||||||
|
def read_dns_name(payload: bytes, offset: int) -> tuple[str, int]:
|
||||||
|
labels = []
|
||||||
|
jumped = False
|
||||||
|
next_offset = offset
|
||||||
|
while True:
|
||||||
|
length = payload[offset]
|
||||||
|
if length == 0:
|
||||||
|
offset += 1
|
||||||
|
if not jumped:
|
||||||
|
next_offset = offset
|
||||||
|
break
|
||||||
|
if length & 0xC0 == 0xC0:
|
||||||
|
pointer = struct.unpack(">H", payload[offset : offset + 2])[0] & 0x3FFF
|
||||||
|
offset = pointer
|
||||||
|
if not jumped:
|
||||||
|
next_offset += 2
|
||||||
|
jumped = True
|
||||||
|
continue
|
||||||
|
offset += 1
|
||||||
|
labels.append(payload[offset : offset + length].decode("utf-8", errors="replace"))
|
||||||
|
offset += length
|
||||||
|
if not jumped:
|
||||||
|
next_offset = offset
|
||||||
|
return ".".join(labels), next_offset
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dns_answers(payload: bytes, qtype: int) -> tuple[str, list[str]]:
|
||||||
|
_, _, _, answer_count, _, _ = struct.unpack(">HHHHHH", payload[:12])
|
||||||
|
offset = 12
|
||||||
|
for _ in range(1):
|
||||||
|
_, offset = read_dns_name(payload, offset)
|
||||||
|
offset += 4
|
||||||
|
answers = []
|
||||||
|
for _ in range(answer_count):
|
||||||
|
_, offset = read_dns_name(payload, offset)
|
||||||
|
record_type, _, _, rdlength = struct.unpack(">HHIH", payload[offset : offset + 10])
|
||||||
|
offset += 10
|
||||||
|
rdata = payload[offset : offset + rdlength]
|
||||||
|
offset += rdlength
|
||||||
|
if record_type == 1 and qtype == 1 and rdlength == 4:
|
||||||
|
answers.append(socket.inet_ntoa(rdata))
|
||||||
|
elif record_type == 16 and qtype == 16 and rdlength > 0:
|
||||||
|
strings = []
|
||||||
|
inner_offset = 0
|
||||||
|
while inner_offset < len(rdata):
|
||||||
|
chunk_len = rdata[inner_offset]
|
||||||
|
inner_offset += 1
|
||||||
|
strings.append(rdata[inner_offset : inner_offset + chunk_len].decode("utf-8", errors="replace"))
|
||||||
|
inner_offset += chunk_len
|
||||||
|
answers.append("".join(strings))
|
||||||
|
rcode = payload[3] & 0x0F
|
||||||
|
return RCODE_NAMES.get(rcode, f"RCODE_{rcode}"), answers
|
||||||
|
|
||||||
|
|
||||||
|
def dns_query(name: str, record_type: str) -> dict[str, object]:
|
||||||
|
qtype = 1 if record_type == "A" else 16
|
||||||
|
query_id = random.randint(0, 65535)
|
||||||
|
flags = 0x0100
|
||||||
|
header = struct.pack(">HHHHHH", query_id, flags, 1, 0, 0, 0)
|
||||||
|
question = encode_dns_name(name) + struct.pack(">HH", qtype, 1)
|
||||||
|
message = header + question
|
||||||
|
start = time.monotonic()
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||||
|
sock.settimeout(SOCKET_TIMEOUT)
|
||||||
|
sock.sendto(message, (DNS_SERVER, DNS_PORT))
|
||||||
|
payload, _ = sock.recvfrom(4096)
|
||||||
|
duration_ms = int((time.monotonic() - start) * 1000)
|
||||||
|
status, answers = parse_dns_answers(payload, qtype)
|
||||||
|
return {"status": status, "answers": answers, "latency_ms": duration_ms}
|
||||||
|
|
||||||
|
|
||||||
|
def run_dns_check(spec: dict[str, str]) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
result = dns_query(spec["name"], spec["record_type"])
|
||||||
|
outcome = "ok" if result["status"] == "NOERROR" and result["answers"] else "failed"
|
||||||
|
if spec["severity"] == "info" and result["status"] in {"NOERROR", "NXDOMAIN"}:
|
||||||
|
outcome = "ok"
|
||||||
|
detail = result["status"]
|
||||||
|
if result["answers"]:
|
||||||
|
detail = f"{result['status']} ({', '.join(result['answers'][:2])})"
|
||||||
|
return {
|
||||||
|
"kind": "dns",
|
||||||
|
"label": spec["label"],
|
||||||
|
"target": spec["name"],
|
||||||
|
"record_type": spec["record_type"],
|
||||||
|
"severity": spec["severity"],
|
||||||
|
"outcome": outcome,
|
||||||
|
"detail": detail,
|
||||||
|
"note": spec["note"],
|
||||||
|
"latency_ms": result["latency_ms"],
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"kind": "dns",
|
||||||
|
"label": spec["label"],
|
||||||
|
"target": spec["name"],
|
||||||
|
"record_type": spec["record_type"],
|
||||||
|
"severity": spec["severity"],
|
||||||
|
"outcome": "failed",
|
||||||
|
"detail": str(exc),
|
||||||
|
"note": spec["note"],
|
||||||
|
"latency_ms": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def recv_exact(sock: socket.socket, size: int) -> bytes:
|
||||||
|
chunks = []
|
||||||
|
remaining = size
|
||||||
|
while remaining > 0:
|
||||||
|
data = sock.recv(remaining)
|
||||||
|
if not data:
|
||||||
|
raise RuntimeError("unexpected EOF")
|
||||||
|
chunks.append(data)
|
||||||
|
remaining -= len(data)
|
||||||
|
return b"".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def socks_reply_message(code: int) -> str:
|
||||||
|
return {
|
||||||
|
0x00: "succeeded",
|
||||||
|
0x01: "general failure",
|
||||||
|
0x02: "connection not allowed",
|
||||||
|
0x03: "network unreachable",
|
||||||
|
0x04: "host unreachable",
|
||||||
|
0x05: "connection refused",
|
||||||
|
0x06: "TTL expired",
|
||||||
|
0x07: "command not supported",
|
||||||
|
0x08: "address type not supported",
|
||||||
|
}.get(code, f"reply {code}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_proxy_check(users: list[dict[str, str]]) -> dict[str, object]:
|
||||||
|
if not users:
|
||||||
|
return {
|
||||||
|
"kind": "proxy",
|
||||||
|
"label": "SOCKS ingress",
|
||||||
|
"target": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
|
"severity": "warning",
|
||||||
|
"outcome": "skipped",
|
||||||
|
"detail": "No proxy users found in secret",
|
||||||
|
"note": "Shows auth + CONNECT readiness",
|
||||||
|
"latency_ms": -1,
|
||||||
|
}
|
||||||
|
user = users[0]
|
||||||
|
host = "example.com".encode("idna")
|
||||||
|
start = time.monotonic()
|
||||||
|
try:
|
||||||
|
with socket.create_connection((PROXY_HOST, PROXY_PORT), timeout=SOCKET_TIMEOUT) as sock:
|
||||||
|
sock.settimeout(SOCKET_TIMEOUT)
|
||||||
|
sock.sendall(b"\x05\x01\x02")
|
||||||
|
greeting = recv_exact(sock, 2)
|
||||||
|
if greeting != b"\x05\x02":
|
||||||
|
raise RuntimeError(f"unexpected auth method {greeting!r}")
|
||||||
|
|
||||||
|
username = user["username"].encode("utf-8")
|
||||||
|
password = user["password"].encode("utf-8")
|
||||||
|
sock.sendall(bytes([0x01, len(username)]) + username + bytes([len(password)]) + password)
|
||||||
|
auth_reply = recv_exact(sock, 2)
|
||||||
|
if auth_reply[1] != 0x00:
|
||||||
|
raise RuntimeError("authentication failed")
|
||||||
|
|
||||||
|
request = b"\x05\x01\x00\x03" + bytes([len(host)]) + host + struct.pack(">H", 80)
|
||||||
|
sock.sendall(request)
|
||||||
|
header = recv_exact(sock, 4)
|
||||||
|
reply_code = header[1]
|
||||||
|
addr_type = header[3]
|
||||||
|
if addr_type == 0x01:
|
||||||
|
recv_exact(sock, 4)
|
||||||
|
elif addr_type == 0x03:
|
||||||
|
domain_len = recv_exact(sock, 1)[0]
|
||||||
|
recv_exact(sock, domain_len)
|
||||||
|
elif addr_type == 0x04:
|
||||||
|
recv_exact(sock, 16)
|
||||||
|
recv_exact(sock, 2)
|
||||||
|
if reply_code != 0x00:
|
||||||
|
raise RuntimeError(socks_reply_message(reply_code))
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"kind": "proxy",
|
||||||
|
"label": "SOCKS ingress",
|
||||||
|
"target": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
|
"severity": "critical",
|
||||||
|
"outcome": "failed",
|
||||||
|
"detail": str(exc),
|
||||||
|
"note": "Shows auth + CONNECT readiness",
|
||||||
|
"latency_ms": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kind": "proxy",
|
||||||
|
"label": "SOCKS ingress",
|
||||||
|
"target": f"{PROXY_HOST}:{PROXY_PORT}",
|
||||||
|
"severity": "critical",
|
||||||
|
"outcome": "ok",
|
||||||
|
"detail": f"Authenticated as {user['username']} and opened CONNECT tunnel",
|
||||||
|
"note": "Shows auth + CONNECT readiness",
|
||||||
|
"latency_ms": int((time.monotonic() - start) * 1000),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def service_severity(state: str, health: str, restart_count: int, error_excerpt: list[str]) -> str:
|
||||||
|
if state in {"exited", "dead"} or health == "unhealthy":
|
||||||
|
return "critical"
|
||||||
|
if state != "running" or health == "starting" or restart_count > 0 or error_excerpt:
|
||||||
|
return "degraded"
|
||||||
|
return "healthy"
|
||||||
|
|
||||||
|
|
||||||
|
def list_project_containers() -> list[dict[str, object]]:
|
||||||
|
filters = urllib.parse.quote(json.dumps({"label": [f"com.docker.compose.project={DOCKER_PROJECT}"]}, separators=(",", ":")))
|
||||||
|
raw_containers = docker_json(f"/containers/json?all=1&filters={filters}")
|
||||||
|
services = []
|
||||||
|
for item in raw_containers:
|
||||||
|
name = item.get("Names", [item["Id"]])[0].lstrip("/")
|
||||||
|
inspect = docker_json(f"/containers/{item['Id']}/json")
|
||||||
|
state = inspect["State"]["Status"]
|
||||||
|
health = inspect["State"].get("Health", {}).get("Status", "none")
|
||||||
|
restart_count = inspect.get("RestartCount", 0)
|
||||||
|
logs = service_log_text(item["Id"], LOG_TAIL_LINES)
|
||||||
|
error_excerpt = extract_error_excerpt(name, logs)
|
||||||
|
services.append(
|
||||||
|
{
|
||||||
|
"id": item["Id"],
|
||||||
|
"service": item.get("Labels", {}).get("com.docker.compose.service", name),
|
||||||
|
"container_name": name,
|
||||||
|
"state": state,
|
||||||
|
"status_text": item.get("Status", state),
|
||||||
|
"health": health,
|
||||||
|
"restart_count": restart_count,
|
||||||
|
"image": item.get("Image", ""),
|
||||||
|
"ports": item.get("Ports", []),
|
||||||
|
"severity": service_severity(state, health, restart_count, error_excerpt),
|
||||||
|
"error_excerpt": error_excerpt,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sorted(services, key=lambda svc: svc["service"])
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_status(services: list[dict[str, object]], checks: list[dict[str, object]]) -> dict[str, object]:
|
||||||
|
critical_services = sum(1 for svc in services if svc["severity"] == "critical")
|
||||||
|
degraded_services = sum(1 for svc in services if svc["severity"] == "degraded")
|
||||||
|
failed_critical_checks = sum(1 for check in checks if check["severity"] == "critical" and check["outcome"] != "ok")
|
||||||
|
failed_warning_checks = sum(1 for check in checks if check["severity"] == "warning" and check["outcome"] != "ok")
|
||||||
|
|
||||||
|
if critical_services or failed_critical_checks:
|
||||||
|
overall = "critical"
|
||||||
|
elif degraded_services or failed_warning_checks:
|
||||||
|
overall = "degraded"
|
||||||
|
else:
|
||||||
|
overall = "healthy"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"overall": overall,
|
||||||
|
"service_counts": {
|
||||||
|
"total": len(services),
|
||||||
|
"healthy": sum(1 for svc in services if svc["severity"] == "healthy"),
|
||||||
|
"degraded": degraded_services,
|
||||||
|
"critical": critical_services,
|
||||||
|
},
|
||||||
|
"check_counts": {
|
||||||
|
"total": len(checks),
|
||||||
|
"ok": sum(1 for check in checks if check["outcome"] == "ok"),
|
||||||
|
"failed": sum(1 for check in checks if check["outcome"] == "failed"),
|
||||||
|
"skipped": sum(1 for check in checks if check["outcome"] == "skipped"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_status_payload() -> dict[str, object]:
|
||||||
|
services = list_project_containers()
|
||||||
|
users = read_proxy_users()
|
||||||
|
checks = [run_dns_check(spec) for spec in DNS_CHECKS]
|
||||||
|
checks.append(run_proxy_check(users))
|
||||||
|
summary = summarize_status(services, checks)
|
||||||
|
return {
|
||||||
|
"generated_at": now_iso(),
|
||||||
|
"project": DOCKER_PROJECT,
|
||||||
|
"summary": summary,
|
||||||
|
"services": services,
|
||||||
|
"checks": checks,
|
||||||
|
"users": [{"username": entry["username"]} for entry in users],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_status_payload(force_refresh: bool = False) -> dict[str, object]:
|
||||||
|
now = time.monotonic()
|
||||||
|
with CACHE_LOCK:
|
||||||
|
if not force_refresh and STATUS_CACHE["payload"] is not None and now < STATUS_CACHE["expires_at"]:
|
||||||
|
return STATUS_CACHE["payload"]
|
||||||
|
payload = build_status_payload()
|
||||||
|
STATUS_CACHE["payload"] = payload
|
||||||
|
STATUS_CACHE["expires_at"] = now + STATUS_CACHE_SECONDS
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def find_service(identifier: str) -> dict[str, object] | None:
|
||||||
|
payload = get_status_payload()
|
||||||
|
for service in payload["services"]:
|
||||||
|
if service["service"] == identifier or service["container_name"] == identifier:
|
||||||
|
return service
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
HTML = """<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>darkproxy status</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0f172a;
|
||||||
|
--panel: #111827;
|
||||||
|
--panel-2: #1f2937;
|
||||||
|
--text: #e5e7eb;
|
||||||
|
--muted: #94a3b8;
|
||||||
|
--healthy: #22c55e;
|
||||||
|
--degraded: #f59e0b;
|
||||||
|
--critical: #ef4444;
|
||||||
|
--border: #334155;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.topbar, .cards, .panel-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.card, .panel {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.panel-grid {
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.healthy { color: var(--healthy); }
|
||||||
|
.degraded { color: var(--degraded); }
|
||||||
|
.critical { color: var(--critical); }
|
||||||
|
.badge.healthy { background: rgba(34, 197, 94, 0.15); }
|
||||||
|
.badge.degraded { background: rgba(245, 158, 11, 0.15); }
|
||||||
|
.badge.critical { background: rgba(239, 68, 68, 0.15); }
|
||||||
|
.badge.info { background: rgba(148, 163, 184, 0.15); color: var(--muted); }
|
||||||
|
.muted {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
button, select {
|
||||||
|
background: var(--panel-2);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #020617;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 220px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
.error-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.panel-grid, .topbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="topbar">
|
||||||
|
<div>
|
||||||
|
<h1>darkproxy status</h1>
|
||||||
|
<div class="muted" id="updated-at">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<button id="refresh-btn">Refresh now</button>
|
||||||
|
<a class="muted" href="/api/status" target="_blank" rel="noreferrer">JSON</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards" id="summary-cards"></div>
|
||||||
|
|
||||||
|
<div class="panel-grid">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Services</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>State</th>
|
||||||
|
<th>Health</th>
|
||||||
|
<th>Restarts</th>
|
||||||
|
<th>Errors</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="services-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<div class="muted">Configured proxy usernames only. Passwords are never shown.</div>
|
||||||
|
<ul id="users-list"></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" style="margin-bottom: 16px;">
|
||||||
|
<h2>Checks</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Check</th>
|
||||||
|
<th>Target</th>
|
||||||
|
<th>Severity</th>
|
||||||
|
<th>Result</th>
|
||||||
|
<th>Latency</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="checks-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div style="display:flex; gap:12px; align-items:center; margin-bottom:12px; flex-wrap:wrap;">
|
||||||
|
<h2 style="margin:0;">Logs</h2>
|
||||||
|
<select id="logs-service"></select>
|
||||||
|
<button id="load-logs-btn">Load logs</button>
|
||||||
|
</div>
|
||||||
|
<pre id="logs-output">Select a service to view recent logs.</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let lastStatus = null;
|
||||||
|
|
||||||
|
function badgeClass(value) {
|
||||||
|
return ["healthy", "degraded", "critical"].includes(value) ? value : "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(value) {
|
||||||
|
return String(value ?? "").replace(/[&<>"]/g, (ch) => ({
|
||||||
|
"&": "&",
|
||||||
|
"<": "<",
|
||||||
|
">": ">",
|
||||||
|
'"': """
|
||||||
|
}[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(summary) {
|
||||||
|
const cards = [
|
||||||
|
["Overall", summary.overall],
|
||||||
|
["Services", `${summary.service_counts.healthy}/${summary.service_counts.total} healthy`],
|
||||||
|
["Degraded", `${summary.service_counts.degraded}`],
|
||||||
|
["Critical", `${summary.service_counts.critical}`],
|
||||||
|
["Checks OK", `${summary.check_counts.ok}/${summary.check_counts.total}`]
|
||||||
|
];
|
||||||
|
document.getElementById("summary-cards").innerHTML = cards.map(([label, value]) => `
|
||||||
|
<div class="card">
|
||||||
|
<div class="muted">${esc(label)}</div>
|
||||||
|
<div class="badge ${badgeClass(String(value).toLowerCase())}">${esc(value)}</div>
|
||||||
|
</div>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderServices(services) {
|
||||||
|
document.getElementById("services-body").innerHTML = services.map((service) => `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>${esc(service.service)}</strong><br>
|
||||||
|
<span class="muted">${esc(service.container_name)}</span>
|
||||||
|
</td>
|
||||||
|
<td><span class="badge ${badgeClass(service.severity)}">${esc(service.state)}</span><br><span class="muted">${esc(service.status_text)}</span></td>
|
||||||
|
<td>${esc(service.health)}</td>
|
||||||
|
<td>${esc(service.restart_count)}</td>
|
||||||
|
<td>
|
||||||
|
${service.error_excerpt.length ? `<ul class="error-list">${service.error_excerpt.map((line) => `<li>${esc(line)}</li>`).join("")}</ul>` : '<span class="muted">none</span>'}
|
||||||
|
</td>
|
||||||
|
<td><button data-service="${esc(service.service)}">Logs</button></td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
|
||||||
|
const select = document.getElementById("logs-service");
|
||||||
|
const current = select.value;
|
||||||
|
select.innerHTML = services.map((service) => `<option value="${esc(service.service)}">${esc(service.service)}</option>`).join("");
|
||||||
|
if (services.some((service) => service.service === current)) {
|
||||||
|
select.value = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("button[data-service]").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
select.value = button.dataset.service;
|
||||||
|
loadLogs();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChecks(checks) {
|
||||||
|
document.getElementById("checks-body").innerHTML = checks.map((check) => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${esc(check.label)}</strong><br><span class="muted">${esc(check.note)}</span></td>
|
||||||
|
<td>${esc(check.target)} ${check.record_type ? `<span class="muted">${esc(check.record_type)}</span>` : ""}</td>
|
||||||
|
<td><span class="badge ${badgeClass(check.severity === "warning" ? "degraded" : check.severity)}">${esc(check.severity)}</span></td>
|
||||||
|
<td><span class="badge ${badgeClass(check.outcome === "ok" ? "healthy" : (check.outcome === "skipped" ? "info" : "critical"))}">${esc(check.outcome)}</span></td>
|
||||||
|
<td>${check.latency_ms >= 0 ? `${esc(check.latency_ms)} ms` : '<span class="muted">n/a</span>'}</td>
|
||||||
|
<td>${esc(check.detail)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUsers(users) {
|
||||||
|
const list = document.getElementById("users-list");
|
||||||
|
if (!users.length) {
|
||||||
|
list.innerHTML = "<li class='muted'>No users found</li>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = users.map((user) => `<li>${esc(user.username)}</li>`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStatus(force = false) {
|
||||||
|
const suffix = force ? "?refresh=1" : "";
|
||||||
|
const response = await fetch(`/api/status${suffix}`);
|
||||||
|
const payload = await response.json();
|
||||||
|
lastStatus = payload;
|
||||||
|
document.getElementById("updated-at").textContent = `Updated ${payload.generated_at}`;
|
||||||
|
renderSummary(payload.summary);
|
||||||
|
renderServices(payload.services);
|
||||||
|
renderChecks(payload.checks);
|
||||||
|
renderUsers(payload.users);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadLogs() {
|
||||||
|
const service = document.getElementById("logs-service").value;
|
||||||
|
if (!service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.getElementById("logs-output").textContent = "Loading logs…";
|
||||||
|
const response = await fetch(`/api/logs?service=${encodeURIComponent(service)}`);
|
||||||
|
const payload = await response.json();
|
||||||
|
document.getElementById("logs-output").textContent = payload.logs || "(no log output)";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("refresh-btn").addEventListener("click", () => loadStatus(true));
|
||||||
|
document.getElementById("load-logs-btn").addEventListener("click", loadLogs);
|
||||||
|
|
||||||
|
loadStatus().catch((error) => {
|
||||||
|
document.getElementById("updated-at").textContent = `Failed to load status: ${error}`;
|
||||||
|
});
|
||||||
|
setInterval(() => loadStatus().catch(() => {}), 15000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
def send_json(self, payload: dict[str, object], status: int = 200):
|
||||||
|
body = json.dumps(payload, indent=2).encode("utf-8")
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
try:
|
||||||
|
self.wfile.write(body)
|
||||||
|
except BrokenPipeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
def send_html(self, body: str, status: int = 200):
|
||||||
|
payload = body.encode("utf-8")
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||||
|
self.send_header("Content-Length", str(len(payload)))
|
||||||
|
self.end_headers()
|
||||||
|
try:
|
||||||
|
self.wfile.write(payload)
|
||||||
|
except BrokenPipeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
params = urllib.parse.parse_qs(parsed.query)
|
||||||
|
try:
|
||||||
|
if parsed.path == "/":
|
||||||
|
self.send_html(HTML)
|
||||||
|
return
|
||||||
|
if parsed.path == "/api/status":
|
||||||
|
force_refresh = params.get("refresh", ["0"])[0] == "1"
|
||||||
|
self.send_json(get_status_payload(force_refresh=force_refresh))
|
||||||
|
return
|
||||||
|
if parsed.path == "/healthz":
|
||||||
|
self.send_json({"ok": True, "generated_at": now_iso()})
|
||||||
|
return
|
||||||
|
if parsed.path == "/api/users":
|
||||||
|
users = [{"username": entry["username"]} for entry in read_proxy_users()]
|
||||||
|
self.send_json({"users": users})
|
||||||
|
return
|
||||||
|
if parsed.path == "/api/logs":
|
||||||
|
service_name = params.get("service", [""])[0]
|
||||||
|
if not service_name:
|
||||||
|
self.send_json({"error": "missing service parameter"}, status=400)
|
||||||
|
return
|
||||||
|
service = find_service(service_name)
|
||||||
|
if service is None:
|
||||||
|
self.send_json({"error": f"unknown service {service_name}"}, status=404)
|
||||||
|
return
|
||||||
|
tail = int(params.get("tail", ["200"])[0])
|
||||||
|
self.send_json(
|
||||||
|
{
|
||||||
|
"service": service["service"],
|
||||||
|
"container_name": service["container_name"],
|
||||||
|
"logs": service_log_text(service["id"], tail),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.send_json({"error": "not found"}, status=404)
|
||||||
|
except BrokenPipeError:
|
||||||
|
return
|
||||||
|
except Exception as exc:
|
||||||
|
self.send_json({"error": str(exc)}, status=500)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server = ThreadingHTTPServer(("0.0.0.0", DASHBOARD_PORT), Handler)
|
||||||
|
server.serve_forever()
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
COMPOSE=(docker compose -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.arm.yml")
|
||||||
|
|
||||||
|
services=(dark3proxy darkdns darkzil darkscale darkpihole darki2p)
|
||||||
|
dns_queries=(google.com torproject.org github.com wikipedia.org example.onion example.i2p)
|
||||||
|
|
||||||
|
echo "[smoke] stack status"
|
||||||
|
"${COMPOSE[@]}" ps
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[smoke] restart/health counters"
|
||||||
|
for c in "${services[@]}"; do
|
||||||
|
if [[ "$c" == "darkpihole" ]]; then
|
||||||
|
docker inspect -f "$c status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} restart={{.RestartCount}}" "$c"
|
||||||
|
elif [[ "$c" == "darki2p" ]]; then
|
||||||
|
docker inspect -f "$c status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} restart={{.RestartCount}}" "$c"
|
||||||
|
else
|
||||||
|
docker inspect -f "$c status={{.State.Status}} restart={{.RestartCount}}" "$c"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[smoke] dns timings via pihole"
|
||||||
|
for pass in 1 2; do
|
||||||
|
echo "-- pass $pass --"
|
||||||
|
for q in "${dns_queries[@]}"; do
|
||||||
|
out="$(docker exec darkpihole sh -lc "dig @127.0.0.1 +time=2 +tries=1 '$q' 2>&1" || true)"
|
||||||
|
status="$(printf "%s\n" "$out" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -n1)"
|
||||||
|
qtime="$(printf "%s\n" "$out" | sed -n 's/;; Query time: \([0-9]*\).*/\1/p' | head -n1)"
|
||||||
|
[[ -n "$status" ]] || status="NO_RESPONSE"
|
||||||
|
[[ -n "$qtime" ]] || qtime="-1"
|
||||||
|
printf '%s status=%s query_ms=%s\n' "$q" "$status" "$qtime"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[smoke] .zil path checks via coredns"
|
||||||
|
for q in brad.zil not-a-real-name-xyz12345.zil; do
|
||||||
|
out="$(docker exec darkpihole sh -lc "dig @10.5.0.4 +time=8 +tries=1 '$q' TXT 2>&1" || true)"
|
||||||
|
status="$(printf "%s\n" "$out" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -n1)"
|
||||||
|
qtime="$(printf "%s\n" "$out" | sed -n 's/;; Query time: \([0-9]*\).*/\1/p' | head -n1)"
|
||||||
|
answers="$(printf "%s\n" "$out" | awk '/^;; ANSWER SECTION:/{flag=1;next}/^;;/{if(flag)exit}flag' | wc -l | tr -d ' ')"
|
||||||
|
[[ -n "$status" ]] || status="NO_RESPONSE"
|
||||||
|
[[ -n "$qtime" ]] || qtime="-1"
|
||||||
|
[[ -n "$answers" ]] || answers="0"
|
||||||
|
printf '%s status=%s query_ms=%s answers=%s\n' "$q" "$status" "$qtime" "$answers"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[smoke] key error scan"
|
||||||
|
for c in "${services[@]}"; do
|
||||||
|
echo "=== $c ==="
|
||||||
|
case "$c" in
|
||||||
|
darkscale)
|
||||||
|
docker logs --tail 120 "$c" 2>&1 \
|
||||||
|
| grep -Ei "error|failed|not found|panic|traceback|unhealthy|refused|parse" \
|
||||||
|
| grep -Ev "tpmrm0|Tailscale is stopped" \
|
||||||
|
| tail -n 20 || echo "(no matching patterns)"
|
||||||
|
;;
|
||||||
|
darkpihole)
|
||||||
|
docker logs --tail 120 "$c" 2>&1 \
|
||||||
|
| grep -Ei "error|failed|not found|panic|traceback|unhealthy|refused|parse" \
|
||||||
|
| grep -Ev "refused to do a recursive query" \
|
||||||
|
| tail -n 20 || echo "(no matching patterns)"
|
||||||
|
;;
|
||||||
|
darki2p)
|
||||||
|
docker logs --tail 120 "$c" 2>&1 \
|
||||||
|
| grep -Ei "error|failed|not found|panic|traceback|unhealthy|refused|parse" \
|
||||||
|
| grep -Ev "SessionCreated read error: End of file|Connect error Operation canceled|Connect error Network is unreachable|RouterInfo for .* not found|RouterInfo not found, failed to send messages|NetDbReq: .* not found after 5 attempts" \
|
||||||
|
| tail -n 20 || echo "(no matching patterns)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
docker logs --tail 120 "$c" 2>&1 | grep -Ei "error|failed|not found|panic|traceback|unhealthy|refused|parse" | tail -n 20 || echo "(no matching patterns)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[smoke] done"
|
||||||
+128
-31
@@ -29,16 +29,52 @@ else
|
|||||||
echo "[validate-config] docker binary not found; skipping compose validation"
|
echo "[validate-config] docker binary not found; skipping compose validation"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CoreDNS syntax check (use local binary or container fallback)
|
# CoreDNS syntax check must use the compiled image because the Corefile relies
|
||||||
if command -v coredns >/dev/null 2>&1; then
|
# on custom plugins (meshname/meship) that are not present in the stock binary.
|
||||||
echo "[validate-config] checking PopuraDNS/Corefile with local coredns"
|
if command -v docker >/dev/null 2>&1; then
|
||||||
coredns -conf PopuraDNS/Corefile -dns.port=0
|
echo "[validate-config] checking PopuraDNS/Corefile with compiled coredns image"
|
||||||
elif command -v docker >/dev/null 2>&1; then
|
docker compose build coredns >/dev/null
|
||||||
echo "[validate-config] checking PopuraDNS/Corefile with container"
|
COREDNS_IMAGE="$(docker compose images -q coredns | head -n1)"
|
||||||
docker run --rm -v "${PWD}/PopuraDNS/Corefile:/etc/coredns/Corefile:ro" \
|
if [ -z "$COREDNS_IMAGE" ]; then
|
||||||
coredns/coredns:latest -conf /etc/coredns/Corefile -dns.port=0
|
echo "[validate-config] error: failed to resolve compiled coredns image" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
set +e
|
||||||
|
timeout -k 2s 8s docker run --rm "$COREDNS_IMAGE" -conf /Corefile -dns.port=0
|
||||||
|
COREDNS_STATUS=$?
|
||||||
|
set -e
|
||||||
|
if [ "$COREDNS_STATUS" -ne 0 ] && [ "$COREDNS_STATUS" -ne 124 ]; then
|
||||||
|
echo "[validate-config] error: compiled coredns failed to load PopuraDNS/Corefile" >&2
|
||||||
|
exit "$COREDNS_STATUS"
|
||||||
|
fi
|
||||||
|
elif command -v coredns >/dev/null 2>&1 && coredns -plugins 2>/dev/null | grep -q '^meshname$'; then
|
||||||
|
echo "[validate-config] checking PopuraDNS/Corefile with local compiled coredns"
|
||||||
|
set +e
|
||||||
|
timeout -k 2s 8s coredns -conf PopuraDNS/Corefile -dns.port=0
|
||||||
|
COREDNS_STATUS=$?
|
||||||
|
set -e
|
||||||
|
if [ "$COREDNS_STATUS" -ne 0 ] && [ "$COREDNS_STATUS" -ne 124 ]; then
|
||||||
|
echo "[validate-config] error: local compiled coredns failed to load PopuraDNS/Corefile" >&2
|
||||||
|
exit "$COREDNS_STATUS"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "[validate-config] cannot check Corefile syntax (no coredns or docker)"
|
echo "[validate-config] cannot check Corefile syntax (need docker or a compiled local coredns with meshname plugin)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# enforce portable defaults for host/LAN-specific settings in the base stack
|
||||||
|
if ! grep -q 'ROUTER_NO_AUTH_CIDRS: ${ROUTER_NO_AUTH_CIDRS:-}' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: ROUTER_NO_AUTH_CIDRS should default to empty and be overridden explicitly per deployment" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q '\${PIHOLE_WEB_BIND_HOST:-127.0.0.1}:\${PIHOLE_WEB_BIND_PORT:-2003}:80/tcp' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: Pi-hole web binding should default to 127.0.0.1 and be configurable via env vars" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q '\${STATUS_DASHBOARD_BIND_HOST:-127.0.0.1}:\${STATUS_DASHBOARD_BIND_PORT:-2004}:8080' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: status dashboard binding should default to 127.0.0.1 and be configurable via env vars" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ensure onion and i2p zones are present in Corefile
|
# ensure onion and i2p zones are present in Corefile
|
||||||
@@ -49,13 +85,18 @@ for zone in "onion.:53" "i2p.:53" "eth.:53" "bit.:53" "alt.:53" "loki.:53" "zil.
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# verify i2pd DNS server is enabled in its config
|
if ! grep -A6 '^\.:53 {' PopuraDNS/Corefile | grep -q 'acl {'; then
|
||||||
if grep -q "\[dns\]" i2pd_yggdrasil_docker/src/i2pd.conf; then
|
echo "[validate-config] error: Corefile catch-all zone must include an ACL to avoid open recursion" >&2
|
||||||
if ! awk '/\[dns\]/,/\[/{if($0~/enabled/ && $0~/true/) ok=1} END{exit !ok}' i2pd_yggdrasil_docker/src/i2pd.conf; then
|
exit 1
|
||||||
echo "[validate-config] warning: i2pd DNS section present but not enabled"
|
fi
|
||||||
|
|
||||||
|
# verify the current .i2p DNS architecture is documented in config
|
||||||
|
if grep -q '^\[dns\]' i2pd_yggdrasil_docker/src/i2pd.conf; then
|
||||||
|
if ! awk '/^\[dns\]/,/^\[/{if($0~/enabled/ && $0~/true/) ok=1} END{exit !ok}' i2pd_yggdrasil_docker/src/i2pd.conf; then
|
||||||
|
echo "[validate-config] warning: i2pd [dns] section exists but is disabled; the stack currently expects the separate i2pdns bridge service"
|
||||||
fi
|
fi
|
||||||
else
|
elif ! grep -qE 'i2pdns bridge|local i2pdns bridge service' i2pd_yggdrasil_docker/src/i2pd.conf; then
|
||||||
echo "[validate-config] warning: i2pd.conf missing [dns] section"
|
echo "[validate-config] warning: i2pd.conf should document that .i2p DNS is served by the separate i2pdns bridge service"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# tor configuration should expose DNSPort for onion resolution
|
# tor configuration should expose DNSPort for onion resolution
|
||||||
@@ -75,20 +116,35 @@ if command -v dig >/dev/null 2>&1; then
|
|||||||
echo "[validate-config] i2p query failed" >&2
|
echo "[validate-config] i2p query failed" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
echo "[validate-config] performing live DNS query for .eth via CoreDNS"
|
||||||
|
if ! dig @10.5.0.4 -p 53 TXT vitalik.eth +short | grep -q 'address='; then
|
||||||
|
echo "[validate-config] eth query failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "[validate-config] performing live DNS query for .zil via CoreDNS"
|
||||||
|
if ! dig @10.5.0.4 -p 53 TXT brad.zil +short | grep -q 'zil_address='; then
|
||||||
|
echo "[validate-config] zil query failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ensure referenced secrets exist
|
# ensure referenced secrets exist
|
||||||
for s in ./secrets/tz.txt ./secrets/YGGDRASIL_GENERATE_KEYS.txt; do
|
for s in \
|
||||||
|
./secrets/tz.txt \
|
||||||
|
./secrets/YGGDRASIL_GENERATE_KEYS.txt \
|
||||||
|
./secrets/pihole_webpassword.txt \
|
||||||
|
./secrets/3proxy_users.txt \
|
||||||
|
./secrets/namecoin_rpc_password.txt; do
|
||||||
if [ ! -f "$s" ]; then
|
if [ ! -f "$s" ]; then
|
||||||
echo "[validate-config] error: secret file $s is missing" >&2
|
echo "[validate-config] error: secret file $s is missing" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# check for :latest tags (discouraged in prod)
|
# check for mutable :latest tags in deployment compose files
|
||||||
if grep -qE 'image: .*:latest' docker-compose.yml; then
|
if grep -qE 'image: .*:latest' docker-compose.yml; then
|
||||||
echo "[validate-config] warning: some services use the ':latest' image tag;" \
|
echo "[validate-config] error: mutable ':latest' image tags are not allowed in the release compose file" >&2
|
||||||
"pin to a specific version before deploying to production."
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# warn if there are no healthchecks at all
|
# warn if there are no healthchecks at all
|
||||||
@@ -96,10 +152,6 @@ if ! grep -q 'healthcheck:' docker-compose.yml; then
|
|||||||
echo "[validate-config] warning: no healthcheck definitions found in docker-compose.yml"
|
echo "[validate-config] warning: no healthcheck definitions found in docker-compose.yml"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[validate-config] warning: some services use the ':latest' image tag;" \
|
|
||||||
"pin to a specific version before deploying to production."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# verify every service has a restart policy
|
# verify every service has a restart policy
|
||||||
if ! grep -q "restart:" docker-compose.yml; then
|
if ! grep -q "restart:" docker-compose.yml; then
|
||||||
echo "[validate-config] warning: some services lack a restart policy."
|
echo "[validate-config] warning: some services lack a restart policy."
|
||||||
@@ -112,13 +164,23 @@ if [ -n "$(echo "$ports" | uniq -d)" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# unbound configuration syntax check
|
# unbound configuration syntax check
|
||||||
if command -v unbound-checkconf >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "[validate-config] checking unbound configuration in runtime image"
|
||||||
|
UNBOUND_IMAGE="$(awk '/image: mvance\/unbound@sha256:/{print $2; exit}' "$TEMP_FILE")"
|
||||||
|
if [ -z "$UNBOUND_IMAGE" ]; then
|
||||||
|
echo "[validate-config] error: failed to resolve unbound runtime image from rendered compose config" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker run --rm --entrypoint /bin/sh \
|
||||||
|
-v "${PWD}/unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf:ro" \
|
||||||
|
-v "${PWD}/unbound/forward-records.conf:/opt/unbound/etc/unbound/forward-records.conf:ro" \
|
||||||
|
-v "${PWD}/unbound/a-records.conf:/opt/unbound/etc/unbound/a-records.conf:ro" \
|
||||||
|
-v "${PWD}/unbound/srv-records.conf:/opt/unbound/etc/unbound/srv-records.conf:ro" \
|
||||||
|
"$UNBOUND_IMAGE" -lc \
|
||||||
|
'getent passwd _unbound >/dev/null || useradd -r -s /usr/sbin/nologin _unbound; mkdir -p /opt/unbound/etc/unbound/dev /opt/unbound/etc/unbound/var/log; : > /opt/unbound/etc/unbound/dev/null; [ -s /opt/unbound/etc/unbound/var/root.key ] || /opt/unbound/sbin/unbound-anchor -a /opt/unbound/etc/unbound/var/root.key >/dev/null 2>&1 || :; [ -e /opt/unbound/etc/unbound/var/root.key ] || : > /opt/unbound/etc/unbound/var/root.key; /opt/unbound/sbin/unbound-checkconf /opt/unbound/etc/unbound/unbound.conf'
|
||||||
|
elif command -v unbound-checkconf >/dev/null 2>&1; then
|
||||||
echo "[validate-config] checking unbound configuration locally"
|
echo "[validate-config] checking unbound configuration locally"
|
||||||
unbound-checkconf -c unbound/unbound.conf
|
unbound-checkconf unbound/unbound.conf
|
||||||
elif command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "[validate-config] checking unbound configuration in container"
|
|
||||||
docker run --rm -v "${PWD}/unbound/unbound.conf:/etc/unbound/unbound.conf:ro" \
|
|
||||||
mvance/unbound:latest unbound-checkconf -c /etc/unbound/unbound.conf
|
|
||||||
else
|
else
|
||||||
echo "[validate-config] cannot check unbound config (no unbound-checkconf or docker)"
|
echo "[validate-config] cannot check unbound config (no unbound-checkconf or docker)"
|
||||||
fi
|
fi
|
||||||
@@ -134,6 +196,26 @@ for cfg in 3proxy/first-instanse.cfg 3proxy/second-instanse.cfg; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if grep -q '^auth none$' 3proxy/first-instanse.cfg; then
|
||||||
|
echo "[validate-config] error: dark3proxy must not expose a public proxy with 'auth none'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q 'CHANGE_ME_NAMECOIN_RPC_PASSWORD' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: replace inline Namecoin RPC password placeholders with Docker secrets" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q 'tskey-YOUR-AUTH-KEY-HERE' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: tailscale placeholder auth key is still present in docker-compose.yml" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q 'profiles: \["tailscale"\]' docker-compose.yml; then
|
||||||
|
echo "[validate-config] error: tailscale should be opt-in via a compose profile" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# simple sanity checks
|
# simple sanity checks
|
||||||
if grep -q "container_name: darkproxy" docker-compose.yml; then
|
if grep -q "container_name: darkproxy" docker-compose.yml; then
|
||||||
echo "[validate-config] warning: 'container_name: darkproxy' appears in compose;" \
|
echo "[validate-config] warning: 'container_name: darkproxy' appears in compose;" \
|
||||||
@@ -141,9 +223,24 @@ if grep -q "container_name: darkproxy" docker-compose.yml; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ensure Pi-hole DNS environment is correctly formatted
|
# ensure Pi-hole DNS environment is correctly formatted
|
||||||
if grep -q "PIHOLE_DNS_" docker-compose.yml | grep -q "PIHOLE_DNS_:"; then
|
if grep -q "PIHOLE_DNS_:" docker-compose.yml; then
|
||||||
echo "[validate-config] warning: PIHOLE_DNS_ variable ends with underscore;" \
|
echo "[validate-config] warning: PIHOLE_DNS_ variable ends with underscore;" \
|
||||||
"consider using PIHOLE_DNS_1, PIHOLE_DNS_2 etc."
|
"consider using PIHOLE_DNS_1, PIHOLE_DNS_2 etc."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure critical services have healthchecks in the rendered compose config
|
||||||
|
if [ -f "$TEMP_FILE" ]; then
|
||||||
|
for service in dark3proxy i2pd_yggdrasil i2pdns unbound pihole ensdns zildns lokinet namecoind namecoindns status_dashboard; do
|
||||||
|
if ! awk -v svc="$service" '
|
||||||
|
$0 ~ "^ " svc ":" { in_svc=1; next }
|
||||||
|
in_svc && $0 ~ "^ [^ ]" { exit !found }
|
||||||
|
in_svc && $0 ~ "^ healthcheck:" { found=1 }
|
||||||
|
END { if (in_svc) exit !found; exit 0 }
|
||||||
|
' "$TEMP_FILE"; then
|
||||||
|
echo "[validate-config] error: service $service is missing a healthcheck in rendered compose config" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[validate-config] all validations passed"
|
echo "[validate-config] all validations passed"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#syntax=docker/dockerfile:1
|
||||||
|
FROM tailscale/tailscale@sha256:dbeff02d2337344b351afac203427218c4d0a06c43fc10a865184063498472a6
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
iptables \
|
||||||
|
socat \
|
||||||
|
curl \
|
||||||
|
bash \
|
||||||
|
redsocks
|
||||||
|
|
||||||
|
COPY ./post-rules.sh /post-rules.sh
|
||||||
|
COPY ./redsocks.conf /etc/redsocks.conf
|
||||||
|
RUN chmod +x /post-rules.sh
|
||||||
|
|
||||||
|
# Entrypoint to configure and start Tailscale + traffic redirection
|
||||||
|
ENTRYPOINT ["/post-rules.sh"]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Start Tailscale in background
|
||||||
|
/usr/local/bin/tailscaled --tun=userspace-networking --encrypt-state=false --hardware-attestation=false &
|
||||||
|
TAILSCALED_PID=$!
|
||||||
|
|
||||||
|
# Wait for Tailscale to initialize
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if [ -z "${TS_AUTHKEY:-}" ]; then
|
||||||
|
echo "TS_AUTHKEY must be set when enabling the tailscale profile" >&2
|
||||||
|
kill "$TAILSCALED_PID"
|
||||||
|
wait "$TAILSCALED_PID" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# tailscale up flags change over time; avoid deprecated options.
|
||||||
|
/usr/local/bin/tailscale up --authkey="$TS_AUTHKEY" ${TS_EXTRA_ARGS:-"--accept-dns=false --advertise-exit-node"}
|
||||||
|
|
||||||
|
# Wait a bit for Tailscale to be ready
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Configure iptables to redirect all traffic through redsocks
|
||||||
|
echo "Setting up iptables rules for traffic redirection..."
|
||||||
|
|
||||||
|
# Enable IP forwarding for exit node
|
||||||
|
sysctl -w net.ipv4.ip_forward=1 2>/dev/null || true
|
||||||
|
sysctl -w net.ipv6.conf.all.forwarding=1 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create a chain for darkproxy traffic
|
||||||
|
iptables -t nat -N DARKPROXY 2>/dev/null || iptables -t nat -F DARKPROXY
|
||||||
|
|
||||||
|
# Whitelist local traffic and Tailscale
|
||||||
|
iptables -t nat -A DARKPROXY -d 127.0.0.0/8 -j RETURN
|
||||||
|
iptables -t nat -A DARKPROXY -d 10.5.0.0/16 -j RETURN
|
||||||
|
iptables -t nat -A DARKPROXY -d 100.64.0.0/10 -j RETURN # Tailscale IP range
|
||||||
|
|
||||||
|
# Redirect all other TCP to redsocks
|
||||||
|
iptables -t nat -A DARKPROXY -p tcp -j REDIRECT --to-ports 12345
|
||||||
|
|
||||||
|
# Apply chain to all output
|
||||||
|
iptables -t nat -I OUTPUT 1 -j DARKPROXY 2>/dev/null || true
|
||||||
|
iptables -t nat -I PREROUTING 1 -j DARKPROXY 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Starting redsocks..."
|
||||||
|
redsocks -c /etc/redsocks.conf
|
||||||
|
|
||||||
|
# Keep container alive
|
||||||
|
wait $TAILSCALED_PID
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
base {
|
||||||
|
log_debug = off;
|
||||||
|
log_info = on;
|
||||||
|
log = "stderr";
|
||||||
|
daemon = on;
|
||||||
|
redirector = iptables;
|
||||||
|
}
|
||||||
|
|
||||||
|
redsocks {
|
||||||
|
local_ip = 0.0.0.0;
|
||||||
|
local_port = 12345;
|
||||||
|
ip = 10.5.0.8;
|
||||||
|
port = 1080;
|
||||||
|
type = socks5;
|
||||||
|
}
|
||||||
|
|
||||||
|
redudp {
|
||||||
|
local_ip = 0.0.0.0;
|
||||||
|
local_port = 10053;
|
||||||
|
ip = 10.5.0.8;
|
||||||
|
port = 1080;
|
||||||
|
|
||||||
|
dest_ip = 10.5.0.6;
|
||||||
|
dest_port = 53;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# A Record
|
||||||
|
#local-data: "somecomputer.local. A 192.168.1.1"
|
||||||
|
|
||||||
|
# PTR Record
|
||||||
|
#local-data-ptr: "192.168.1.1 somecomputer.local."
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
forward-zone:
|
||||||
|
# Forward all queries (except those in cache and local zone) to
|
||||||
|
# upstream recursive servers
|
||||||
|
name: "."
|
||||||
|
# Queries to this forward zone use TLS
|
||||||
|
forward-tls-upstream: yes
|
||||||
|
|
||||||
|
# https://dnsprivacy.org/wiki/display/DP/DNS+Privacy+Test+Servers
|
||||||
|
|
||||||
|
## Cloudflare
|
||||||
|
forward-addr: 1.1.1.1@853#cloudflare-dns.com
|
||||||
|
forward-addr: 1.0.0.1@853#cloudflare-dns.com
|
||||||
|
#forward-addr: 2606:4700:4700::1111@853#cloudflare-dns.com
|
||||||
|
#forward-addr: 2606:4700:4700::1001@853#cloudflare-dns.com
|
||||||
|
|
||||||
|
## Cloudflare Malware
|
||||||
|
# forward-addr: 1.1.1.2@853#security.cloudflare-dns.com
|
||||||
|
# forward-addr: 1.0.0.2@853#security.cloudflare-dns.com
|
||||||
|
# forward-addr: 2606:4700:4700::1112@853#security.cloudflare-dns.com
|
||||||
|
# forward-addr: 2606:4700:4700::1002@853#security.cloudflare-dns.com
|
||||||
|
|
||||||
|
## Cloudflare Malware and Adult Content
|
||||||
|
# forward-addr: 1.1.1.3@853#family.cloudflare-dns.com
|
||||||
|
# forward-addr: 1.0.0.3@853#family.cloudflare-dns.com
|
||||||
|
# forward-addr: 2606:4700:4700::1113@853#family.cloudflare-dns.com
|
||||||
|
# forward-addr: 2606:4700:4700::1003@853#family.cloudflare-dns.com
|
||||||
|
|
||||||
|
## CleanBrowsing Security Filter
|
||||||
|
# forward-addr: 185.228.168.9@853#security-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 185.228.169.9@853#security-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:1::2@853#security-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:2::2@853#security-filter-dns.cleanbrowsing.org
|
||||||
|
|
||||||
|
## CleanBrowsing Adult Filter
|
||||||
|
# forward-addr: 185.228.168.10@853#adult-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 185.228.169.11@853#adult-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:1::1@853#adult-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:2::1@853#adult-filter-dns.cleanbrowsing.org
|
||||||
|
|
||||||
|
## CleanBrowsing Family Filter
|
||||||
|
# forward-addr: 185.228.168.168@853#family-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 185.228.169.168@853#family-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:1::@853#family-filter-dns.cleanbrowsing.org
|
||||||
|
# forward-addr: 2a0d:2a00:2::@853#family-filter-dns.cleanbrowsing.org
|
||||||
|
|
||||||
|
## Quad9
|
||||||
|
# forward-addr: 9.9.9.9@853#dns.quad9.net
|
||||||
|
# forward-addr: 149.112.112.112@853#dns.quad9.net
|
||||||
|
# forward-addr: 2620:fe::fe@853#dns.quad9.net
|
||||||
|
# forward-addr: 2620:fe::9@853#dns.quad9.net
|
||||||
|
|
||||||
|
## getdnsapi.net
|
||||||
|
# forward-addr: 185.49.141.37@853#getdnsapi.net
|
||||||
|
# forward-addr: 2a04:b900:0:100::37@853#getdnsapi.net
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# SRV records for local DNS overrides
|
||||||
|
# Example:
|
||||||
|
# local-data: "_service._tcp.example.local. 300 IN SRV 10 5 8080 host.example.local."
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
server:
|
||||||
|
###########################################################################
|
||||||
|
# BASIC SETTINGS
|
||||||
|
###########################################################################
|
||||||
|
# Time to live maximum for RRsets and messages in the cache. If the maximum
|
||||||
|
# kicks in, responses to clients still get decrementing TTLs based on the
|
||||||
|
# original (larger) values. When the internal TTL expires, the cache item
|
||||||
|
# has expired. Can be set lower to force the resolver to query for data
|
||||||
|
# often, and not trust (very large) TTL values.
|
||||||
|
cache-max-ttl: 86400
|
||||||
|
|
||||||
|
# Time to live minimum for RRsets and messages in the cache. If the minimum
|
||||||
|
# kicks in, the data is cached for longer than the domain owner intended,
|
||||||
|
# and thus less queries are made to look up the data. Zero makes sure the
|
||||||
|
# data in the cache is as the domain owner intended, higher values,
|
||||||
|
# especially more than an hour or so, can lead to trouble as the data in
|
||||||
|
# the cache does not match up with the actual data any more.
|
||||||
|
cache-min-ttl: 300
|
||||||
|
|
||||||
|
# Set the working directory for the program.
|
||||||
|
directory: "/opt/unbound/etc/unbound"
|
||||||
|
|
||||||
|
# Enable or disable whether IPv4 queries are answered or issued.
|
||||||
|
# Default: yes
|
||||||
|
do-ip4: yes
|
||||||
|
|
||||||
|
# Enable or disable whether IPv6 queries are answered or issued.
|
||||||
|
# If disabled, queries are not answered on IPv6, and queries are not sent
|
||||||
|
# on IPv6 to the internet nameservers. With this option you can disable the
|
||||||
|
# IPv6 transport for sending DNS traffic, it does not impact the contents
|
||||||
|
# of the DNS traffic, which may have IPv4 (A) and IPv6 (AAAA) addresses in
|
||||||
|
# it.
|
||||||
|
# Default: yes
|
||||||
|
# May be set to yes if you have IPv6 connectivity
|
||||||
|
do-ip6: yes
|
||||||
|
|
||||||
|
# Enable or disable whether TCP queries are answered or issued.
|
||||||
|
# Default: yes
|
||||||
|
do-tcp: yes
|
||||||
|
|
||||||
|
# Enable or disable whether UDP queries are answered or issued.
|
||||||
|
# Default: yes
|
||||||
|
do-udp: yes
|
||||||
|
|
||||||
|
# RFC 6891. Number of bytes size to advertise as the EDNS reassembly buffer
|
||||||
|
# size. This is the value put into datagrams over UDP towards peers.
|
||||||
|
# The actual buffer size is determined by msg-buffer-size (both for TCP and
|
||||||
|
# UDP). Do not set higher than that value.
|
||||||
|
# Default is 1232 which is the DNS Flag Day 2020 recommendation.
|
||||||
|
# Setting to 512 bypasses even the most stringent path MTU problems, but
|
||||||
|
# is seen as extreme, since the amount of TCP fallback generated is
|
||||||
|
# excessive (probably also for this resolver, consider tuning the outgoing
|
||||||
|
# tcp number).
|
||||||
|
edns-buffer-size: 1232
|
||||||
|
|
||||||
|
# Listen to for queries from clients and answer from this network interface
|
||||||
|
# and port.
|
||||||
|
interface: 0.0.0.0@53
|
||||||
|
# interface: ::0
|
||||||
|
port: 53
|
||||||
|
|
||||||
|
# If enabled, prefer IPv6 transport for sending DNS queries to internet
|
||||||
|
# nameservers.
|
||||||
|
# Default: yes
|
||||||
|
# You want to leave this to no unless you have *native* IPv6. With 6to4 and
|
||||||
|
# Terredo tunnels your web browser should favor IPv4 for the same reasons
|
||||||
|
prefer-ip6: no
|
||||||
|
|
||||||
|
# Rotates RRSet order in response (the pseudo-random number is taken from
|
||||||
|
# the query ID, for speed and thread safety).
|
||||||
|
rrset-roundrobin: yes
|
||||||
|
|
||||||
|
# Drop user privileges after binding the port.
|
||||||
|
username: "unbound"
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# LOGGING
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
# Do not print log lines to inform about local zone actions
|
||||||
|
log-local-actions: no
|
||||||
|
|
||||||
|
# Do not print one line per query to the log
|
||||||
|
log-queries: no
|
||||||
|
|
||||||
|
# Do not print one line per reply to the log
|
||||||
|
log-replies: no
|
||||||
|
|
||||||
|
# Do not print log lines that say why queries return SERVFAIL to clients
|
||||||
|
log-servfail: no
|
||||||
|
|
||||||
|
# If you want to log to a file, use:
|
||||||
|
# logfile: /opt/unbound/etc/unbound/unbound.log
|
||||||
|
# Set log location (using /dev/null further limits logging)
|
||||||
|
logfile: /dev/null
|
||||||
|
|
||||||
|
# Set logging level
|
||||||
|
# Level 0: No verbosity, only errors.
|
||||||
|
# Level 1: Gives operational information.
|
||||||
|
# Level 2: Gives detailed operational information including short information per query.
|
||||||
|
# Level 3: Gives query level information, output per query.
|
||||||
|
# Level 4: Gives algorithm level information.
|
||||||
|
# Level 5: Logs client identification for cache misses.
|
||||||
|
verbosity: 0
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# PERFORMANCE SETTINGS
|
||||||
|
###########################################################################
|
||||||
|
# https://nlnetlabs.nl/documentation/unbound/howto-optimise/
|
||||||
|
# https://nlnetlabs.nl/news/2019/Feb/05/unbound-1.9.0-released/
|
||||||
|
|
||||||
|
# Number of slabs in the infrastructure cache. Slabs reduce lock contention
|
||||||
|
# by threads. Must be set to a power of 2.
|
||||||
|
infra-cache-slabs: 4
|
||||||
|
|
||||||
|
# Number of incoming TCP buffers to allocate per thread. Default
|
||||||
|
# is 10. If set to 0, or if do-tcp is "no", no TCP queries from
|
||||||
|
# clients are accepted. For larger installations increasing this
|
||||||
|
# value is a good idea.
|
||||||
|
incoming-num-tcp: 10
|
||||||
|
|
||||||
|
# Number of slabs in the key cache. Slabs reduce lock contention by
|
||||||
|
# threads. Must be set to a power of 2. Setting (close) to the number
|
||||||
|
# of cpus is a reasonable guess.
|
||||||
|
key-cache-slabs: 4
|
||||||
|
|
||||||
|
# Number of bytes size of the message cache.
|
||||||
|
# Unbound recommendation is to Use roughly twice as much rrset cache memory
|
||||||
|
# as you use msg cache memory.
|
||||||
|
msg-cache-size: 142768128
|
||||||
|
|
||||||
|
# Number of slabs in the message cache. Slabs reduce lock contention by
|
||||||
|
# threads. Must be set to a power of 2. Setting (close) to the number of
|
||||||
|
# cpus is a reasonable guess.
|
||||||
|
msg-cache-slabs: 4
|
||||||
|
|
||||||
|
# The number of queries that every thread will service simultaneously. If
|
||||||
|
# more queries arrive that need servicing, and no queries can be jostled
|
||||||
|
# out (see jostle-timeout), then the queries are dropped.
|
||||||
|
# This is best set at half the number of the outgoing-range.
|
||||||
|
# This Unbound instance was compiled with libevent so it can efficiently
|
||||||
|
# use more than 1024 file descriptors.
|
||||||
|
num-queries-per-thread: 4096
|
||||||
|
|
||||||
|
# The number of threads to create to serve clients.
|
||||||
|
# This is set dynamically at run time to effectively use available CPUs
|
||||||
|
# resources
|
||||||
|
num-threads: 3
|
||||||
|
|
||||||
|
# Number of ports to open. This number of file descriptors can be opened
|
||||||
|
# per thread.
|
||||||
|
# This Unbound instance was compiled with libevent so it can efficiently
|
||||||
|
# use more than 1024 file descriptors.
|
||||||
|
outgoing-range: 8192
|
||||||
|
|
||||||
|
# Number of bytes size of the RRset cache.
|
||||||
|
# Use roughly twice as much rrset cache memory as msg cache memory
|
||||||
|
rrset-cache-size: 285536256
|
||||||
|
|
||||||
|
# Number of slabs in the RRset cache. Slabs reduce lock contention by
|
||||||
|
# threads. Must be set to a power of 2.
|
||||||
|
rrset-cache-slabs: 4
|
||||||
|
|
||||||
|
# Do no insert authority/additional sections into response messages when
|
||||||
|
# those sections are not required. This reduces response size
|
||||||
|
# significantly, and may avoid TCP fallback for some responses. This may
|
||||||
|
# cause a slight speedup.
|
||||||
|
minimal-responses: yes
|
||||||
|
|
||||||
|
# # Fetch the DNSKEYs earlier in the validation process, when a DS record
|
||||||
|
# is encountered. This lowers the latency of requests at the expense of
|
||||||
|
# little more CPU usage.
|
||||||
|
prefetch: yes
|
||||||
|
|
||||||
|
# Fetch the DNSKEYs earlier in the validation process, when a DS record is
|
||||||
|
# encountered. This lowers the latency of requests at the expense of little
|
||||||
|
# more CPU usage.
|
||||||
|
prefetch-key: yes
|
||||||
|
|
||||||
|
# Have unbound attempt to serve old responses from cache with a TTL of 0 in
|
||||||
|
# the response without waiting for the actual resolution to finish. The
|
||||||
|
# actual resolution answer ends up in the cache later on.
|
||||||
|
serve-expired: yes
|
||||||
|
|
||||||
|
# If not 0, then set the SO_RCVBUF socket option to get more buffer space on
|
||||||
|
# UDP port 53 incoming queries. So that short spikes on busy servers do not
|
||||||
|
# drop packets (see counter in netstat -su). Otherwise, the number of bytes
|
||||||
|
# to ask for, try “4m” on a busy server.
|
||||||
|
# The OS caps it at a maximum, on linux Unbound needs root permission to
|
||||||
|
# bypass the limit, or the admin can use sysctl net.core.rmem_max.
|
||||||
|
# Default: 0 (use system value)
|
||||||
|
# For example: sysctl -w net.core.rmem_max=4194304
|
||||||
|
# To persist reboots, edit /etc/sysctl.conf to include:
|
||||||
|
# net.core.rmem_max=4194304
|
||||||
|
# Larger socket buffer. OS may need config.
|
||||||
|
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
|
||||||
|
#so-rcvbuf: 4m
|
||||||
|
|
||||||
|
# Open dedicated listening sockets for incoming queries for each thread and
|
||||||
|
# try to set the SO_REUSEPORT socket option on each socket. May distribute
|
||||||
|
# incoming queries to threads more evenly.
|
||||||
|
so-reuseport: yes
|
||||||
|
|
||||||
|
# If not 0, then set the SO_SNDBUF socket option to get more buffer space
|
||||||
|
# on UDP port 53 outgoing queries.
|
||||||
|
# Specify the number of bytes to ask for, try “4m” on a very busy server.
|
||||||
|
# The OS caps it at a maximum, on linux Unbound needs root permission to
|
||||||
|
# bypass the limit, or the admin can use sysctl net.core.wmem_max.
|
||||||
|
# For example: sysctl -w net.core.wmem_max=4194304
|
||||||
|
# To persist reboots, edit /etc/sysctl.conf to include:
|
||||||
|
# net.core.wmem_max=4194304
|
||||||
|
# Default: 0 (use system value)
|
||||||
|
# Larger socket buffer. OS may need config.
|
||||||
|
# Ensure kernel buffer is large enough to not lose messages in traffic spikes
|
||||||
|
#so-sndbuf: 4m
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# PRIVACY SETTINGS
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
# RFC 8198. Use the DNSSEC NSEC chain to synthesize NXDO-MAIN and other
|
||||||
|
# denials, using information from previous NXDO-MAINs answers. In other
|
||||||
|
# words, use cached NSEC records to generate negative answers within a
|
||||||
|
# range and positive answers from wildcards. This increases performance,
|
||||||
|
# decreases latency and resource utilization on both authoritative and
|
||||||
|
# recursive servers, and increases privacy. Also, it may help increase
|
||||||
|
# resilience to certain DoS attacks in some circumstances.
|
||||||
|
aggressive-nsec: yes
|
||||||
|
|
||||||
|
# Extra delay for timeouted UDP ports before they are closed, in msec.
|
||||||
|
# This prevents very delayed answer packets from the upstream (recursive)
|
||||||
|
# servers from bouncing against closed ports and setting off all sort of
|
||||||
|
# close-port counters, with eg. 1500 msec. When timeouts happen you need
|
||||||
|
# extra sockets, it checks the ID and remote IP of packets, and unwanted
|
||||||
|
# packets are added to the unwanted packet counter.
|
||||||
|
delay-close: 10000
|
||||||
|
|
||||||
|
# Prevent the unbound server from forking into the background as a daemon
|
||||||
|
do-daemonize: no
|
||||||
|
|
||||||
|
# Add localhost to the do-not-query-address list.
|
||||||
|
do-not-query-localhost: no
|
||||||
|
|
||||||
|
# Number of bytes size of the aggressive negative cache.
|
||||||
|
neg-cache-size: 4M
|
||||||
|
|
||||||
|
# Send minimum amount of information to upstream servers to enhance
|
||||||
|
# privacy (best privacy).
|
||||||
|
qname-minimisation: yes
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# SECURITY SETTINGS
|
||||||
|
###########################################################################
|
||||||
|
# Only give access to recursion clients from LAN IPs
|
||||||
|
access-control: 127.0.0.1/32 allow
|
||||||
|
access-control: 192.168.0.0/16 allow
|
||||||
|
access-control: 172.16.0.0/12 allow
|
||||||
|
access-control: 10.0.0.0/8 allow
|
||||||
|
access-control: fc00::/7 allow
|
||||||
|
access-control: ::1/128 allow
|
||||||
|
|
||||||
|
# File with trust anchor for one zone, which is tracked with RFC5011
|
||||||
|
# probes.
|
||||||
|
auto-trust-anchor-file: "var/root.key"
|
||||||
|
|
||||||
|
# Enable chroot (i.e, change apparent root directory for the current
|
||||||
|
# running process and its children)
|
||||||
|
chroot: "/opt/unbound/etc/unbound"
|
||||||
|
|
||||||
|
# Deny queries of type ANY with an empty response.
|
||||||
|
deny-any: yes
|
||||||
|
|
||||||
|
# Harden against algorithm downgrade when multiple algorithms are
|
||||||
|
# advertised in the DS record.
|
||||||
|
harden-algo-downgrade: yes
|
||||||
|
|
||||||
|
# RFC 8020. returns nxdomain to queries for a name below another name that
|
||||||
|
# is already known to be nxdomain.
|
||||||
|
harden-below-nxdomain: yes
|
||||||
|
|
||||||
|
# Require DNSSEC data for trust-anchored zones, if such data is absent, the
|
||||||
|
# zone becomes bogus. If turned off you run the risk of a downgrade attack
|
||||||
|
# that disables security for a zone.
|
||||||
|
harden-dnssec-stripped: yes
|
||||||
|
|
||||||
|
# Only trust glue if it is within the servers authority.
|
||||||
|
harden-glue: yes
|
||||||
|
|
||||||
|
# Ignore very large queries.
|
||||||
|
harden-large-queries: yes
|
||||||
|
|
||||||
|
# Perform additional queries for infrastructure data to harden the referral
|
||||||
|
# path. Validates the replies if trust anchors are configured and the zones
|
||||||
|
# are signed. This enforces DNSSEC validation on nameserver NS sets and the
|
||||||
|
# nameserver addresses that are encountered on the referral path to the
|
||||||
|
# answer. Experimental option.
|
||||||
|
harden-referral-path: no
|
||||||
|
|
||||||
|
# Ignore very small EDNS buffer sizes from queries.
|
||||||
|
harden-short-bufsize: yes
|
||||||
|
|
||||||
|
# If enabled the HTTP header User-Agent is not set. Use with caution
|
||||||
|
# as some webserver configurations may reject HTTP requests lacking
|
||||||
|
# this header. If needed, it is better to explicitly set the
|
||||||
|
# the http-user-agent.
|
||||||
|
hide-http-user-agent: no
|
||||||
|
|
||||||
|
# Refuse id.server and hostname.bind queries
|
||||||
|
hide-identity: yes
|
||||||
|
|
||||||
|
# Refuse version.server and version.bind queries
|
||||||
|
hide-version: yes
|
||||||
|
|
||||||
|
# Set the HTTP User-Agent header for outgoing HTTP requests. If
|
||||||
|
# set to "", the default, then the package name and version are
|
||||||
|
# used.
|
||||||
|
http-user-agent: "DNS"
|
||||||
|
|
||||||
|
# Report this identity rather than the hostname of the server.
|
||||||
|
identity: "DNS"
|
||||||
|
|
||||||
|
# These private network addresses are not allowed to be returned for public
|
||||||
|
# internet names. Any occurrence of such addresses are removed from DNS
|
||||||
|
# answers. Additionally, the DNSSEC validator may mark the answers bogus.
|
||||||
|
# This protects against DNS Rebinding
|
||||||
|
private-address: 10.0.0.0/8
|
||||||
|
private-address: 172.16.0.0/12
|
||||||
|
private-address: 192.168.0.0/16
|
||||||
|
private-address: 169.254.0.0/16
|
||||||
|
private-address: fd00::/8
|
||||||
|
private-address: fe80::/10
|
||||||
|
private-address: ::ffff:0:0/96
|
||||||
|
|
||||||
|
# Enable ratelimiting of queries (per second) sent to nameserver for
|
||||||
|
# performing recursion. More queries are turned away with an error
|
||||||
|
# (servfail). This stops recursive floods (e.g., random query names), but
|
||||||
|
# not spoofed reflection floods. Cached responses are not rate limited by
|
||||||
|
# this setting. Experimental option.
|
||||||
|
ratelimit: 1000
|
||||||
|
|
||||||
|
# Use this certificate bundle for authenticating connections made to
|
||||||
|
# outside peers (e.g., auth-zone urls, DNS over TLS connections).
|
||||||
|
tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
# Set the total number of unwanted replies to eep track of in every thread.
|
||||||
|
# When it reaches the threshold, a defensive action of clearing the rrset
|
||||||
|
# and message caches is taken, hopefully flushing away any poison.
|
||||||
|
# Unbound suggests a value of 10 million.
|
||||||
|
unwanted-reply-threshold: 10000
|
||||||
|
|
||||||
|
# Use 0x20-encoded random bits in the query to foil spoof attempts. This
|
||||||
|
# perturbs the lowercase and uppercase of query names sent to authority
|
||||||
|
# servers and checks if the reply still has the correct casing.
|
||||||
|
# This feature is an experimental implementation of draft dns-0x20.
|
||||||
|
# Experimental option.
|
||||||
|
# Don't use Capitalization randomization as it known to cause DNSSEC issues
|
||||||
|
# see https://discourse.pi-hole.net/t/unbound-stubby-or-dnscrypt-proxy/9378
|
||||||
|
use-caps-for-id: no
|
||||||
|
|
||||||
|
# Help protect users that rely on this validator for authentication from
|
||||||
|
# potentially bad data in the additional section. Instruct the validator to
|
||||||
|
# remove data from the additional section of secure messages that are not
|
||||||
|
# signed properly. Messages that are insecure, bogus, indeterminate or
|
||||||
|
# unchecked are not affected.
|
||||||
|
val-clean-additional: yes
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# FORWARD ZONE
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
# include: /opt/unbound/etc/unbound/forward-records.conf
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# LOCAL ZONE
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
# Include file for local-data and local-data-ptr
|
||||||
|
include: /opt/unbound/etc/unbound/a-records.conf
|
||||||
|
include: /opt/unbound/etc/unbound/srv-records.conf
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
# WILDCARD INCLUDE
|
||||||
|
###########################################################################
|
||||||
|
#include: "/opt/unbound/etc/unbound/*.conf"
|
||||||
|
|
||||||
|
remote-control:
|
||||||
|
control-enable: no
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
FROM debian:stable-slim
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
unbound \
|
||||||
|
unbound-anchor \
|
||||||
|
dnsutils \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Keep path compatibility with existing bind mounts in docker-compose.yml
|
||||||
|
RUN mkdir -p /opt/unbound/etc/unbound /opt/unbound/etc/unbound/dev /opt/unbound/etc/unbound/var \
|
||||||
|
&& : > /opt/unbound/etc/unbound/dev/null
|
||||||
|
|
||||||
|
# Pre-create DNSSEC trust anchor used by the mounted unbound.conf.
|
||||||
|
RUN unbound-anchor -a /opt/unbound/etc/unbound/var/root.key || true
|
||||||
|
|
||||||
|
# Allow unbound daemon user to update trust anchors and write to chrooted /dev/null.
|
||||||
|
RUN chown -R unbound:unbound /opt/unbound/etc/unbound/var /opt/unbound/etc/unbound/dev \
|
||||||
|
&& chmod 666 /opt/unbound/etc/unbound/dev/null
|
||||||
|
|
||||||
|
EXPOSE 53/tcp 53/udp
|
||||||
|
|
||||||
|
CMD ["/bin/sh", "-c", "[ -s /opt/unbound/etc/unbound/var/root.key ] || unbound-anchor -a /opt/unbound/etc/unbound/var/root.key || true; exec unbound -d -c /opt/unbound/etc/unbound/unbound.conf"]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:16-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json /app/package.json
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
|
COPY server.js /app/server.js
|
||||||
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 53/udp
|
||||||
|
EXPOSE 53/tcp
|
||||||
|
|
||||||
|
CMD ["/app/entrypoint.sh"]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Fix Docker's broken internal DNS by using external DNS directly
|
||||||
|
echo "nameserver 1.1.1.1" > /etc/resolv.conf
|
||||||
|
echo "nameserver 8.8.8.8" >> /etc/resolv.conf
|
||||||
|
|
||||||
|
# Start the Node.js server
|
||||||
|
exec node /app/server.js
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "zildns",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Blockchain-backed DNS resolver for .zil domains",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "server.js",
|
||||||
|
"dependencies": {
|
||||||
|
"@unstoppabledomains/resolution": "7.1.4",
|
||||||
|
"dns2": "^2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const dns2 = require("dns2");
|
||||||
|
const { default: Resolution } = require("@unstoppabledomains/resolution");
|
||||||
|
|
||||||
|
const LISTEN_HOST = process.env.ZILDNS_LISTEN_HOST || "0.0.0.0";
|
||||||
|
const LISTEN_PORT = Number(process.env.ZILDNS_LISTEN_PORT || "53");
|
||||||
|
const DEFAULT_TTL = Number(process.env.ZILDNS_TTL || "120");
|
||||||
|
const CACHE_SECONDS = Number(process.env.ZILDNS_CACHE_SECONDS || "30");
|
||||||
|
|
||||||
|
const ZNS_URL = process.env.ZILDNS_ZNS_URL || "https://api.zilliqa.com";
|
||||||
|
const ZNS_NETWORK = process.env.ZILDNS_ZNS_NETWORK || "mainnet";
|
||||||
|
const PREWARM_DOMAIN = normalizePrewarmDomain(process.env.ZILDNS_PREWARM_DOMAIN || "brad.zil");
|
||||||
|
|
||||||
|
const { Packet, UDPServer, TCPServer } = dns2;
|
||||||
|
const RCODE = {
|
||||||
|
NOERROR: 0,
|
||||||
|
SERVFAIL: 2,
|
||||||
|
NXDOMAIN: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolution = new Resolution({
|
||||||
|
sourceConfig: {
|
||||||
|
zns: {
|
||||||
|
url: ZNS_URL,
|
||||||
|
network: ZNS_NETWORK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const cache = new Map();
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || "").replace(/\.$/, "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePrewarmDomain(name) {
|
||||||
|
const normalized = normalizeName(name);
|
||||||
|
return normalized.endsWith(".zil") ? normalized : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheGet(key) {
|
||||||
|
const entry = cache.get(key);
|
||||||
|
if (!entry) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Date.now() > entry.expiresAt) {
|
||||||
|
cache.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheSet(key, value) {
|
||||||
|
cache.set(key, {
|
||||||
|
value,
|
||||||
|
expiresAt: Date.now() + CACHE_SECONDS * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeout(promise, ms) {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveZilDomain(domain) {
|
||||||
|
const cached = cacheGet(domain);
|
||||||
|
if (cached !== null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [zilResult, ipfsResult] = await withTimeout(
|
||||||
|
Promise.allSettled([
|
||||||
|
resolution.addr(domain, "ZIL"),
|
||||||
|
resolution.ipfsHash(domain),
|
||||||
|
]),
|
||||||
|
5000 // 5 second timeout
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
zilAddress: zilResult.status === "fulfilled" && zilResult.value ? zilResult.value : null,
|
||||||
|
ipfsHash: ipfsResult.status === "fulfilled" && ipfsResult.value ? ipfsResult.value : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasData = Boolean(result.zilAddress || result.ipfsHash);
|
||||||
|
const resolved = hasData ? result : null;
|
||||||
|
cacheSet(domain, resolved);
|
||||||
|
return resolved;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[zildns] API failed for ${domain}:`, err.message);
|
||||||
|
// Cache negative result for a short time to avoid repeated API failures
|
||||||
|
cacheSet(domain, null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResponse(request) {
|
||||||
|
const response = Packet.createResponseFromRequest(request);
|
||||||
|
response.header.aa = 1;
|
||||||
|
response.header.ra = 1;
|
||||||
|
response.header.z = 0;
|
||||||
|
response.header.tc = 0;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTxtAnswers(response, name, resolved) {
|
||||||
|
if (resolved.zilAddress) {
|
||||||
|
response.answers.push({
|
||||||
|
name,
|
||||||
|
type: Packet.TYPE.TXT,
|
||||||
|
class: Packet.CLASS.IN,
|
||||||
|
ttl: DEFAULT_TTL,
|
||||||
|
data: `zil_address=${resolved.zilAddress}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolved.ipfsHash) {
|
||||||
|
response.answers.push({
|
||||||
|
name,
|
||||||
|
type: Packet.TYPE.TXT,
|
||||||
|
class: Packet.CLASS.IN,
|
||||||
|
ttl: DEFAULT_TTL,
|
||||||
|
data: `ipfs_hash=${resolved.ipfsHash}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCnameAnswer(response, name, resolved) {
|
||||||
|
if (!resolved.ipfsHash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.answers.push({
|
||||||
|
name,
|
||||||
|
type: Packet.TYPE.CNAME,
|
||||||
|
class: Packet.CLASS.IN,
|
||||||
|
ttl: DEFAULT_TTL,
|
||||||
|
domain: `${resolved.ipfsHash}.ipfs.dweb.link.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildResponse(request) {
|
||||||
|
const response = makeResponse(request);
|
||||||
|
const question = request.questions && request.questions[0];
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qname = normalizeName(question.name);
|
||||||
|
const qtype = question.type;
|
||||||
|
const isType = (name) => qtype === name || qtype === Packet.TYPE[name];
|
||||||
|
|
||||||
|
if (!qname.endsWith(".zil")) {
|
||||||
|
response.header.rcode = RCODE.NXDOMAIN;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = await resolveZilDomain(qname);
|
||||||
|
if (!resolved) {
|
||||||
|
response.header.rcode = RCODE.NXDOMAIN;
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isType("TXT") || isType("ANY")) {
|
||||||
|
addTxtAnswers(response, question.name, resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isType("CNAME") || isType("A") || isType("AAAA") || isType("ANY")) {
|
||||||
|
addCnameAnswer(response, question.name, resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybePrewarm() {
|
||||||
|
if (!PREWARM_DOMAIN) {
|
||||||
|
console.log("[zildns] prewarm disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const started = Date.now();
|
||||||
|
try {
|
||||||
|
await resolveZilDomain(PREWARM_DOMAIN);
|
||||||
|
console.log(`[zildns] prewarm complete for ${PREWARM_DOMAIN} in ${Date.now() - started}ms`);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`[zildns] prewarm failed for ${PREWARM_DOMAIN}:`,
|
||||||
|
err && err.message ? err.message : String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
await maybePrewarm();
|
||||||
|
|
||||||
|
const udpServer = new UDPServer((request, send) => {
|
||||||
|
buildResponse(request)
|
||||||
|
.then((response) => {
|
||||||
|
send(response);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[zildns] request failed:", err.message);
|
||||||
|
const response = makeResponse(request);
|
||||||
|
response.header.rcode = RCODE.SERVFAIL;
|
||||||
|
send(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const tcpServer = new TCPServer((request, send) => {
|
||||||
|
buildResponse(request)
|
||||||
|
.then((response) => {
|
||||||
|
send(response);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("[zildns] request failed:", err.message);
|
||||||
|
const response = makeResponse(request);
|
||||||
|
response.header.rcode = RCODE.SERVFAIL;
|
||||||
|
send(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
udpServer.on("error", (err) => {
|
||||||
|
console.error("[zildns] udp error:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
tcpServer.on("error", (err) => {
|
||||||
|
console.error("[zildns] tcp error:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
udpServer.listen(LISTEN_PORT, LISTEN_HOST);
|
||||||
|
tcpServer.listen(LISTEN_PORT, LISTEN_HOST);
|
||||||
|
|
||||||
|
console.log(`[zildns] listening on ${LISTEN_HOST}:${LISTEN_PORT} (udp/tcp)`);
|
||||||
|
console.log(`[zildns] zns provider ${ZNS_URL} (${ZNS_NETWORK})`);
|
||||||
|
|
||||||
|
return { udpServer, tcpServer };
|
||||||
|
}
|
||||||
|
|
||||||
|
start().catch((err) => {
|
||||||
|
console.error("[zildns] startup failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user