Compare commits
16 Commits
25ca5c1ba4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1936e7b92d | ||
|
|
4a2d11399e | ||
|
|
eb0288313b | ||
|
|
22b302d98d | ||
|
|
7636fcf462 | ||
|
|
3857ae0adb | ||
|
|
a2c33734fb | ||
|
|
b92a02a770 | ||
|
|
f5b9a90c9f | ||
|
|
de85ff494c | ||
|
|
e67738535d | ||
|
|
d8ad812256 | ||
|
|
464644f5d8 | ||
|
|
70d8b1c93c | ||
|
|
9f7a889447 | ||
|
|
466af55640 |
116
GW/authentik,yml
Normal file
116
GW/authentik,yml
Normal file
@@ -0,0 +1,116 @@
|
||||
services:
|
||||
# Init service - generates secrets on first run and outputs them to logs
|
||||
init-secrets:
|
||||
image: alpine:latest
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
if [ ! -f /secrets/.initialized ]; then
|
||||
PG_PASS=$$(head -c 32 /dev/urandom | base64 | tr -d '\n')
|
||||
SECRET_KEY=$$(head -c 64 /dev/urandom | base64 | tr -d '\n')
|
||||
echo "$$PG_PASS" > /secrets/pg_pass
|
||||
echo "$$SECRET_KEY" > /secrets/secret_key
|
||||
touch /secrets/.initialized
|
||||
echo "========================================================"
|
||||
echo " AUTHENTIK SECRETS GENERATED - SAVE THESE!"
|
||||
echo "========================================================"
|
||||
echo "PG_PASS: $$PG_PASS"
|
||||
echo ""
|
||||
echo "AUTHENTIK_SECRET_KEY: $$SECRET_KEY"
|
||||
echo "========================================================"
|
||||
else
|
||||
echo "Secrets already initialized, skipping generation."
|
||||
echo "PG_PASS: $$(cat /secrets/pg_pass)"
|
||||
echo "AUTHENTIK_SECRET_KEY: $$(cat /secrets/secret_key)"
|
||||
fi
|
||||
volumes:
|
||||
- secrets:/secrets
|
||||
restart: "no"
|
||||
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:16-alpine
|
||||
depends_on:
|
||||
init-secrets:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD_FILE: /secrets/pg_pass
|
||||
healthcheck:
|
||||
interval: 30s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- pg_isready -d authentik -U authentik
|
||||
timeout: 5s
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
- secrets:/secrets:ro
|
||||
|
||||
server:
|
||||
# http://<your-server-ip>:9000/if/flow/initial-setup/
|
||||
# dobromir.popov@gateway.one rT42eH5!sGR&4g2X6
|
||||
# mem C379F
|
||||
# MS Entra: memdemo@gateway.one Tufu857515
|
||||
# Auth0 : memdemo@gateway.one diGSKh06z7SkxwpBS
|
||||
|
||||
|
||||
image: ghcr.io/goauthentik/server:2025.10.3
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
init-secrets:
|
||||
condition: service_completed_successfully
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
export AUTHENTIK_SECRET_KEY=$$(cat /secrets/secret_key)
|
||||
export AUTHENTIK_POSTGRESQL__PASSWORD=$$(cat /secrets/pg_pass)
|
||||
exec ak server
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
ports:
|
||||
- 9002:9000
|
||||
- 9443:9443
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./media:/media
|
||||
- ./custom-templates:/templates
|
||||
- secrets:/secrets:ro
|
||||
|
||||
worker:
|
||||
image: ghcr.io/goauthentik/server:2025.10.3
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
init-secrets:
|
||||
condition: service_completed_successfully
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command:
|
||||
- |
|
||||
export AUTHENTIK_SECRET_KEY=$$(cat /secrets/secret_key)
|
||||
export AUTHENTIK_POSTGRESQL__PASSWORD=$$(cat /secrets/pg_pass)
|
||||
exec ak worker
|
||||
environment:
|
||||
AUTHENTIK_POSTGRESQL__HOST: postgresql
|
||||
AUTHENTIK_POSTGRESQL__NAME: authentik
|
||||
AUTHENTIK_POSTGRESQL__USER: authentik
|
||||
restart: unless-stopped
|
||||
user: root
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./media:/media
|
||||
- ./certs:/certs
|
||||
- ./custom-templates:/templates
|
||||
- secrets:/secrets:ro
|
||||
|
||||
volumes:
|
||||
database:
|
||||
driver: local
|
||||
secrets:
|
||||
driver: local
|
||||
46
containers/code-server.yml
Normal file
46
containers/code-server.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
# code-server: VS Code in the browser
|
||||
# Image: linuxserver/code-server - lightweight, s6 overlay, PUID/PGID support
|
||||
# Access: http://<host>:8443 Set PASSWORD in yml or Portainer stack env; if empty, one is generated and saved to /config/.password-generated (and logged on first run).
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
code-server:
|
||||
image: lscr.io/linuxserver/code-server:latest
|
||||
container_name: code-server
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8443:8443"
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Sofia
|
||||
- PASSWORD=
|
||||
- SUDO_PASSWORD=
|
||||
- PROXY_DOMAIN=code-server
|
||||
- DEFAULT_WORKSPACE=/config/workspace
|
||||
volumes:
|
||||
- code-server-config:/config
|
||||
- code-server-workspace:/config/workspace
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
command: >
|
||||
sh -c '
|
||||
if [ -z "$$PASSWORD" ]; then
|
||||
if [ -f /config/.password-generated ]; then
|
||||
export PASSWORD=$$(cat /config/.password-generated);
|
||||
else
|
||||
export PASSWORD=$$(openssl rand -hex 16);
|
||||
echo "$$PASSWORD" > /config/.password-generated;
|
||||
echo "Generated code-server password (saved in /config/.password-generated): $$PASSWORD";
|
||||
fi;
|
||||
fi;
|
||||
HOME=/config git config --global http.postBuffer 524288000;
|
||||
HOME=/config git config --global http.lowSpeedLimit 0;
|
||||
HOME=/config git config --global http.lowSpeedTime 999999;
|
||||
[ -f /config/.gitconfig ] && chown $${PUID}:$${PGID} /config/.gitconfig;
|
||||
exec /init
|
||||
'
|
||||
|
||||
volumes:
|
||||
code-server-config:
|
||||
code-server-workspace:
|
||||
28
containers/novnc-desktop.yml
Normal file
28
containers/novnc-desktop.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
# Lightweight noVNC desktop: Alpine + XFCE4
|
||||
# Image: novaspirit/alpine_xfce4_novnc - minimal footprint
|
||||
# Access: http://<host>:6080/vnc.html (default: alpine/alpine)
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
novnc-desktop:
|
||||
image: novaspirit/alpine_xfce4_novnc:latest
|
||||
container_name: novnc-desktop
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6080:6080"
|
||||
environment:
|
||||
- TZ=Europe/Sofia
|
||||
- VNC_RESOLUTION=1280x720
|
||||
volumes:
|
||||
- novnc-workspace:/headless
|
||||
shm_size: "256m"
|
||||
networks:
|
||||
- default
|
||||
- infrastructure_default
|
||||
|
||||
volumes:
|
||||
novnc-workspace:
|
||||
|
||||
networks:
|
||||
infrastructure_default:
|
||||
external: true
|
||||
32
containers/novnc-py-console.yml
Normal file
32
containers/novnc-py-console.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
novnc-console:
|
||||
image: theasp/novnc:latest
|
||||
container_name: python-console
|
||||
ports:
|
||||
- "8080:8080" # noVNC web interface
|
||||
environment:
|
||||
- DISPLAY_WIDTH=1280
|
||||
- DISPLAY_HEIGHT=720
|
||||
- RUN_XTERM=yes
|
||||
volumes:
|
||||
- ./workspace:/workspace
|
||||
command: >
|
||||
bash -c "
|
||||
apt-get update &&
|
||||
apt-get install -y wget git &&
|
||||
wget -q https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh &&
|
||||
bash /tmp/miniconda.sh -b -p /opt/conda &&
|
||||
rm /tmp/miniconda.sh &&
|
||||
/opt/conda/bin/conda init bash &&
|
||||
exec /app/entrypoint.sh
|
||||
"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- default
|
||||
- infrastructure_default
|
||||
|
||||
networks:
|
||||
infrastructure_default:
|
||||
external: true
|
||||
54
gitea/delete-users-by-creation-date.sh
Normal file
54
gitea/delete-users-by-creation-date.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Mark Gitea users for deletion by creation date (e.g. after a hack/spam wave).
|
||||
# Users created ON or AFTER the cutoff date will be marked inactive; then run
|
||||
# Gitea admin task "Delete all unactivated accounts" to remove them.
|
||||
#
|
||||
# Usage:
|
||||
# ./delete-users-by-creation-date.sh 2025-02-01
|
||||
# ./delete-users-by-creation-date.sh "2025-02-10 14:00"
|
||||
# ./delete-users-by-creation-date.sh 2025-02-01 --whitelist "admin,myuser"
|
||||
#
|
||||
# Steps after running this script:
|
||||
# 1. Backup your Gitea database.
|
||||
# 2. Run the generated SQL against your DB (PostgreSQL or SQLite).
|
||||
# 3. In Gitea: Site Administration -> Maintenance -> "Delete all unactivated accounts".
|
||||
# 4. Consider DISABLE_REGISTRATION = true in app.ini if registration was abused.
|
||||
|
||||
set -e
|
||||
CUTOFF_DATE="${1:?Usage: $0 YYYY-MM-DD [--whitelist user1,user2]}"
|
||||
WHITELIST=""
|
||||
if [[ "$2" == "--whitelist" && -n "$3" ]]; then
|
||||
WHITELIST="$3"
|
||||
fi
|
||||
|
||||
# Convert cutoff to Unix timestamp (GNU date on Linux)
|
||||
CUTOFF_UNIX=$(date -d "$CUTOFF_DATE" +%s 2>/dev/null) || true
|
||||
if [[ -z "$CUTOFF_UNIX" ]]; then
|
||||
echo "Invalid date: $CUTOFF_DATE (use YYYY-MM-DD or \"YYYY-MM-DD HH:MM\")"
|
||||
echo "Run this script on the Gitea server (Linux) or pass the Unix timestamp manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "# Cutoff: $CUTOFF_DATE -> Unix $CUTOFF_UNIX (users created on or after this will be marked inactive)"
|
||||
echo "# Backup your database before running any of the below."
|
||||
echo ""
|
||||
|
||||
if [[ -n "$WHITELIST" ]]; then
|
||||
# Build NOT IN list for SQL
|
||||
WL=$(echo "$WHITELIST" | sed "s/,/','/g" | sed "s/^/'/; s/$/'/")
|
||||
WHERE_EXTRA=" AND name NOT IN ($WL)"
|
||||
WHERE_EXTRA_PG=" AND name NOT IN ($WL)"
|
||||
else
|
||||
WHERE_EXTRA=""
|
||||
WHERE_EXTRA_PG=""
|
||||
fi
|
||||
|
||||
echo "# --- PostgreSQL (table: public.user) ---"
|
||||
echo "UPDATE public.user SET is_active = false WHERE created_unix >= $CUTOFF_UNIX${WHERE_EXTRA_PG};"
|
||||
echo ""
|
||||
|
||||
echo "# --- SQLite / MySQL (table: user) ---"
|
||||
echo "UPDATE user SET is_active = 0 WHERE created_unix >= $CUTOFF_UNIX${WHERE_EXTRA};"
|
||||
echo ""
|
||||
|
||||
echo "# Then in Gitea: Site Administration -> Maintenance -> 'Delete all unactivated accounts'"
|
||||
48
gitea/fix-postgres-collation-version.sh
Normal file
48
gitea/fix-postgres-collation-version.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fix PostgreSQL "collation version mismatch" (e.g. DB created with 2.36, OS provides 2.41).
|
||||
# Reindexes then refreshes collation version. REINDEX will be canceled if Gitea (or other
|
||||
# clients) keep using the DB, so either stop Gitea first or use --terminate-connections.
|
||||
#
|
||||
# Usage (from host):
|
||||
# ./fix-postgres-collation-version.sh <postgres_container_name>
|
||||
# ./fix-postgres-collation-version.sh <postgres_container_name> gitea
|
||||
# ./fix-postgres-collation-version.sh <postgres_container_name> gitea --terminate-connections
|
||||
#
|
||||
# Option: --terminate-connections Terminate all other sessions on the DB so REINDEX can
|
||||
# run without being canceled. Gitea will disconnect; restart it after.
|
||||
|
||||
|
||||
# REINDEX DATABASE gitea;
|
||||
# ALTER DATABASE gitea REFRESH COLLATION VERSION;
|
||||
|
||||
|
||||
set -e
|
||||
CONTAINER="${1:?Usage: $0 <postgres_container_name> [database_name] [--terminate-connections]}"
|
||||
DB="${2:-gitea}"
|
||||
TERMINATE=""
|
||||
if [[ "$2" == "--terminate-connections" ]]; then
|
||||
TERMINATE=1
|
||||
DB="gitea"
|
||||
elif [[ "$3" == "--terminate-connections" ]]; then
|
||||
TERMINATE=1
|
||||
fi
|
||||
|
||||
if [[ -z "$TERMINATE" ]]; then
|
||||
echo "Stop the Gitea container (and any other app using database $DB) before continuing,"
|
||||
echo "otherwise REINDEX may be canceled. Press Enter when ready, or Ctrl+C to abort."
|
||||
read -r
|
||||
fi
|
||||
|
||||
if [[ -n "$TERMINATE" ]]; then
|
||||
echo "Terminating other connections to $DB..."
|
||||
docker exec "$CONTAINER" psql -U postgres -d "$DB" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB' AND pid <> pg_backend_pid();" || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Reindexing database $DB (may take a while)..."
|
||||
docker exec "$CONTAINER" psql -U postgres -d "$DB" -c "REINDEX DATABASE $DB;"
|
||||
|
||||
echo "Refreshing collation version..."
|
||||
docker exec "$CONTAINER" psql -U postgres -d "$DB" -c "ALTER DATABASE $DB REFRESH COLLATION VERSION;"
|
||||
|
||||
echo "Done. Start Gitea again if you stopped it or used --terminate-connections."
|
||||
18
linux/antivirus.sh
Normal file
18
linux/antivirus.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
# Install ClamAV
|
||||
sudo apt install clamav clamav-daemon
|
||||
|
||||
# Update signatures
|
||||
sudo freshclam
|
||||
|
||||
# Scan Docker volumes
|
||||
sudo clamscan -r -i /var/lib/docker/volumes/
|
||||
|
||||
# Install Lynis
|
||||
cd /opt/
|
||||
sudo wget https://downloads.cisofy.com/lynis/lynis-3.1.6.tar.gz
|
||||
sudo tar xvzf lynis-3.1.6.tar.gz
|
||||
sudo mv lynis /usr/local/
|
||||
sudo ln -s /usr/local/lynis/lynis /usr/local/bin/lynis
|
||||
|
||||
# Run security audit
|
||||
sudo lynis audit system
|
||||
113
linux/docker-host-setup.sh
Normal file
113
linux/docker-host-setup.sh
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup a fresh Linux server as Docker host with Portainer and Nginx Proxy Manager.
|
||||
# Run over SSH: copy script to server, then: chmod +x docker-host-setup.sh && sudo ./docker-host-setup.sh
|
||||
#
|
||||
# Optional env (set before running):
|
||||
# DOCKER_DATA_ROOT - e.g. /mnt/data/docker (default: leave Docker's default)
|
||||
# VOL_BASE - volume base for portainer and proxy (default: /opt/docker-vol)
|
||||
|
||||
# usage: sudo DOCKER_DATA_ROOT=/mnt/data/docker VOL_BASE=/mnt/data/docker-vol ./docker-host-setup.sh
|
||||
set -e
|
||||
|
||||
VOL_BASE="${VOL_BASE:-/opt/docker-vol}"
|
||||
DOCKER_DATA_ROOT="${DOCKER_DATA_ROOT:-}"
|
||||
|
||||
# --- 1. Detect OS and install Docker ---
|
||||
if ! command -v docker &>/dev/null; then
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
else
|
||||
echo "Cannot detect OS. Install Docker manually and re-run for Portainer/NPM only."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then
|
||||
echo "This script supports Ubuntu/Debian. For other distros install Docker and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Docker (Ubuntu/Debian)..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq ca-certificates curl gnupg
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/"$ID"/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$ID $VERSION_CODENAME stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
fi
|
||||
|
||||
# --- 2. Optional custom Docker data root ---
|
||||
if [ -n "$DOCKER_DATA_ROOT" ]; then
|
||||
echo "Setting Docker data-root to $DOCKER_DATA_ROOT"
|
||||
mkdir -p "$DOCKER_DATA_ROOT"
|
||||
if [ -f /etc/docker/daemon.json ] && grep -q '"data-root"' /etc/docker/daemon.json; then
|
||||
echo "Docker daemon.json already has data-root, skipping."
|
||||
else
|
||||
if [ -f /etc/docker/daemon.json ]; then
|
||||
if command -v python3 &>/dev/null; then
|
||||
python3 -c "
|
||||
import json, sys
|
||||
p = '/etc/docker/daemon.json'
|
||||
with open(p) as f: d = json.load(f)
|
||||
d['data-root'] = sys.argv[1]
|
||||
with open(p, 'w') as f: json.dump(d, f, indent=2)
|
||||
" "$DOCKER_DATA_ROOT"
|
||||
else
|
||||
echo "Add manually to /etc/docker/daemon.json: \"data-root\": \"$DOCKER_DATA_ROOT\""
|
||||
fi
|
||||
else
|
||||
printf '%s\n' "{\"data-root\": \"$DOCKER_DATA_ROOT\"}" > /etc/docker/daemon.json
|
||||
fi
|
||||
fi
|
||||
systemctl restart docker
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
|
||||
# Add current user to docker group so they can run docker without sudo
|
||||
if [ -n "${SUDO_USER:-}" ]; then
|
||||
usermod -aG docker "$SUDO_USER"
|
||||
echo "Added $SUDO_USER to group docker. Log out and back in (or newgrp docker) for it to take effect."
|
||||
fi
|
||||
|
||||
# --- 3. Volume dirs for Portainer and Nginx Proxy Manager ---
|
||||
mkdir -p "$VOL_BASE/portainer" "$VOL_BASE/proxy/data" "$VOL_BASE/proxy/letsencrypt"
|
||||
echo "Volume base: $VOL_BASE"
|
||||
|
||||
# --- 4. Start Portainer ---
|
||||
if ! docker ps -a --format '{{.Names}}' | grep -qx portainer; then
|
||||
echo "Starting Portainer..."
|
||||
docker run -d \
|
||||
-p 8000:8000 -p 9000:9000 \
|
||||
--name portainer --restart always \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "$VOL_BASE/portainer:/data" \
|
||||
portainer/portainer-ce:latest
|
||||
echo "Portainer: http://<host>:9000 (set admin password on first visit)"
|
||||
else
|
||||
echo "Portainer container already exists. Start with: docker start portainer"
|
||||
fi
|
||||
|
||||
# --- 5. Start Nginx Proxy Manager ---
|
||||
if ! docker ps -a --format '{{.Names}}' | grep -qx proxy; then
|
||||
echo "Starting Nginx Proxy Manager..."
|
||||
docker run -d \
|
||||
--name proxy --restart always \
|
||||
-p 80:80 -p 443:443 -p 81:81 \
|
||||
-p 2222:2222 \
|
||||
-e PUID=0 -e PGID=0 \
|
||||
-v "$VOL_BASE/proxy/data:/data" \
|
||||
-v "$VOL_BASE/proxy/letsencrypt:/etc/letsencrypt" \
|
||||
jc21/nginx-proxy-manager:latest
|
||||
echo "NPM admin: http://<host>:81 (default login: admin@example.com / changeme)"
|
||||
else
|
||||
echo "Nginx Proxy Manager container already exists. Start with: docker start proxy"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. Portainer: :9000 NPM admin: :81 NPM HTTP/HTTPS: :80 :443"
|
||||
echo "Change NPM default password and Portainer admin password after first login."
|
||||
4
linux/openwrt/hotplug-iface-99-starlink-policy
Normal file
4
linux/openwrt/hotplug-iface-99-starlink-policy
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Copy to router: /etc/hotplug.d/iface/99-starlink-policy
|
||||
# chmod +x /etc/hotplug.d/iface/99-starlink-policy
|
||||
[ "$INTERFACE" = "wan2" ] && [ "$ACTION" = "ifup" ] && /etc/starlink-policy-route.sh setup
|
||||
223
linux/openwrt/openwrt-starlink-luci-setup.md
Normal file
223
linux/openwrt/openwrt-starlink-luci-setup.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# OpenWrt: Connect to Starlink WiFi (client) and route blocked sites via it (LuCI)
|
||||
|
||||
Step-by-step using the LuCI web UI where possible. Router: Archer C6, OpenWrt/LuCI. Goal: main WAN stays default; traffic to polymarket (and similar) goes via Starlink WiFi.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Connect router to Starlink WiFi (client mode)
|
||||
|
||||
You need one radio as **AP** (your LAN WiFi) and one as **Client** (Starlink). Archer C6 has 2.4 GHz and 5 GHz; use one for Starlink client.
|
||||
|
||||
### 1.1 Install WiFi client (if needed)
|
||||
|
||||
SSH into the router, then:
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install wpad-mesh-openssl
|
||||
```
|
||||
|
||||
(Some images already include this. If "Scan" works in LuCI, skip.)
|
||||
|
||||
### 1.2 Create the Starlink client interface in LuCI
|
||||
|
||||
1. Log in to LuCI (e.g. `http://192.168.0.1`).
|
||||
2. Go to **Network** → **Wireless**.
|
||||
3. You should see two radios (e.g. "Radio0 (2.4 GHz)", "Radio1 (5 GHz)").
|
||||
4. On the radio you will use for Starlink (e.g. **Radio1 (5 GHz)**):
|
||||
- Click **Scan**.
|
||||
- Wait for the list; find your **Starlink WiFi SSID**.
|
||||
- Click **Join network** next to it.
|
||||
5. In the dialog:
|
||||
- **Network**: leave as new (e.g. `wwan`) or set a name like `starlink`.
|
||||
- **Wireless Security**: choose the encryption (usually **WPA2-PSK**) and enter the **Starlink WiFi password**.
|
||||
- Leave other options default. Submit.
|
||||
6. The new interface (e.g. `wwan` or `starlink`) appears under **Network** → **Wireless** as a **Client** network. Ensure it is **Enabled** and not disabled.
|
||||
|
||||
### 1.3 Create a WAN interface for Starlink and assign firewall
|
||||
|
||||
The client connection gets an IP via DHCP from Starlink. You must create a protocol interface for it and put it in the **wan** firewall zone so it is used as a WAN.
|
||||
|
||||
1. Go to **Network** → **Interfaces**.
|
||||
2. Click **Add new interface**:
|
||||
- **Name**: `wan2` (or `starlink`).
|
||||
- **Protocol**: **DHCP client**.
|
||||
- **Device**: select the device that corresponds to the Starlink client (e.g. `wwan` or the wireless device name shown for that client network). If unsure, check **Network** → **Wireless** and see which device the client is on (e.g. `wlan1`).
|
||||
- Submit.
|
||||
3. On the new interface’s page:
|
||||
- **General Setup**: ensure "Bring up on boot" or similar is checked.
|
||||
- **Firewall Settings**: assign to **wan** (same zone as your main WAN). Required for NAT and for policy routing (mwan3 or the script in Part 2b).
|
||||
- **Save & Apply**.
|
||||
|
||||
### 1.4 Verify Starlink connectivity
|
||||
|
||||
- In **Network** → **Interfaces**, `wan2` should show an IP (from Starlink’s DHCP).
|
||||
- From a device on your LAN, you can ping 8.8.8.8 (main WAN is still default). To test Starlink alone you’ll confirm after Part 2 with a policy.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Install and configure mwan3 (Load Balancing)
|
||||
|
||||
mwan3 will use both WANs: default traffic via main WAN, and specific destination IPs (polymarket) via Starlink.
|
||||
|
||||
### 2.1 Install mwan3 (SSH)
|
||||
|
||||
LuCI app for mwan3 is not always preinstalled. On the router via SSH:
|
||||
|
||||
```bash
|
||||
opkg update
|
||||
opkg install mwan3 luci-app-mwan3
|
||||
```
|
||||
|
||||
Then in LuCI you should see **Network** → **Load Balancing** (or **Multi-WAN**).
|
||||
|
||||
### 2.2 Configure interfaces (LuCI)
|
||||
|
||||
1. Go to **Network** → **Load Balancing** → **Configuration** (or **Interfaces** tab).
|
||||
2. **Interfaces**:
|
||||
- You should see **wan** (main) and **wan2** (Starlink). If not, add **wan2**:
|
||||
- **Interface**: `wan2`
|
||||
- **Enable**: checked
|
||||
- **Track IP**: e.g. `8.8.8.8` or `1.1.1.1` (used for health check).
|
||||
- **Metric**: `20` (higher than wan so default route prefers main WAN).
|
||||
- **Reliability**: e.g. `1`.
|
||||
- Save.
|
||||
- For **wan** (main WAN):
|
||||
- **Metric**: `10` (lower = preferred for default).
|
||||
- **Track IP**: e.g. `8.8.8.8`.
|
||||
- Save.
|
||||
3. **Members** tab:
|
||||
- **wan** → member e.g. `wan_m1`, metric `1`.
|
||||
- **wan2** → member e.g. `wan2_m1`, metric `1`.
|
||||
4. **Policies** tab:
|
||||
- **default_policy**: last resort; assign only **wan_m1** (main WAN only). So all traffic that doesn’t match a rule uses main WAN.
|
||||
- Add policy **starlink_only**: assign only **wan2_m1**. This will be used for polymarket IPs.
|
||||
5. **Rules** tab (order matters; more specific first):
|
||||
- Add a rule for polymarket:
|
||||
- **Name**: e.g. `polymarket_via_starlink`
|
||||
- **Destination address**: see below (polymarket IPs). You can add one rule with multiple IPs/CIDRs or several rules.
|
||||
- **Policy**: **starlink_only**
|
||||
- **Sticky**: optional (e.g. 1 minute) so the same connection stays on Starlink.
|
||||
- Ensure there is a **default** rule:
|
||||
- **Destination address**: `0.0.0.0/0`
|
||||
- **Policy**: **default_policy**
|
||||
- Default rule must be **last** (lowest priority). Polymarket rule must be **above** it.
|
||||
|
||||
### 2.3 Polymarket destination IPs
|
||||
|
||||
mwan3 matches by **destination IP**, not domain. You need to add the IPs (or CIDRs) for polymarket.com and any related hostnames.
|
||||
|
||||
- Resolve from a PC (that can reach polymarket, or use any DNS):
|
||||
- `nslookup polymarket.com`
|
||||
- `nslookup www.polymarket.com`
|
||||
- Add any other subdomains you use (e.g. `gamma-api.polymarket.com`).
|
||||
- In LuCI **Load Balancing** → **Rules**, in the polymarket rule set **Destination address** to one of:
|
||||
- Single IP: `a.b.c.d/32`
|
||||
- Several IPs: add multiple rules with the same policy, or use a space-separated list if LuCI allows (e.g. `1.2.3.4/32 5.6.7.8/32`).
|
||||
- CDN IPs can change. If the site stops working via Starlink, resolve the domains again and add/update the IPs in the rule. You can later automate this with a script that updates the mwan3 config or uses ipset.
|
||||
|
||||
**Example** (replace with real IPs you resolved):
|
||||
|
||||
- Destination address: `104.18.2.2/32 172.67.1.1/32` (example only; get real IPs for polymarket.com).
|
||||
|
||||
### 2.4 Save and apply
|
||||
|
||||
- **Save & Apply** in **Load Balancing** and in **Network** → **Interfaces** if you changed anything.
|
||||
- Test: from a LAN device, open polymarket.com; it should go via Starlink. Other sites still via main WAN.
|
||||
|
||||
---
|
||||
|
||||
## Part 2b: Policy routing without mwan3 (low flash)
|
||||
|
||||
If you cannot install mwan3 (e.g. only ~80 KB free on flash), you can get the same behaviour using **ip rules** and a **custom routing table**. No extra packages: uses `ip`, `resolveip`, `ubus`, `jsonfilter` (all default on OpenWrt).
|
||||
|
||||
### 2b.1 Copy and run the script
|
||||
|
||||
1. Copy `starlink-policy-route.sh` to the router (e.g. `/etc/starlink-policy-route.sh`).
|
||||
2. Make it executable: `chmod +x /etc/starlink-policy-route.sh`.
|
||||
3. Run once when wan2 is up: `/etc/starlink-policy-route.sh setup`.
|
||||
|
||||
The script resolves `polymarket.com` and `www.polymarket.com` (via `resolveip`), gets wan2 gateway from `ubus`, adds a default route in table 100 via wan2, and adds `ip rule add to <ip> table 100` for each resolved IP. All other traffic keeps using the main WAN.
|
||||
|
||||
### 2b.2 Run on wan2 up (hotplug)
|
||||
|
||||
So routes are applied after Starlink (wan2) gets an IP, create a hotplug script:
|
||||
|
||||
```bash
|
||||
# On router: create /etc/hotplug.d/iface/99-starlink-policy
|
||||
#!/bin/sh
|
||||
[ "$INTERFACE" = "wan2" ] && [ "$ACTION" = "ifup" ] && /etc/starlink-policy-route.sh setup
|
||||
```
|
||||
|
||||
Make it executable: `chmod +x /etc/hotplug.d/iface/99-starlink-policy`.
|
||||
|
||||
### 2b.3 Optional: set IPs manually
|
||||
|
||||
If DNS is not ready when the script runs (e.g. wan2 up before main WAN), resolve the domains on a PC and set them in the script:
|
||||
|
||||
```bash
|
||||
# In starlink-policy-route.sh set (replace with real IPs):
|
||||
POLYMARKET_IPS="104.18.2.2 172.67.1.1"
|
||||
```
|
||||
|
||||
Then the script skips `resolveip` and uses these IPs. Update them if the site stops working (CDN changes).
|
||||
|
||||
### 2b.4 Remove routes
|
||||
|
||||
To remove the policy routes: `/etc/starlink-policy-route.sh remove`.
|
||||
|
||||
---
|
||||
|
||||
## Part 2c: Policy routing via LuCI only (static routes)
|
||||
|
||||
You can achieve the same result **entirely in LuCI** without mwan3 or scripts: (1) override the hostname so DNS returns the **real** Polymarket IP (not your ISP’s spoofed one), and (2) add a **static route** for that IP via wan2. Route type: **unicast** (default).
|
||||
|
||||
### 2c.1 Get the real IP
|
||||
|
||||
Your ISP may resolve polymarket.com to a block/fake IP. You need the **real** server IP. Resolve while connected to Starlink WiFi (or using DNS 8.8.8.8): `nslookup polymarket.com` and `nslookup www.polymarket.com`. Note the IPv4 address (e.g. `64.239.109.1`). Use that for the route and the hostname override below.
|
||||
|
||||
### 2c.2 Override the hostname in LuCI (so DNS returns the real IP)
|
||||
|
||||
So your LAN devices don’t get the ISP-spoofed IP:
|
||||
|
||||
1. Go to **Network** → **Hostnames** (or **DHCP and DNS** → **Hostnames** / **Custom domain** / **Address** entries, depending on your LuCI).
|
||||
2. Add an entry: **Hostname** `polymarket.com`, **IP** the real IP (e.g. `64.239.109.1`). Add another for `www.polymarket.com` with the same IP if needed.
|
||||
3. **Save & Apply**.
|
||||
|
||||
### 2c.3 Add static route in LuCI
|
||||
|
||||
1. Go to **Network** → **Routes** (or **Static routes**).
|
||||
2. Click **Add** (or **Add new IPv4 route**).
|
||||
3. Set:
|
||||
- **Target**: the real IP with `/32` (e.g. `64.239.109.1/32`).
|
||||
- **Gateway**: use the **Starlink (wan2) gateway** — either choose **Use gateway from interface** and select **wan2**, or enter the **wan2 DHCP gateway IP** (the IP your router uses as default gateway on the Starlink side; see **Network** → **Interfaces** → wan2 for the gateway).
|
||||
- **Type**: **unicast** (default).
|
||||
- **Metric**: leave default.
|
||||
4. **Save & Apply**.
|
||||
|
||||
Traffic to that IP now goes via wan2; DNS gives your devices the real IP, so the route is used. If the site stops working later, re-resolve the domain (e.g. on Starlink), update the hostname override and the route if the IP changed.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Quick reference (LuCI locations)
|
||||
|
||||
| Step | LuCI path |
|
||||
|-------------------------|-------------------------------------|
|
||||
| Create Starlink client | Network → Wireless → Scan → Join |
|
||||
| WAN interface for WiFi | Network → Interfaces → Add (DHCP, wan zone) |
|
||||
| Load Balancing config | Network → Load Balancing |
|
||||
| Interfaces (wan, wan2) | Load Balancing → Interfaces |
|
||||
| Policies | Load Balancing → Policies |
|
||||
| Rules (polymarket, default) | Load Balancing → Rules |
|
||||
| Policy without mwan3 | Part 2b: script + hotplug |
|
||||
| Policy via LuCI only | Part 2c: Hostnames + Network → Routes |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Starlink client not getting IP**: Check WiFi password; ensure Starlink router is in range; check **Network** → **Wireless** that the client network is enabled and associated.
|
||||
- **All traffic still via main WAN**: Ensure the polymarket rule is **above** the default rule; check **Destination address** uses the correct IPs/CIDRs; ensure **starlink_only** policy uses only **wan2_m1**.
|
||||
- **Polymarket works then stops**: CDN IPs changed; re-resolve the domain(s) and update the rule’s destination IPs.
|
||||
- **LuCI "Load Balancing" missing**: Install `luci-app-mwan3` via SSH and refresh the page.
|
||||
- **Using Part 2b (no mwan3)**: If polymarket stops working, CDN IPs may have changed; run `resolveip -4 polymarket.com` (or from a PC) and set `POLYMARKET_IPS` in the script, or ensure the script runs when DNS is available (e.g. after both WANs are up).
|
||||
83
linux/openwrt/policy-route-starlink.sh
Normal file
83
linux/openwrt/policy-route-starlink.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# Policy routing: send traffic to specified domains via Starlink interface.
|
||||
# Run on Linux host (e.g. Linux Mint) that has both main connection and WiFi to Starlink.
|
||||
# Usage: sudo ./policy-route-starlink.sh [setup|remove]
|
||||
# Configure STARLINK_IF, STARLINK_GW, DOMAINS below or via env.
|
||||
|
||||
set -e
|
||||
|
||||
# Starlink WiFi interface (e.g. wlan0)
|
||||
STARLINK_IF="${STARLINK_IF:-wlan0}"
|
||||
# Starlink gateway IP (get from: ip route show dev "$STARLINK_IF" | head -1)
|
||||
STARLINK_GW="${STARLINK_GW:-}"
|
||||
# Domains to route via Starlink (space-separated)
|
||||
DOMAINS="${DOMAINS:-polymarket.com www.polymarket.com}"
|
||||
# Routing table id for Starlink
|
||||
TABLE_ID=200
|
||||
|
||||
action="${1:-setup}"
|
||||
|
||||
resolve_domains() {
|
||||
local list=""
|
||||
for d in $DOMAINS; do
|
||||
local ips
|
||||
ips=$(getent ahosts "$d" 2>/dev/null | awk '$1 !~ /^:/ {print $1}' | sort -u) || true
|
||||
for ip in $ips; do
|
||||
[[ -n "$ip" ]] && list="$list $ip"
|
||||
done
|
||||
done
|
||||
echo "$list"
|
||||
}
|
||||
|
||||
get_starlink_gw() {
|
||||
if [[ -n "$STARLINK_GW" ]]; then
|
||||
echo "$STARLINK_GW"
|
||||
return
|
||||
fi
|
||||
ip route show dev "$STARLINK_IF" 2>/dev/null | awk '/default via/ {print $3; exit}'
|
||||
}
|
||||
|
||||
do_setup() {
|
||||
if ! ip link show "$STARLINK_IF" &>/dev/null; then
|
||||
echo "Interface $STARLINK_IF not found. Set STARLINK_IF (e.g. wlan0)."
|
||||
exit 1
|
||||
fi
|
||||
local gw
|
||||
gw=$(get_starlink_gw)
|
||||
if [[ -z "$gw" ]]; then
|
||||
echo "Could not determine Starlink gateway. Set STARLINK_GW or ensure $STARLINK_IF has a route."
|
||||
exit 1
|
||||
fi
|
||||
# Ensure table 200 has default via Starlink (idempotent)
|
||||
ip route replace default via "$gw" dev "$STARLINK_IF" table "$TABLE_ID" 2>/dev/null || true
|
||||
local ips
|
||||
ips=$(resolve_domains)
|
||||
if [[ -z "$ips" ]]; then
|
||||
echo "No IPs resolved for domains: $DOMAINS. Check DNS/connectivity."
|
||||
exit 1
|
||||
fi
|
||||
for ip in $ips; do
|
||||
ip rule add to "$ip" table "$TABLE_ID" 2>/dev/null || true
|
||||
done
|
||||
echo "Routing via Starlink ($STARLINK_IF): $ips"
|
||||
}
|
||||
|
||||
do_remove() {
|
||||
local ips
|
||||
ips=$(resolve_domains)
|
||||
for ip in $ips; do
|
||||
ip rule del to "$ip" table "$TABLE_ID" 2>/dev/null || true
|
||||
done
|
||||
ip route flush table "$TABLE_ID" 2>/dev/null || true
|
||||
echo "Removed policy rules for table $TABLE_ID."
|
||||
}
|
||||
|
||||
case "$action" in
|
||||
setup) do_setup ;;
|
||||
remove) do_remove ;;
|
||||
*)
|
||||
echo "Usage: $0 [setup|remove]"
|
||||
echo "Env: STARLINK_IF ($STARLINK_IF), STARLINK_GW, DOMAINS"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
120
linux/openwrt/policy-route-via-starlink.md
Normal file
120
linux/openwrt/policy-route-via-starlink.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Policy routing: send blocked sites via Starlink (WiFi)
|
||||
|
||||
Your main connection blocks some sites (e.g. polymarket.com). Starlink is available over WiFi. This routes only selected traffic via Starlink; the rest stays on the main link.
|
||||
|
||||
## Where to implement
|
||||
|
||||
| Place | When to use |
|
||||
|-------|-------------|
|
||||
| **OpenWrt router** | Starlink is a second WAN on the same router. One config, all LAN devices benefit. |
|
||||
| **Linux host (Mint)** | Starlink is only reachable from this machine (e.g. WiFi to Starlink, Ethernet to main LAN). |
|
||||
| **Docker** | No separate step. Containers use the host’s routing; fix it on the host (or router). |
|
||||
|
||||
So: **router** if Starlink is second WAN on OpenWrt; otherwise **Linux host**. Docker follows the host.
|
||||
|
||||
---
|
||||
|
||||
## Router as WiFi client to Starlink
|
||||
|
||||
Using the router to connect to Starlink’s WiFi as a client gives you one device with two WANs (main + Starlink over WiFi). Then policy routing can send only blocked sites via Starlink.
|
||||
|
||||
**Stock TP-Link (e.g. Archer C6):**
|
||||
Most stock firmwares do **not** support “connect to another WiFi as client and use it as a **second** WAN”. They may have “WISP” / “Wireless ISP” mode, which uses WiFi-as-WAN but typically **replaces** the main WAN, not adds a second one. So dual-WAN with one being WiFi client is usually **not** available on stock.
|
||||
|
||||
**OpenWrt:**
|
||||
Supports this. You use one wireless interface in **Client** mode, connected to Starlink’s SSID (and password). That interface gets an IP via DHCP from Starlink and acts as a second WAN. Your existing Ethernet WAN stays the first. Requirements:
|
||||
|
||||
- Router has OpenWrt installed (Archer C6 is supported; check [OpenWrt Table of Hardware](https://openwrt.org/toh/start)).
|
||||
- Two wireless “sides”: one stays in AP mode for your LAN WiFi, the other is in **Client** mode to Starlink. On dual-band routers (e.g. 2.4 GHz + 5 GHz) you use one band for AP and the other for client, so both can run at once.
|
||||
- Then configure mwan3 with two WANs and policy routing as in Option A. **Step-by-step LuCI guide:** see `openwrt-starlink-luci-setup.md`.
|
||||
|
||||
So: **yes, you can configure the router to connect to Starlink as a client**, but you need **OpenWrt** (or similar) to both join Starlink WiFi and use it as a second WAN next to your main connection.
|
||||
|
||||
---
|
||||
|
||||
## Option A: OpenWrt (router level)
|
||||
|
||||
Requirements:
|
||||
|
||||
- OpenWrt with two WANs: main (blocking) + Starlink.
|
||||
- Starlink connected to OpenWrt: **Ethernet** to Starlink router, or **WiFi client** (router joins Starlink’s WiFi as above).
|
||||
|
||||
Steps (short):
|
||||
|
||||
1. **Multi-WAN**: Install `mwan3`, configure two interfaces (e.g. `wan`, `wan2`), each with its gateway and metric.
|
||||
2. **Policy**: In mwan3, add a policy that uses only the Starlink member for a specific rule.
|
||||
3. **Matching traffic**:
|
||||
- Either assign **source IP** of the Linux host (and optionally other devices) to use that policy, or
|
||||
- Use **destination IP** (see “Domain → IP” below) in firewall/routing so only those IPs go via Starlink.
|
||||
|
||||
Domain → IP on OpenWrt: resolve the domain (e.g. via `nslookup polymarket.com` or a script), then add those IPs to a firewall fwmark or an mwan3 rule. Some use `dnsmasq` with `ipset` + firewall to mark by domain and then mwan3 routes by mark.
|
||||
|
||||
---
|
||||
|
||||
## Option B: Linux host (Mint) – two interfaces
|
||||
|
||||
Your machine has:
|
||||
|
||||
- Main: e.g. Ethernet (default route, blocking).
|
||||
- Starlink: WiFi to Starlink.
|
||||
|
||||
Idea: keep default route on main; add a second routing table whose default is via Starlink; use `ip rule` so that traffic to specific IPs (resolved from polymarket.com etc.) uses that table.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Identify interfaces and gateways**
|
||||
- Main: `ip route show default` (e.g. `eth0`, gateway `192.168.0.1`).
|
||||
- Starlink: connect WiFi, then `ip route` and note gateway on `wlan0` (e.g. `192.168.1.1`).
|
||||
|
||||
2. **Starlink routing table**
|
||||
- Pick a table id, e.g. `200`.
|
||||
- Add default via Starlink gateway in table 200 (see script).
|
||||
|
||||
3. **Which IPs to send via Starlink**
|
||||
- Resolve domains (e.g. `polymarket.com`, `www.polymarket.com`, `gamma-api.polymarket.com` if needed). IPs can change (CDN), so either:
|
||||
- Run a small script periodically (cron) that resolves domains and updates `ip rule`/routing, or
|
||||
- Add a known set of IPs and update when blocking starts again.
|
||||
|
||||
4. **Rules**
|
||||
- `ip rule add to <IP> table 200` for each IP (or use `ipset` + one rule `ip rule add to match set <setname> table 200`).
|
||||
|
||||
Use the script `policy-route-starlink.sh`: it wraps the above and can be run at boot and on a timer.
|
||||
|
||||
**Script usage (host):**
|
||||
```bash
|
||||
# One-time: set gateway if auto-detect fails
|
||||
export STARLINK_IF=wlan0
|
||||
export STARLINK_GW=192.168.1.1 # optional, else from default route on wlan0
|
||||
export DOMAINS="polymarket.com www.polymarket.com" # optional
|
||||
sudo ./policy-route-starlink.sh setup
|
||||
# To remove: sudo ./policy-route-starlink.sh remove
|
||||
```
|
||||
Because CDN IPs can change, run `setup` after boot (e.g. systemd service or @reboot cron) and optionally every 10–15 min via cron so new IPs get added.
|
||||
|
||||
---
|
||||
|
||||
## Option C: Docker
|
||||
|
||||
No extra layer. Containers use the host’s routing and DNS. Once policy routing works on the host (or router), traffic from Docker to polymarket.com will go via Starlink if the rule matches that traffic (same destination IPs).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Router (OpenWrt):** Yes, if Starlink is second WAN; use mwan3 + policy + (optionally) domain→ipset.
|
||||
- **Host (Linux Mint):** Yes; two interfaces, second routing table, `ip rule` for destination IPs of blocked domains; script can maintain IP list.
|
||||
- **Docker:** No separate config; host (or router) handles it.
|
||||
|
||||
If only the Linux Mint box has Starlink WiFi, implement on the host with the script. If Starlink is a second WAN on OpenWrt, implement on the router.
|
||||
|
||||
---
|
||||
|
||||
## Pi-hole DNS on the Linux host
|
||||
|
||||
**Does Pi-hole help route blocked sites through Starlink?**
|
||||
No. Pi-hole only does DNS (answers and forwarding). It does not decide which WAN is used for the actual traffic. Routing is done by the kernel (policy routing / mwan3).
|
||||
|
||||
- **If the block is DNS-only** (ISP DNS returns NXDOMAIN or a block page): Using a different DNS (e.g. Pi-hole with upstream 1.1.1.1 / 8.8.8.8) can give clients the real IP. Traffic still goes out the main WAN; if the ISP also blocks by IP or SNI, you still need policy routing.
|
||||
- **If the block is IP/SNI/DPI**: You need policy routing so traffic to polymarket’s IPs goes via Starlink. Pi-hole does not do that.
|
||||
|
||||
Pi-hole is useful for ad blocking and DNS control; use it together with policy routing (OpenWrt or host script), not as a substitute for it.
|
||||
82
linux/openwrt/starlink-policy-route.sh
Normal file
82
linux/openwrt/starlink-policy-route.sh
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/bin/sh
|
||||
# Policy routing: send traffic to specified domains via Starlink (wan2).
|
||||
# No mwan3 required. Uses ip rule + custom table. Run on OpenWrt.
|
||||
# Usage: run from hotplug (wan2 ifup) or manually after wan2 is up.
|
||||
# Requires: ip, resolveip, ubus, jsonfilter (all default on OpenWrt).
|
||||
|
||||
TABLE_ID=100
|
||||
WAN2_IF=wan2
|
||||
DOMAINS="polymarket.com www.polymarket.com"
|
||||
|
||||
# Optional: set IPs manually if resolveip fails (e.g. no DNS yet). Space-separated.
|
||||
# POLYMARKET_IPS="1.2.3.4 5.6.7.8"
|
||||
POLYMARKET_IPS=""
|
||||
|
||||
get_wan2_gw() {
|
||||
local status
|
||||
status=$(ubus call network.interface."$WAN2_IF" status 2>/dev/null) || return 1
|
||||
echo "$status" | jsonfilter -e '@.route[0].nexthop' 2>/dev/null
|
||||
}
|
||||
|
||||
get_wan2_dev() {
|
||||
local status
|
||||
status=$(ubus call network.interface."$WAN2_IF" status 2>/dev/null) || return 1
|
||||
echo "$status" | jsonfilter -e '@.device' 2>/dev/null
|
||||
}
|
||||
|
||||
resolve_domains() {
|
||||
if [ -n "$POLYMARKET_IPS" ]; then
|
||||
echo "$POLYMARKET_IPS"
|
||||
return
|
||||
fi
|
||||
local list=""
|
||||
for d in $DOMAINS; do
|
||||
for ip in $(resolveip -4 -t 2 "$d" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'); do
|
||||
[ -n "$ip" ] && list="$list $ip"
|
||||
done
|
||||
done
|
||||
echo "$list" | tr ' ' '\n' | sort -u | tr '\n' ' '
|
||||
}
|
||||
|
||||
do_setup() {
|
||||
local gw dev ips ip
|
||||
gw=$(get_wan2_gw)
|
||||
dev=$(get_wan2_dev)
|
||||
if [ -z "$gw" ] || [ -z "$dev" ]; then
|
||||
logger -t starlink-policy "wan2 not ready (gw=$gw dev=$dev)"
|
||||
return 1
|
||||
fi
|
||||
ips=$(resolve_domains)
|
||||
if [ -z "$ips" ]; then
|
||||
logger -t starlink-policy "No IPs resolved for: $DOMAINS. Set POLYMARKET_IPS in script or check DNS."
|
||||
return 1
|
||||
fi
|
||||
for ip in $ips; do
|
||||
ip rule del to "$ip" table $TABLE_ID 2>/dev/null
|
||||
done
|
||||
ip route flush table $TABLE_ID 2>/dev/null
|
||||
ip route add default via "$gw" dev "$dev" table $TABLE_ID
|
||||
for ip in $ips; do
|
||||
ip rule add to "$ip" table $TABLE_ID
|
||||
done
|
||||
logger -t starlink-policy "Routes via wan2 ($dev): $ips"
|
||||
}
|
||||
|
||||
do_remove() {
|
||||
local ips ip
|
||||
ips=$(resolve_domains)
|
||||
for ip in $ips; do
|
||||
ip rule del to "$ip" table $TABLE_ID 2>/dev/null
|
||||
done
|
||||
ip route flush table $TABLE_ID 2>/dev/null
|
||||
logger -t starlink-policy "Removed policy routes for table $TABLE_ID"
|
||||
}
|
||||
|
||||
case "${1:-setup}" in
|
||||
setup) do_setup ;;
|
||||
remove) do_remove ;;
|
||||
*)
|
||||
echo "Usage: $0 [setup|remove]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
1
python/utils/__init__.py
Normal file
1
python/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Utils package
|
||||
77
python/utils/interval-timer-ding.ps1
Normal file
77
python/utils/interval-timer-ding.ps1
Normal file
@@ -0,0 +1,77 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Plays a ding/timer sound at regular intervals aligned to round period marks (e.g. every 15 min),
|
||||
with an optional offset so the sound plays a few seconds before each mark.
|
||||
|
||||
.DESCRIPTION
|
||||
Default: period 15 minutes, offset 15 seconds. Notification plays at :14:45, :29:45, :44:45, :59:45.
|
||||
|
||||
.PARAMETER PeriodMinutes
|
||||
Interval in minutes between notifications (default 15). Defines the "round" marks (0, 15, 30, 45 past the hour).
|
||||
|
||||
.PARAMETER OffsetSeconds
|
||||
Seconds before each round mark to play the sound (default 15). E.g. 15 means play at XX:14:45, XX:29:45, etc.
|
||||
|
||||
.EXAMPLE
|
||||
.\interval-timer-ding.ps1
|
||||
.\interval-timer-ding.ps1 -PeriodMinutes 30 -OffsetSeconds 10
|
||||
#>
|
||||
|
||||
param(
|
||||
[int] $PeriodMinutes = 15,
|
||||
[int] $OffsetSeconds = 15
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($PeriodMinutes -lt 1) { throw "PeriodMinutes must be >= 1" }
|
||||
if ($OffsetSeconds -lt 0) { throw "OffsetSeconds must be >= 0" }
|
||||
|
||||
$periodSec = $PeriodMinutes * 60
|
||||
|
||||
function Get-SecondsSinceMidnight {
|
||||
$now = Get-Date
|
||||
return $now.Hour * 3600 + $now.Minute * 60 + $now.Second
|
||||
}
|
||||
|
||||
function Get-NextRingSeconds {
|
||||
$nowSec = Get-SecondsSinceMidnight
|
||||
$nextBoundarySec = [Math]::Ceiling($nowSec / $periodSec) * $periodSec
|
||||
$ringSec = $nextBoundarySec - $OffsetSeconds
|
||||
if ($ringSec -le $nowSec) {
|
||||
$ringSec += $periodSec
|
||||
}
|
||||
return $ringSec
|
||||
}
|
||||
|
||||
function Get-WaitSeconds {
|
||||
$nowSec = Get-SecondsSinceMidnight
|
||||
$ringSec = Get-NextRingSeconds
|
||||
$wait = $ringSec - $nowSec
|
||||
if ($wait -le 0) { $wait = 1 }
|
||||
return [int]$wait
|
||||
}
|
||||
|
||||
function Play-Ding {
|
||||
try {
|
||||
[System.Media.SystemSounds]::Asterisk.Play()
|
||||
} catch {
|
||||
[Console]::Beep(800, 200)
|
||||
}
|
||||
}
|
||||
|
||||
$nextRing = Get-NextRingSeconds
|
||||
$nextH = [Math]::Floor($nextRing / 3600)
|
||||
$nextM = [Math]::Floor(($nextRing % 3600) / 60)
|
||||
$nextS = $nextRing % 60
|
||||
Write-Host "Period: $PeriodMinutes min, offset: $OffsetSeconds s. Next ding at $($nextH.ToString('00')):$($nextM.ToString('00')):$($nextS.ToString('00')) (then every $PeriodMinutes min). Press Ctrl+C to stop."
|
||||
|
||||
while ($true) {
|
||||
$waitSec = Get-WaitSeconds
|
||||
if ($waitSec -gt 0) {
|
||||
Start-Sleep -Seconds $waitSec
|
||||
}
|
||||
Play-Ding
|
||||
$ts = Get-Date -Format "HH:mm:ss"
|
||||
Write-Host "[$ts] ding"
|
||||
}
|
||||
177
python/utils/interval_timer_ding.py
Normal file
177
python/utils/interval_timer_ding.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Plays a ding/timer sound at regular intervals aligned to round period marks
|
||||
(e.g. every 15 min), with an optional offset so the sound plays a few seconds
|
||||
before each mark.
|
||||
|
||||
Default: period 15 min, offset 15 s, 1 ding, sound asterisk.
|
||||
Notification at :14:45, :29:45, :44:45, :59:45. Windows + Linux (stdlib).
|
||||
|
||||
python python/utils/interval_timer_ding.py
|
||||
python python/utils/interval_timer_ding.py -p 30 -o 10
|
||||
python python/utils/interval_timer_ding.py -s exclamation -n 2
|
||||
python python/utils/interval_timer_ding.py -s /path/to/notify.wav
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _seconds_since_midnight(dt: datetime) -> int:
|
||||
return dt.hour * 3600 + dt.minute * 60 + dt.second
|
||||
|
||||
|
||||
def _next_ring_seconds(period_sec: int, offset_sec: int, now_sec: int) -> int:
|
||||
next_boundary = math.ceil(now_sec / period_sec) * period_sec
|
||||
ring_sec = next_boundary - offset_sec
|
||||
if ring_sec <= now_sec:
|
||||
ring_sec += period_sec
|
||||
return ring_sec
|
||||
|
||||
|
||||
def _wait_seconds(period_sec: int, offset_sec: int) -> int:
|
||||
now = datetime.now()
|
||||
now_sec = _seconds_since_midnight(now)
|
||||
ring_sec = _next_ring_seconds(period_sec, offset_sec, now_sec)
|
||||
wait = ring_sec - now_sec
|
||||
return max(1, wait) if wait > 0 else 1
|
||||
|
||||
|
||||
# Gap between repeated dings (Windows MessageBeep is async so need ~0.5s+)
|
||||
_DING_GAP_SEC = 1.7
|
||||
_WIN_BEEP_MS = 350
|
||||
|
||||
# Sound choice: None = use default, "asterisk"|"exclamation"|... = Windows preset, or file path
|
||||
_sound: str | None = None
|
||||
|
||||
|
||||
def _play_ding_windows() -> None:
|
||||
import winsound
|
||||
sound = _sound or "asterisk"
|
||||
if sound in ("asterisk", "exclamation", "hand", "question", "beep"):
|
||||
try:
|
||||
if sound == "asterisk":
|
||||
winsound.MessageBeep(winsound.MB_ICONASTERISK)
|
||||
elif sound == "exclamation":
|
||||
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION)
|
||||
elif sound == "hand":
|
||||
winsound.MessageBeep(winsound.MB_ICONHAND)
|
||||
elif sound == "question":
|
||||
winsound.MessageBeep(winsound.MB_ICONQUESTION)
|
||||
else: # beep
|
||||
winsound.Beep(800, _WIN_BEEP_MS)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
# Custom file path (SND_FILENAME only = blocking until sound finishes)
|
||||
try:
|
||||
winsound.PlaySound(sound, winsound.SND_FILENAME)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
winsound.Beep(800, _WIN_BEEP_MS)
|
||||
except Exception:
|
||||
sys.stdout.write("\a")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _play_ding_linux() -> None:
|
||||
sound = _sound
|
||||
if sound:
|
||||
for cmd in (["paplay", sound], ["aplay", sound]):
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=3)
|
||||
return
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
continue
|
||||
candidates = [
|
||||
["paplay", "/usr/share/sounds/freedesktop/stereo/bell.oga"],
|
||||
["paplay", "/usr/share/sounds/freedesktop/stereo/message.oga"],
|
||||
["aplay", "/usr/share/sounds/alsa/Front_Center.wav"],
|
||||
["aplay", "/usr/share/sounds/speech-dispatcher/test.wav"],
|
||||
]
|
||||
for cmd in candidates:
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, timeout=2)
|
||||
return
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
||||
continue
|
||||
sys.stdout.write("\a")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Play a ding every N minutes, offset seconds before each round mark."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--period",
|
||||
type=int,
|
||||
default=15,
|
||||
metavar="MIN",
|
||||
help="Interval in minutes between notifications (default: 15)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", "--offset",
|
||||
type=int,
|
||||
default=15,
|
||||
metavar="SEC",
|
||||
help="Seconds before each round mark to play (default: 15)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s", "--sound",
|
||||
type=str,
|
||||
default=None,
|
||||
metavar="PRESET|PATH",
|
||||
help="Notification sound: Windows presets asterisk|exclamation|hand|question|beep, or path to .wav file (default: asterisk)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n", "--dings",
|
||||
type=int,
|
||||
default=1,
|
||||
metavar="N",
|
||||
help="Number of dings per notification (default: 1, max: 10)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
global _sound
|
||||
_sound = args.sound
|
||||
|
||||
if args.period < 1:
|
||||
parser.error("period must be >= 1")
|
||||
if args.offset < 0:
|
||||
parser.error("offset must be >= 0")
|
||||
if not 1 <= args.dings <= 10:
|
||||
parser.error("dings must be 1..10")
|
||||
|
||||
period_sec = args.period * 60
|
||||
now = datetime.now()
|
||||
now_sec = _seconds_since_midnight(now)
|
||||
next_ring = _next_ring_seconds(period_sec, args.offset, now_sec)
|
||||
h = next_ring // 3600
|
||||
m = (next_ring % 3600) // 60
|
||||
s = next_ring % 60
|
||||
print(
|
||||
f"Period: {args.period} min, offset: {args.offset} s. "
|
||||
f"Next ding at {h:02d}:{m:02d}:{s:02d} (then every {args.period} min). "
|
||||
"Press Ctrl+C to stop."
|
||||
)
|
||||
|
||||
play = _play_ding_windows if sys.platform == "win32" else _play_ding_linux
|
||||
while True:
|
||||
wait_sec = _wait_seconds(period_sec, args.offset)
|
||||
time.sleep(wait_sec)
|
||||
for i in range(args.dings):
|
||||
play()
|
||||
if i < args.dings - 1:
|
||||
time.sleep(_DING_GAP_SEC)
|
||||
print(f"[{datetime.now().strftime('%H:%M:%S')}] ding")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user