#!/usr/bin/env bash set -euo pipefail # ============================================================ # Interactive Squid HTTPS Proxy Installer with Let's Encrypt # Debian 12/13 # # 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() { echo -e "${GREEN}[OK]${RESET} $*"; } info() { echo -e "${BLUE}[INFO]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } SQUID_USER="proxy" SQUID_GROUP="proxy" STOPPED_SERVICES=() require_root() { if [[ "${EUID}" -ne 0 ]]; then die "Run this script as root." fi } ask() { local prompt="$1" local default="${2:-}" local value if [[ -n "$default" ]]; then read -rp "$prompt [$default]: " value echo "${value:-$default}" else read -rp "$prompt: " value echo "$value" fi } ask_password_confirmed() { local pass1 pass2 while true; do read -rsp "Proxy password: " pass1 printf '\n' >&2 read -rsp "Confirm proxy password: " pass2 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 [[ ! "$domain" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then die "Invalid domain: $domain" fi if [[ "$domain" =~ ^[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 ! [[ "$port" =~ ^[0-9]+$ ]]; then die "Invalid port: $port" fi if (( port < 1 || port > 65535 )); then die "Port must be between 1 and 65535." fi } detect_proxy_user_group() { if id proxy >/dev/null 2>&1; then SQUID_USER="proxy" elif id squid >/dev/null 2>&1; then SQUID_USER="squid" else SQUID_USER="proxy" fi if getent group proxy >/dev/null 2>&1; then SQUID_GROUP="proxy" elif getent group squid >/dev/null 2>&1; then SQUID_GROUP="squid" else SQUID_GROUP="$SQUID_USER" fi } install_packages() { info "Installing required packages..." apt update if apt-cache show squid-openssl >/dev/null 2>&1; then apt install -y squid-openssl apache2-utils certbot openssl curl ca-certificates dnsutils else warn "squid-openssl package was not found." warn "Installing normal squid, but HTTPS proxy listener may not work." apt install -y squid apache2-utils certbot openssl curl ca-certificates dnsutils fi 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 "If https_port fails later, install squid-openssl or use a TLS wrapper." 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." read -rp "Continue anyway? [y/N]: " yn [[ "${yn,,}" == "y" ]] || die "Stopped." 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 ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qi active; then ufw allow 80/tcp || true ufw allow "$port/tcp" || true log "UFW rules added." else warn "UFW not active or not installed. Skipping UFW rules." fi 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 unless your VPS image saves them." fi } stop_port_80_services_if_needed() { STOPPED_SERVICES=() if ss -tulpn | grep -qE '(:80[[:space:]]|:80$|:80,)'; then warn "Something appears to be using port 80." echo ss -tulpn | grep ':80' || true echo read -rp "Temporarily stop nginx/apache2/caddy if running so Certbot can use port 80? [Y/n]: " yn yn="${yn:-Y}" if [[ "${yn,,}" == "y" ]]; then for svc in nginx apache2 caddy; do if systemctl is-active --quiet "$svc" 2>/dev/null; then systemctl stop "$svc" STOPPED_SERVICES+=("systemd:$svc") warn "Stopped $svc temporarily." fi done fi fi } restart_stopped_services() { if [[ ${#STOPPED_SERVICES[@]} -gt 0 ]]; then info "Restarting services stopped for Certbot..." for item in "${STOPPED_SERVICES[@]}"; do kind="${item%%:*}" name="${item#*:}" if [[ "$kind" == "systemd" ]]; then systemctl start "$name" || warn "Could not restart $name." fi 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." read -rp "Use existing certificate? [Y/n]: " yn yn="${yn:-Y}" if [[ "${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 systemctl start squid if systemctl is-active --quiet squid; then systemctl enable squid >/dev/null 2>&1 || true log "Squid is running." else systemctl status squid --no-pager -l || 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 ss -tulpn | grep -q ":$port"; then log "Something is listening on port $port:" ss -tulpn | grep ":$port" || true else warn "Port $port does not appear in ss 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 "============================================================" 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 read -rp "Continue with install? [Y/n]: " confirm confirm="${confirm:-Y}" [[ "${confirm,,}" == "y" ]] || die "Cancelled." 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 "$@"