#!/bin/sh set -eu # ============================================================ # Interactive Squid HTTPS Proxy Installer with Let's Encrypt # Alpine Linux Edition (OpenRC) # # Creates: # Firefox/FoxyProxy -> HTTPS proxy -> Squid -> Internet # # IMPORTANT: # In FoxyProxy, use the DOMAIN NAME, not the IP address. # ============================================================ RED="\033[31m" GREEN="\033[32m" YELLOW="\033[33m" BLUE="\033[34m" RESET="\033[0m" log() { printf "${GREEN}[OK]${RESET} %s\n" "$*"; } info() { printf "${BLUE}[INFO]${RESET} %s\n" "$*"; } warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*"; } die() { printf "${RED}[ERROR]${RESET} %s\n" "$*" >&2; exit 1; } SQUID_USER="squid" SQUID_GROUP="squid" STOPPED_SERVICES="" require_root() { if [ "$(id -u)" -ne 0 ]; then die "Run this script as root." fi } ask() { local prompt="$1" local default="${2:-}" local value # Output the prompt to stderr (>&2) so it displays on the screen # even when the function's output is being captured by a variable. if [ -n "$default" ]; then printf "%s [%s]: " "$prompt" "$default" >&2 else printf "%s: " "$prompt" >&2 fi read -r value echo "${value:-$default}" } ask_password_confirmed() { local pass1 pass2 while true; do printf "Proxy password: " >&2 stty -echo read -r pass1 stty echo printf '\n' >&2 printf "Confirm proxy password: " >&2 stty -echo read -r pass2 stty echo printf '\n' >&2 if [ -z "$pass1" ]; then warn "Password cannot be empty." >&2 continue fi if [ "$pass1" != "$pass2" ]; then warn "Passwords do not match." >&2 continue fi printf '%s' "$pass1" return 0 done } validate_domain() { local domain="$1" if ! echo "$domain" | grep -qE '^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'; then die "Invalid domain: $domain" fi if echo "$domain" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then die "Do not use an IP address. Use a real hostname, e.g. proxy.example.com" fi } validate_port() { local port="$1" if ! echo "$port" | grep -qE '^[0-9]+$'; then die "Invalid port: $port" fi if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then die "Port must be between 1 and 65535." fi } detect_proxy_user_group() { if id squid >/dev/null 2>&1; then SQUID_USER="squid" elif id proxy >/dev/null 2>&1; then SQUID_USER="proxy" else SQUID_USER="squid" fi if getent group squid >/dev/null 2>&1; then SQUID_GROUP="squid" elif getent group proxy >/dev/null 2>&1; then SQUID_GROUP="proxy" else SQUID_GROUP="$SQUID_USER" fi } install_packages() { info "Installing required packages via apk..." apk update apk add squid apache2-utils certbot openssl curl ca-certificates bind-tools iptables log "Packages installed." } check_squid_openssl() { info "Checking Squid OpenSSL support..." if squid -v 2>/dev/null | grep -qi openssl; then log "Squid appears to have OpenSSL support." else warn "Could not confirm OpenSSL support from squid -v." warn "Alpine's default squid should support it, but check logs if https_port fails." fi } check_dns() { local domain="$1" info "Checking DNS for $domain..." local resolved_v4 local resolved_v6 resolved_v4="$(dig +short A "$domain" | tail -n1 || true)" resolved_v6="$(dig +short AAAA "$domain" | tail -n1 || true)" if [ -z "$resolved_v4" ] && [ -z "$resolved_v6" ]; then warn "No A or AAAA record found for $domain." warn "Let's Encrypt will fail unless this hostname points to this server." printf "Continue anyway? [y/N]: " read -r yn if [ "$yn" != "y" ] && [ "$yn" != "Y" ]; then die "Stopped." fi else if [ -n "$resolved_v4" ]; then log "$domain A record: $resolved_v4" fi if [ -n "$resolved_v6" ]; then log "$domain AAAA record: $resolved_v6" else warn "$domain has no AAAA record. That is fine if you only want IPv4." fi fi return 0 } open_firewall() { local port="$1" info "Attempting to open firewall ports 80 and $port..." if command -v iptables >/dev/null 2>&1; then iptables -C INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport 80 -j ACCEPT iptables -C INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$port" -j ACCEPT log "Temporary iptables rules added." warn "iptables rules may not persist after reboot depending on your Alpine configuration (awall/iptables-save)." fi } stop_port_80_services_if_needed() { STOPPED_SERVICES="" if netstat -tlnp | grep -qE '(:80[[:space:]]|:80$)'; then warn "Something appears to be using port 80." echo netstat -tlnp | grep ':80' || true echo printf "Temporarily stop nginx/apache2/caddy if running so Certbot can use port 80? [Y/n]: " read -r yn yn="${yn:-Y}" if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then for svc in nginx apache2 caddy; do if rc-service "$svc" status >/dev/null 2>&1; then rc-service "$svc" stop STOPPED_SERVICES="$STOPPED_SERVICES $svc" warn "Stopped $svc temporarily." fi done fi fi } restart_stopped_services() { if [ -n "$STOPPED_SERVICES" ]; then info "Restarting services stopped for Certbot..." for svc in $STOPPED_SERVICES; do rc-service "$svc" start || warn "Could not restart $svc." done fi } get_certificate() { local domain="$1" local email="$2" if [ -f "/etc/letsencrypt/live/$domain/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$domain/privkey.pem" ]; then log "Existing Let's Encrypt cert found for $domain." printf "Use existing certificate? [Y/n]: " read -r yn yn="${yn:-Y}" if [ "$yn" = "y" ] || [ "$yn" = "Y" ]; then return 0 fi fi stop_port_80_services_if_needed info "Requesting Let's Encrypt certificate for $domain..." if [ -n "$email" ]; then certbot certonly --standalone \ -d "$domain" \ --non-interactive \ --agree-tos \ --email "$email" else certbot certonly --standalone \ -d "$domain" \ --non-interactive \ --agree-tos \ --register-unsafely-without-email fi restart_stopped_services log "Certificate issued." } copy_certs() { local domain="$1" info "Copying certificate files into /etc/squid/certs..." mkdir -p /etc/squid/certs cp "/etc/letsencrypt/live/$domain/fullchain.pem" /etc/squid/certs/proxy-fullchain.pem cp "/etc/letsencrypt/live/$domain/privkey.pem" /etc/squid/certs/proxy-privkey.pem chown -R "$SQUID_USER:$SQUID_GROUP" /etc/squid/certs chmod 644 /etc/squid/certs/proxy-fullchain.pem chmod 600 /etc/squid/certs/proxy-privkey.pem log "Certificates copied." } create_auth() { local username="$1" local password="$2" info "Creating Squid username/password auth..." touch /etc/squid/passwd chown "$SQUID_USER:$SQUID_GROUP" /etc/squid/passwd chmod 640 /etc/squid/passwd printf '%s\n' "$password" | htpasswd -m -i /etc/squid/passwd "$username" >/dev/null chown "$SQUID_USER:$SQUID_GROUP" /etc/squid/passwd chmod 640 /etc/squid/passwd log "Proxy user created: $username" } test_auth_helper() { local username="$1" local password="$2" info "Testing Squid auth helper directly..." local result result="$(printf '%s %s\n' "$username" "$password" | /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd || true)" if echo "$result" | grep -q '^OK'; then log "Squid auth helper accepted the username/password." else warn "Squid auth helper did not accept the username/password." echo "Auth helper output:" echo "$result" die "Stopping because proxy auth would fail." fi } write_squid_config() { local domain="$1" local port="$2" local syntax="$3" local https_line if [ "$syntax" = "new" ]; then https_line="https_port $port tls-cert=/etc/squid/certs/proxy-fullchain.pem tls-key=/etc/squid/certs/proxy-privkey.pem" else https_line="https_port $port cert=/etc/squid/certs/proxy-fullchain.pem key=/etc/squid/certs/proxy-privkey.pem" fi info "Writing Squid config using syntax: $syntax" if [ -f /etc/squid/squid.conf ] && [ ! -f /etc/squid/squid.conf.bak-before-https-proxy ]; then cp /etc/squid/squid.conf /etc/squid/squid.conf.bak-before-https-proxy log "Backed up original config to /etc/squid/squid.conf.bak-before-https-proxy" fi cat > /etc/squid/squid.conf </tmp/squid-parse.log 2>&1; then log "Squid config parsed successfully with tls-cert/tls-key syntax." return 0 fi warn "tls-cert/tls-key syntax failed. Trying older cert/key syntax..." cat /tmp/squid-parse.log || true write_squid_config "$domain" "$port" "old" if squid -k parse >/tmp/squid-parse.log 2>&1; then log "Squid config parsed successfully with cert/key syntax." return 0 fi cat /tmp/squid-parse.log || true die "Squid could not parse either HTTPS config style. Your Squid build may not support https_port/OpenSSL." } write_renewal_hook() { local domain="$1" info "Creating Certbot renewal hook..." mkdir -p /etc/letsencrypt/renewal-hooks/deploy cat > /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh </dev/null 2>&1 || true sleep 2 pkill -9 squid >/dev/null 2>&1 || true sleep 1 rc-service squid start if rc-service squid status >/dev/null 2>&1; then rc-update add squid default >/dev/null 2>&1 || true log "Squid is running and enabled on boot." else rc-service squid status || true tail -100 /var/log/squid/cache.log || true die "Squid failed to start." fi } test_listener() { local port="$1" info "Checking if Squid is listening on port $port..." if netstat -tlnp | grep -q ":$port "; then log "Something is listening on port $port:" netstat -tlnp | grep ":$port " || true else warn "Port $port does not appear in netstat output." fi } test_proxy() { local domain="$1" local port="$2" local username="$3" local password="$4" info "Testing proxy with curl..." set +e local result result="$(curl -4 -sS --max-time 30 \ --proxy "https://$domain:$port" \ --proxy-user "$username:$password" \ https://chy.li/ip 2>&1)" local rc=$? set -e if [ $rc -eq 0 ]; then log "Proxy test succeeded. Outgoing IP:" echo "$result" else warn "Curl proxy test failed." echo "$result" echo warn "Check logs:" echo " tail -f /var/log/squid/cache.log" echo " tail -f /var/log/squid/access.log" fi } print_summary() { local domain="$1" local port="$2" local username="$3" echo echo "============================================================" echo " HTTPS Squid Proxy Setup Complete" echo "============================================================" echo echo "FoxyProxy settings:" echo echo " Proxy Type: HTTPS" echo " Host: $domain" echo " Port: $port" echo " Username: $username" echo " Password: the password you entered" echo echo "IMPORTANT:" echo " Use the hostname \"$domain\" in FoxyProxy." echo " Do NOT use the server IP address." echo echo "Test command:" echo echo " curl -v \\" echo " --proxy \"https://$domain:$port\" \\" echo " --proxy-user \"$username:YOUR_PASSWORD\" \\" echo " https://chy.li/ip" echo echo "Useful logs:" echo echo " tail -f /var/log/squid/cache.log" echo " tail -f /var/log/squid/access.log" echo echo "============================================================" } main() { require_root echo echo "============================================================" echo " Interactive Squid HTTPS Proxy + Let's Encrypt Installer" echo " Alpine Linux Edition" echo "============================================================" echo DOMAIN="$(ask "Proxy domain, e.g. proxy.example.com")" validate_domain "$DOMAIN" PORT="$(ask "HTTPS proxy port" "8443")" validate_port "$PORT" USERNAME="$(ask "Proxy username" "proxyuser")" if [ -z "$USERNAME" ]; then die "Username cannot be empty." fi PASSWORD="$(ask_password_confirmed)" EMAIL="$(ask "Email for Let's Encrypt renewal notices, or leave blank" "")" echo echo "Summary:" echo " Domain: $DOMAIN" echo " Port: $PORT" echo " Username: $USERNAME" if [ -n "$EMAIL" ]; then echo " LE Email: $EMAIL" else echo " LE Email: none" fi echo printf "Continue with install? [Y/n]: " read -r confirm confirm="${confirm:-Y}" if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then die "Cancelled." fi install_packages detect_proxy_user_group info "Detected Squid user/group: $SQUID_USER:$SQUID_GROUP" check_squid_openssl check_dns "$DOMAIN" open_firewall "$PORT" get_certificate "$DOMAIN" "$EMAIL" copy_certs "$DOMAIN" create_auth "$USERNAME" "$PASSWORD" test_auth_helper "$USERNAME" "$PASSWORD" find_working_squid_syntax "$DOMAIN" "$PORT" write_renewal_hook "$DOMAIN" restart_squid_cleanly test_listener "$PORT" test_proxy "$DOMAIN" "$PORT" "$USERNAME" "$PASSWORD" print_summary "$DOMAIN" "$PORT" "$USERNAME" } main "$@"