diff options
| author | Filip Wandzio <contact@philw.dev> | 2025-08-20 22:30:38 +0200 |
|---|---|---|
| committer | Filip Wandzio <contact@philw.dev> | 2025-08-20 22:30:38 +0200 |
| commit | 53c9924ad4db79bc9c4b1c2fce045e7fe7d6f23e (patch) | |
| tree | bc971ea26b2c9ffc9fc258d3bea61d7d261668ad /whiterabbit.sh | |
| parent | 8684505db67bc41822130dfec9110670f5655834 (diff) | |
| download | whiterabbit-53c9924ad4db79bc9c4b1c2fce045e7fe7d6f23e.tar.gz whiterabbit-53c9924ad4db79bc9c4b1c2fce045e7fe7d6f23e.zip | |
Add turn server setup and nginx reverse proxy
Signed-off-by: Filip Wandzio <contact@philw.dev>
Diffstat (limited to 'whiterabbit.sh')
| -rwxr-xr-x[-rw-r--r--] | whiterabbit.sh | 358 |
1 files changed, 194 insertions, 164 deletions
diff --git a/whiterabbit.sh b/whiterabbit.sh index 8f1d77d..e68351f 100644..100755 --- a/whiterabbit.sh +++ b/whiterabbit.sh | |||
| @@ -1,194 +1,224 @@ | |||
| 1 | #!/bin/sh | 1 | #!/usr/bin/sh |
| 2 | set -e | 2 | set -euo pipefail |
| 3 | 3 | ||
| 4 | echo "Warning: secrets will be visible when typing. Press Enter after each input." | 4 | echo "=== Matrix Dendrite + Coturn auto-setup (Docker + standalone Coturn) ===" |
| 5 | 5 | ||
| 6 | # --- User input --- | 6 | # ---- User Input ---- |
| 7 | printf "Base domain (e.g., example.com): " | 7 | read -rp "Matrix domain (eg. matrix.example.com): " MATRIX_DOMAIN |
| 8 | read DOMAIN | 8 | read -rp "TURN domain(eg. turn.example.com): " TURN_DOMAIN |
| 9 | printf "Subdomain for this instance (e.g., matrix1): " | 9 | read -rp "Let's Encrypt certificate email: " EMAIL |
| 10 | read SUBDOMAIN | 10 | read -rp "Postgres secret: " DB_PASS |
| 11 | CN="$SUBDOMAIN.$DOMAIN" | 11 | read -rp "registration_shared_secret: " REG_SECRET |
| 12 | 12 | read -rp "TURN shared secret: " TURN_SECRET | |
| 13 | printf "Postgres secret: " | 13 | read -rp "TURN IP (eg. 127.0.0.1): " TURN_SERVER_IP |
| 14 | read POSTGRES_SECRET | 14 | read -rp "MATRIX server IP (eg. 127.0.0.1): " MATRIX_SERVER_IP |
| 15 | printf "REG Secret (registration_shared_secret): " | 15 | read -rp "Listening device (eg. eth0): " TURN_LISTENING_DEVICE |
| 16 | read REG_SECRET | 16 | |
| 17 | 17 | INSTALL_DIR="/opt/matrix/$MATRIX_DOMAIN" | |
| 18 | # --- Directories --- | 18 | mkdir -p "$INSTALL_DIR/config" |
| 19 | BASE_DIR="/opt/matrix/$CN" | 19 | cd "$INSTALL_DIR" |
| 20 | mkdir -p "$BASE_DIR/data" | 20 | |
| 21 | mkdir -p "$BASE_DIR/db" | 21 | cat <<EOF > .env |
| 22 | 22 | # PostgreSQL | |
| 23 | # --- Automatic port assignment --- | 23 | POSTGRES_HOSTNAME=postgres |
| 24 | BASE_PORT=8008 | 24 | POSTGRES_VERSION=15-alpine |
| 25 | FEDERATION_PORT=8448 | 25 | POSTGRES_USER=dendrite |
| 26 | 26 | POSTGRES_PASSWORD=$DB_PASS | |
| 27 | for dir in /opt/matrix/*; do | 27 | POSTGRES_DB=dendrite |
| 28 | if [ -f "$dir/docker-compose.yml" ]; then | 28 | |
| 29 | used_ports=$(grep 'ports:' -A1 "$dir/docker-compose.yml" | awk -F: '{print $2}' | tr -d '"') | 29 | # Monolith |
| 30 | for port in $used_ports; do | 30 | MONOLITH_HOSTNAME=monolith |
| 31 | if [ "$port" ] && [ "$port" -ge "$BASE_PORT" ]; then | 31 | MONOLITH_IMAGE=matrixdotorg/dendrite-monolith:latest |
| 32 | BASE_PORT=$((port + 1)) | 32 | MONOLITH_PORT_HTTP=8008 |
| 33 | fi | 33 | MONOLITH_PORT_HTTPS=8448 |
| 34 | if [ "$port" ] && [ "$port" -ge "$FEDERATION_PORT" ]; then | 34 | EOF |
| 35 | FEDERATION_PORT=$((port + 1)) | ||
| 36 | fi | ||
| 37 | done | ||
| 38 | fi | ||
| 39 | done | ||
| 40 | |||
| 41 | echo "Assigning ports: client-server=$BASE_PORT, federation=$FEDERATION_PORT" | ||
| 42 | 35 | ||
| 43 | # --- Docker Compose --- | 36 | cat <<'EOF' > compose.yml |
| 44 | cat > "$BASE_DIR/docker-compose.yml" <<EOF | ||
| 45 | services: | 37 | services: |
| 46 | dendrite: | 38 | postgres: |
| 47 | image: ghcr.io/element-hq/dendrite-monolith:latest | 39 | hostname: ${POSTGRES_HOSTNAME:-postgres} |
| 48 | restart: unless-stopped | 40 | image: postgres:${POSTGRES_VERSION:-15-alpine} |
| 41 | restart: always | ||
| 42 | volumes: | ||
| 43 | - ./dendrite_postgres_data:/var/lib/postgresql/data | ||
| 49 | environment: | 44 | environment: |
| 50 | POSTGRES_URI: postgres://dendrite:$POSTGRES_SECRET@db/dendrite | 45 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-itsasecret} |
| 51 | SERVER_NAME: $CN | 46 | POSTGRES_USER: ${POSTGRES_USER:-dendrite} |
| 52 | REGISTRATION_SHARED_SECRET: $REG_SECRET | 47 | POSTGRES_DB: ${POSTGRES_DB:-dendrite} |
| 53 | DISABLE_TLS: "true" | 48 | healthcheck: |
| 49 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dendrite}"] | ||
| 50 | interval: 5s | ||
| 51 | timeout: 5s | ||
| 52 | retries: 5 | ||
| 53 | networks: | ||
| 54 | - internal | ||
| 55 | monolith: | ||
| 56 | hostname: ${MONOLITH_HOSTNAME:-monolith} | ||
| 57 | image: ${MONOLITH_IMAGE:-matrixdotorg/dendrite-monolith:latest} | ||
| 54 | ports: | 58 | ports: |
| 55 | - "$BASE_PORT:8008" | 59 | - ${MONOLITH_PORT_HTTP:-8008}:8008 |
| 56 | - "$FEDERATION_PORT:8448" | 60 | - ${MONOLITH_PORT_HTTPS:-8448}:8448 |
| 57 | volumes: | 61 | volumes: |
| 58 | - ./data:/data | 62 | - ./config:/etc/dendrite |
| 59 | 63 | - ./dendrite_media:/var/dendrite/media | |
| 60 | db: | 64 | - ./dendrite_jetstream:/var/dendrite/jetstream |
| 61 | image: postgres:15 | 65 | - ./dendrite_search_index:/var/dendrite/searchindex |
| 66 | - ./:/mnt | ||
| 67 | depends_on: | ||
| 68 | postgres: | ||
| 69 | condition: service_healthy | ||
| 70 | networks: | ||
| 71 | - internal | ||
| 62 | restart: unless-stopped | 72 | restart: unless-stopped |
| 63 | environment: | 73 | networks: |
| 64 | POSTGRES_USER: dendrite | 74 | internal: |
| 65 | POSTGRES_PASSWORD: $POSTGRES_SECRET | 75 | attachable: true |
| 66 | volumes: | 76 | volumes: |
| 67 | - ./db:/var/lib/postgresql/data | 77 | dendrite_postgres_data: |
| 78 | dendrite_media: | ||
| 79 | dendrite_jetstream: | ||
| 80 | dendrite_search_index: | ||
| 68 | EOF | 81 | EOF |
| 69 | 82 | ||
| 70 | # --- Nginx config --- | 83 | cat <<EOF > config/dendrite.yaml |
| 71 | NGINX_CONF="/etc/nginx/sites-available/$CN" | 84 | version: 2 |
| 72 | sudo tee "$NGINX_CONF" > /dev/null <<EOF | 85 | global: |
| 73 | server { | 86 | server_name: $MATRIX_DOMAIN |
| 74 | listen 80; | 87 | private_key: /mnt/matrix_key.pem |
| 75 | listen [::]:80; | 88 | old_private_keys: |
| 76 | server_name $CN; | 89 | key_validity_period: 168h0m0s |
| 77 | 90 | database: | |
| 78 | location /.well-known/matrix/ { | 91 | connection_string: postgresql://dendrite:$DB_PASS@postgres/dendrite?sslmode=disable |
| 79 | try_files \$uri =404; | 92 | max_open_conns: 90 |
| 80 | } | 93 | max_idle_conns: 5 |
| 94 | conn_max_lifetime: -1 | ||
| 95 | |||
| 96 | well_known_server_name: "$MATRIX_DOMAIN:443" | ||
| 97 | well_known_client_name: "https://$MATRIX_DOMAIN" | ||
| 98 | |||
| 99 | client_api: | ||
| 100 | registration_disabled: true | ||
| 101 | guests_disabled: true | ||
| 102 | registration_shared_secret: "$REG_SECRET" | ||
| 103 | |||
| 104 | turn: | ||
| 105 | turn_user_lifetime: "5m" | ||
| 106 | turn_uris: | ||
| 107 | - turn:$TURN_DOMAIN?transport=udp | ||
| 108 | - turn:$TURN_DOMAIN?transport=tcp | ||
| 109 | turn_shared_secret: "$TURN_SECRET" | ||
| 110 | |||
| 111 | federation_api: | ||
| 112 | send_max_retries: 16 | ||
| 113 | disable_tls_validation: false | ||
| 114 | |||
| 115 | media_api: | ||
| 116 | base_path: ./media_store | ||
| 117 | max_file_size_bytes: 10485760 | ||
| 118 | dynamic_thumbnails: false | ||
| 119 | thumbnail_sizes: | ||
| 120 | - width: 96 | ||
| 121 | height: 96 | ||
| 122 | method: crop | ||
| 123 | - width: 640 | ||
| 124 | height: 480 | ||
| 125 | method: scale | ||
| 126 | |||
| 127 | user_api: | ||
| 128 | bcrypt_cost: 10 | ||
| 129 | auto_join_rooms: | ||
| 130 | - "#main:$MATRIX_DOMAIN" | ||
| 131 | |||
| 132 | logging: | ||
| 133 | - type: std | ||
| 134 | level: info | ||
| 135 | EOF | ||
| 81 | 136 | ||
| 82 | location / { | 137 | cat <<EOF > /etc/turnserver.conf |
| 83 | proxy_pass http://127.0.0.1:$BASE_PORT; | 138 | listening-device=$TURN_LISTENING_DEVICE |
| 84 | proxy_http_version 1.1; | 139 | listening-port=3478 |
| 85 | proxy_set_header Host \$host; | 140 | tls-listening-port=5349 |
| 86 | proxy_set_header X-Real-IP \$remote_addr; | 141 | listening-ip=$TURN_SERVER_IP |
| 87 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | 142 | min-port=49152 |
| 88 | proxy_set_header X-Forwarded-Proto \$scheme; | 143 | max-port=65535 |
| 89 | client_max_body_size 50M; | 144 | use-auth-secret |
| 90 | proxy_read_timeout 600s; | 145 | static-auth-secret=$TURN_SECRET |
| 91 | } | 146 | syslog |
| 92 | } | 147 | no-rfc5780 |
| 148 | no-stun-backward-compatibility | ||
| 149 | response-origin-only-with-rfc5780 | ||
| 93 | EOF | 150 | EOF |
| 94 | 151 | ||
| 95 | sudo ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/ | 152 | echo "[INFO] Generating server keys..." |
| 96 | sudo nginx -t | 153 | docker run --rm --entrypoint="" -v $(pwd):/mnt matrixdotorg/dendrite-monolith:latest /usr/bin/generate-keys -private-key /mnt/matrix_key.pem -tls-cert /mnt/server.crt -tls-key /mnt/server.key |
| 97 | sudo systemctl reload nginx | ||
| 98 | 154 | ||
| 99 | # --- Start Docker Compose --- | 155 | echo "[INFO] Booting up containers(HTTP-only test)..." |
| 100 | cd "$BASE_DIR" | ||
| 101 | docker compose up -d | 156 | docker compose up -d |
| 102 | 157 | ||
| 103 | # --- DNS propagation check --- | 158 | echo "[INFO] Waiting for dendrite monolith to answer..." |
| 104 | if ! command -v dig >/dev/null 2>&1; then | 159 | for i in {1..30}; do |
| 105 | echo "Installing dnsutils (needed for DNS checks)..." | 160 | if curl -fs "http://$MATRIX_SERVER_IP:8008/_matrix/client/versions" >/dev/null 2>&1; then |
| 106 | sudo apt-get update && sudo apt-get install -y dnsutils | 161 | echo "[OK] Monolith dziaĆa na http://$MATRIX_SERVER_IP:8008" |
| 107 | fi | 162 | break |
| 108 | 163 | fi | |
| 109 | # Collect all VPS IPs (IPv4 + IPv6) | 164 | echo " ...retry $i" |
| 110 | VPS_IPS=$(hostname -I | tr ' ' '\n') | 165 | sleep 5 |
| 111 | echo "VPS addresses: $VPS_IPS" | ||
| 112 | |||
| 113 | echo "Checking DNS propagation for $CN ..." | ||
| 114 | MAX_RETRIES=30 | ||
| 115 | SLEEP_SEC=10 | ||
| 116 | count=0 | ||
| 117 | |||
| 118 | while true; do | ||
| 119 | DNS_IPS=$( (dig +short "$CN" A; dig +short "$CN" AAAA) | sort -u ) | ||
| 120 | MATCH="false" | ||
| 121 | |||
| 122 | for dns_ip in $DNS_IPS; do | ||
| 123 | for vps_ip in $VPS_IPS; do | ||
| 124 | if [ "$dns_ip" = "$vps_ip" ]; then | ||
| 125 | MATCH="true" | ||
| 126 | break | ||
| 127 | fi | ||
| 128 | done | ||
| 129 | done | ||
| 130 | |||
| 131 | if [ "$MATCH" = "true" ]; then | ||
| 132 | echo "$CN resolves correctly to one of the VPS IPs: $DNS_IPS" | ||
| 133 | break | ||
| 134 | else | ||
| 135 | count=$((count + 1)) | ||
| 136 | if [ "$count" -ge "$MAX_RETRIES" ]; then | ||
| 137 | echo "DNS propagation not detected after $((MAX_RETRIES*SLEEP_SEC)) seconds." | ||
| 138 | echo "Please make sure $CN points to this VPS and rerun the script." | ||
| 139 | exit 1 | ||
| 140 | fi | ||
| 141 | echo "DNS not ready yet ($count/$MAX_RETRIES). Found: $DNS_IPS Expected one of: $VPS_IPS" | ||
| 142 | echo "Retrying in $SLEEP_SEC seconds..." | ||
| 143 | sleep $SLEEP_SEC | ||
| 144 | fi | ||
| 145 | done | 166 | done |
| 146 | 167 | ||
| 147 | # --- Obtain HTTPS certificate --- | 168 | echo "[INFO] Setting up NGINX RevPrx files" |
| 148 | sudo certbot certonly --nginx -d "$CN" --non-interactive --agree-tos -m "admin@$DOMAIN" | 169 | cat <<EOF > /etc/nginx/sites-available/$MATRIX_DOMAIN |
| 149 | |||
| 150 | sudo tee "$NGINX_CONF" > /dev/null <<EOF | ||
| 151 | server { | 170 | server { |
| 152 | listen 80; | 171 | listen 80; |
| 153 | listen [::]:80; | 172 | listen [::]:80; |
| 154 | server_name $CN; | 173 | server_name $MATRIX_DOMAIN; |
| 155 | return 301 https://\$host\$request_uri; | 174 | location /.well-known/matrix/client { |
| 175 | return 301 https://$host$request_uri; | ||
| 176 | } | ||
| 177 | location /.well-known/matrix/server { | ||
| 178 | return 301 https://$host$request_uri; | ||
| 179 | } | ||
| 180 | location / { | ||
| 181 | return 301 https://$host$request_uri; | ||
| 182 | } | ||
| 156 | } | 183 | } |
| 157 | 184 | ||
| 158 | server { | 185 | server { |
| 159 | listen 443 ssl http2; | 186 | listen 443 ssl http2; |
| 160 | listen [::]:443 ssl http2; | 187 | listen [::]:443 ssl http2; |
| 161 | server_name $CN; | 188 | server_name $MATRIX_DOMAIN; |
| 162 | 189 | ||
| 163 | ssl_certificate /etc/letsencrypt/live/$CN/fullchain.pem; | 190 | ssl_certificate /etc/letsencrypt/live/$MATRIX_DOMAIN/fullchain.pem; |
| 164 | ssl_certificate_key /etc/letsencrypt/live/$CN/privkey.pem; | 191 | ssl_certificate_key /etc/letsencrypt/live/$MATRIX_DOMAIN/privkey.pem; |
| 165 | ssl_protocols TLSv1.2 TLSv1.3; | 192 | include /etc/letsencrypt/options-ssl-nginx.conf; |
| 166 | ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; | 193 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; |
| 167 | ssl_prefer_server_ciphers on; | ||
| 168 | ssl_session_cache shared:SSL:10m; | ||
| 169 | ssl_session_timeout 1h; | ||
| 170 | add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; | ||
| 171 | add_header X-Content-Type-Options nosniff; | ||
| 172 | add_header X-Frame-Options DENY; | ||
| 173 | add_header X-XSS-Protection "1; mode=block"; | ||
| 174 | add_header Referrer-Policy "no-referrer-when-downgrade"; | ||
| 175 | |||
| 176 | location /.well-known/matrix/ { | ||
| 177 | try_files \$uri =404; | ||
| 178 | } | ||
| 179 | 194 | ||
| 180 | location / { | 195 | location / { |
| 181 | proxy_pass http://127.0.0.1:$BASE_PORT; | 196 | proxy_pass http://127.0.0.1:8008; |
| 182 | proxy_http_version 1.1; | 197 | proxy_http_version 1.1; |
| 183 | proxy_set_header Host \$host; | 198 | proxy_set_header Host $host; |
| 184 | proxy_set_header X-Real-IP \$remote_addr; | 199 | proxy_set_header X-Real-IP $remote_addr; |
| 185 | proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | 200 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 186 | proxy_set_header X-Forwarded-Proto \$scheme; | 201 | proxy_set_header X-Forwarded-Proto $scheme; |
| 187 | client_max_body_size 50M; | 202 | proxy_set_header Upgrade $http_upgrade; |
| 188 | proxy_read_timeout 600s; | 203 | proxy_set_header Connection "upgrade"; |
| 204 | } | ||
| 205 | |||
| 206 | location /.well-known/matrix/client { | ||
| 207 | default_type application/json; | ||
| 208 | add_header Access-Control-Allow-Origin *; | ||
| 209 | return 200 '{"m.homeserver": {"base_url": "https://$MATRIX_DOMAIN"}}'; | ||
| 210 | } | ||
| 211 | |||
| 212 | location /.well-known/matrix/server { | ||
| 213 | default_type application/json; | ||
| 214 | add_header Access-Control-Allow-Origin *; | ||
| 215 | return 200 '{"m.server": "$MATRIX_DOMAIN:443"}'; | ||
| 189 | } | 216 | } |
| 190 | } | 217 | } |
| 191 | EOF | 218 | EOF |
| 192 | sudo nginx -t | 219 | |
| 193 | sudo systemctl reload nginx | 220 | ln -s /etc/nginx/sites-available/$MATRIX_DOMAIN |
| 194 | echo "HTTPS active for $CN with federation support!" | 221 | echo "============================================" |
| 222 | echo " Matrix server setup finished!" | ||
| 223 | echo " HomeServer: https://$MATRIX_DOMAIN:8448" | ||
| 224 | echo " TURN: $TURN_DOMAIN (3478/5349)" | ||
