wget -O setup-https-squid-proxy.sh 'https://beanman.net/gist/beanman109/setup-https-squid-proxy/raw/HEAD/setup-https-squid-proxy.sh' && chmod +x setup-https-squid-proxy.sh && ./setup-https-squid-proxy.sh
setup-https-squid-proxy.sh
· 15 KiB · Bash
Raw
#!/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 <<EOF
# ============================================================
# Squid HTTPS/TLS encrypted forward proxy
# Generated by setup-https-squid-proxy.sh
#
# Domain: $domain
# Port: $port
#
# IMPORTANT:
# In FoxyProxy, use host "$domain", not the server IP.
# ============================================================
$https_line
auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd
auth_param basic realm HTTPS Proxy
auth_param basic credentialsttl 2 hours
acl authenticated proxy_auth REQUIRED
acl SSL_ports port 443
acl Safe_ports port 80
acl Safe_ports port 443
acl Safe_ports port 1025-65535
acl CONNECT method CONNECT
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow authenticated
http_access deny all
cache deny all
cache_mem 8 MB
maximum_object_size 0 KB
via off
forwarded_for delete
request_header_access X-Forwarded-For deny all
request_header_access Via deny all
request_header_access Cache-Control deny all
access_log /var/log/squid/access.log
cache_log /var/log/squid/cache.log
cache_effective_user $SQUID_USER
cache_effective_group $SQUID_GROUP
visible_hostname $domain
EOF
}
find_working_squid_syntax() {
local domain="$1"
local port="$2"
write_squid_config "$domain" "$port" "new"
if squid -k parse >/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 <<EOF
#!/bin/bash
set -e
DOMAIN="$domain"
SRC_CERT="/etc/letsencrypt/live/\$DOMAIN/fullchain.pem"
SRC_KEY="/etc/letsencrypt/live/\$DOMAIN/privkey.pem"
DST_DIR="/etc/squid/certs"
DST_CERT="\$DST_DIR/proxy-fullchain.pem"
DST_KEY="\$DST_DIR/proxy-privkey.pem"
mkdir -p "\$DST_DIR"
cp "\$SRC_CERT" "\$DST_CERT"
cp "\$SRC_KEY" "\$DST_KEY"
chown $SQUID_USER:$SQUID_GROUP "\$DST_CERT" "\$DST_KEY"
chmod 644 "\$DST_CERT"
chmod 600 "\$DST_KEY"
systemctl reload squid || systemctl restart squid
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh
log "Renewal hook created at /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh"
}
restart_squid_cleanly() {
info "Restarting Squid cleanly..."
systemctl stop squid >/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 "$@"
| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ============================================================ |
| 5 | # Interactive Squid HTTPS Proxy Installer with Let's Encrypt |
| 6 | # Debian 12/13 |
| 7 | # |
| 8 | # Creates: |
| 9 | # Firefox/FoxyProxy -> HTTPS proxy -> Squid -> Internet |
| 10 | # |
| 11 | # IMPORTANT: |
| 12 | # In FoxyProxy, use the DOMAIN NAME, not the IP address. |
| 13 | # ============================================================ |
| 14 | |
| 15 | RED="\033[31m" |
| 16 | GREEN="\033[32m" |
| 17 | YELLOW="\033[33m" |
| 18 | BLUE="\033[34m" |
| 19 | RESET="\033[0m" |
| 20 | |
| 21 | log() { echo -e "${GREEN}[OK]${RESET} $*"; } |
| 22 | info() { echo -e "${BLUE}[INFO]${RESET} $*"; } |
| 23 | warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } |
| 24 | die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } |
| 25 | |
| 26 | SQUID_USER="proxy" |
| 27 | SQUID_GROUP="proxy" |
| 28 | STOPPED_SERVICES=() |
| 29 | |
| 30 | require_root() { |
| 31 | if [[ "${EUID}" -ne 0 ]]; then |
| 32 | die "Run this script as root." |
| 33 | fi |
| 34 | } |
| 35 | |
| 36 | ask() { |
| 37 | local prompt="$1" |
| 38 | local default="${2:-}" |
| 39 | local value |
| 40 | |
| 41 | if [[ -n "$default" ]]; then |
| 42 | read -rp "$prompt [$default]: " value |
| 43 | echo "${value:-$default}" |
| 44 | else |
| 45 | read -rp "$prompt: " value |
| 46 | echo "$value" |
| 47 | fi |
| 48 | } |
| 49 | |
| 50 | ask_password_confirmed() { |
| 51 | local pass1 pass2 |
| 52 | |
| 53 | while true; do |
| 54 | read -rsp "Proxy password: " pass1 |
| 55 | printf '\n' >&2 |
| 56 | |
| 57 | read -rsp "Confirm proxy password: " pass2 |
| 58 | printf '\n' >&2 |
| 59 | |
| 60 | if [[ -z "$pass1" ]]; then |
| 61 | warn "Password cannot be empty." >&2 |
| 62 | continue |
| 63 | fi |
| 64 | |
| 65 | if [[ "$pass1" != "$pass2" ]]; then |
| 66 | warn "Passwords do not match." >&2 |
| 67 | continue |
| 68 | fi |
| 69 | |
| 70 | printf '%s' "$pass1" |
| 71 | return 0 |
| 72 | done |
| 73 | } |
| 74 | |
| 75 | validate_domain() { |
| 76 | local domain="$1" |
| 77 | |
| 78 | if [[ ! "$domain" =~ ^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then |
| 79 | die "Invalid domain: $domain" |
| 80 | fi |
| 81 | |
| 82 | if [[ "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then |
| 83 | die "Do not use an IP address. Use a real hostname, e.g. proxy.example.com" |
| 84 | fi |
| 85 | } |
| 86 | |
| 87 | validate_port() { |
| 88 | local port="$1" |
| 89 | |
| 90 | if ! [[ "$port" =~ ^[0-9]+$ ]]; then |
| 91 | die "Invalid port: $port" |
| 92 | fi |
| 93 | |
| 94 | if (( port < 1 || port > 65535 )); then |
| 95 | die "Port must be between 1 and 65535." |
| 96 | fi |
| 97 | } |
| 98 | |
| 99 | detect_proxy_user_group() { |
| 100 | if id proxy >/dev/null 2>&1; then |
| 101 | SQUID_USER="proxy" |
| 102 | elif id squid >/dev/null 2>&1; then |
| 103 | SQUID_USER="squid" |
| 104 | else |
| 105 | SQUID_USER="proxy" |
| 106 | fi |
| 107 | |
| 108 | if getent group proxy >/dev/null 2>&1; then |
| 109 | SQUID_GROUP="proxy" |
| 110 | elif getent group squid >/dev/null 2>&1; then |
| 111 | SQUID_GROUP="squid" |
| 112 | else |
| 113 | SQUID_GROUP="$SQUID_USER" |
| 114 | fi |
| 115 | } |
| 116 | |
| 117 | install_packages() { |
| 118 | info "Installing required packages..." |
| 119 | |
| 120 | apt update |
| 121 | |
| 122 | if apt-cache show squid-openssl >/dev/null 2>&1; then |
| 123 | apt install -y squid-openssl apache2-utils certbot openssl curl ca-certificates dnsutils |
| 124 | else |
| 125 | warn "squid-openssl package was not found." |
| 126 | warn "Installing normal squid, but HTTPS proxy listener may not work." |
| 127 | apt install -y squid apache2-utils certbot openssl curl ca-certificates dnsutils |
| 128 | fi |
| 129 | |
| 130 | log "Packages installed." |
| 131 | } |
| 132 | |
| 133 | check_squid_openssl() { |
| 134 | info "Checking Squid OpenSSL support..." |
| 135 | |
| 136 | if squid -v 2>/dev/null | grep -qi openssl; then |
| 137 | log "Squid appears to have OpenSSL support." |
| 138 | else |
| 139 | warn "Could not confirm OpenSSL support from squid -v." |
| 140 | warn "If https_port fails later, install squid-openssl or use a TLS wrapper." |
| 141 | fi |
| 142 | } |
| 143 | |
| 144 | check_dns() { |
| 145 | local domain="$1" |
| 146 | |
| 147 | info "Checking DNS for $domain..." |
| 148 | |
| 149 | local resolved_v4="" |
| 150 | local resolved_v6="" |
| 151 | |
| 152 | resolved_v4="$(dig +short A "$domain" | tail -n1 || true)" |
| 153 | resolved_v6="$(dig +short AAAA "$domain" | tail -n1 || true)" |
| 154 | |
| 155 | if [[ -z "$resolved_v4" && -z "$resolved_v6" ]]; then |
| 156 | warn "No A or AAAA record found for $domain." |
| 157 | warn "Let's Encrypt will fail unless this hostname points to this server." |
| 158 | read -rp "Continue anyway? [y/N]: " yn |
| 159 | [[ "${yn,,}" == "y" ]] || die "Stopped." |
| 160 | else |
| 161 | if [[ -n "$resolved_v4" ]]; then |
| 162 | log "$domain A record: $resolved_v4" |
| 163 | fi |
| 164 | |
| 165 | if [[ -n "$resolved_v6" ]]; then |
| 166 | log "$domain AAAA record: $resolved_v6" |
| 167 | else |
| 168 | warn "$domain has no AAAA record. That is fine if you only want IPv4." |
| 169 | fi |
| 170 | fi |
| 171 | |
| 172 | return 0 |
| 173 | } |
| 174 | |
| 175 | open_firewall() { |
| 176 | local port="$1" |
| 177 | |
| 178 | info "Attempting to open firewall ports 80 and $port..." |
| 179 | |
| 180 | if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qi active; then |
| 181 | ufw allow 80/tcp || true |
| 182 | ufw allow "$port/tcp" || true |
| 183 | log "UFW rules added." |
| 184 | else |
| 185 | warn "UFW not active or not installed. Skipping UFW rules." |
| 186 | fi |
| 187 | |
| 188 | if command -v iptables >/dev/null 2>&1; then |
| 189 | iptables -C INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport 80 -j ACCEPT |
| 190 | iptables -C INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null || iptables -I INPUT -p tcp --dport "$port" -j ACCEPT |
| 191 | log "Temporary iptables rules added." |
| 192 | warn "iptables rules may not persist after reboot unless your VPS image saves them." |
| 193 | fi |
| 194 | } |
| 195 | |
| 196 | stop_port_80_services_if_needed() { |
| 197 | STOPPED_SERVICES=() |
| 198 | |
| 199 | if ss -tulpn | grep -qE '(:80[[:space:]]|:80$|:80,)'; then |
| 200 | warn "Something appears to be using port 80." |
| 201 | echo |
| 202 | ss -tulpn | grep ':80' || true |
| 203 | echo |
| 204 | |
| 205 | read -rp "Temporarily stop nginx/apache2/caddy if running so Certbot can use port 80? [Y/n]: " yn |
| 206 | yn="${yn:-Y}" |
| 207 | |
| 208 | if [[ "${yn,,}" == "y" ]]; then |
| 209 | for svc in nginx apache2 caddy; do |
| 210 | if systemctl is-active --quiet "$svc" 2>/dev/null; then |
| 211 | systemctl stop "$svc" |
| 212 | STOPPED_SERVICES+=("systemd:$svc") |
| 213 | warn "Stopped $svc temporarily." |
| 214 | fi |
| 215 | done |
| 216 | fi |
| 217 | fi |
| 218 | } |
| 219 | |
| 220 | restart_stopped_services() { |
| 221 | if [[ ${#STOPPED_SERVICES[@]} -gt 0 ]]; then |
| 222 | info "Restarting services stopped for Certbot..." |
| 223 | |
| 224 | for item in "${STOPPED_SERVICES[@]}"; do |
| 225 | kind="${item%%:*}" |
| 226 | name="${item#*:}" |
| 227 | |
| 228 | if [[ "$kind" == "systemd" ]]; then |
| 229 | systemctl start "$name" || warn "Could not restart $name." |
| 230 | fi |
| 231 | done |
| 232 | fi |
| 233 | } |
| 234 | |
| 235 | get_certificate() { |
| 236 | local domain="$1" |
| 237 | local email="$2" |
| 238 | |
| 239 | if [[ -f "/etc/letsencrypt/live/$domain/fullchain.pem" && -f "/etc/letsencrypt/live/$domain/privkey.pem" ]]; then |
| 240 | log "Existing Let's Encrypt cert found for $domain." |
| 241 | read -rp "Use existing certificate? [Y/n]: " yn |
| 242 | yn="${yn:-Y}" |
| 243 | |
| 244 | if [[ "${yn,,}" == "y" ]]; then |
| 245 | return 0 |
| 246 | fi |
| 247 | fi |
| 248 | |
| 249 | stop_port_80_services_if_needed |
| 250 | |
| 251 | info "Requesting Let's Encrypt certificate for $domain..." |
| 252 | |
| 253 | if [[ -n "$email" ]]; then |
| 254 | certbot certonly --standalone \ |
| 255 | -d "$domain" \ |
| 256 | --non-interactive \ |
| 257 | --agree-tos \ |
| 258 | --email "$email" |
| 259 | else |
| 260 | certbot certonly --standalone \ |
| 261 | -d "$domain" \ |
| 262 | --non-interactive \ |
| 263 | --agree-tos \ |
| 264 | --register-unsafely-without-email |
| 265 | fi |
| 266 | |
| 267 | restart_stopped_services |
| 268 | |
| 269 | log "Certificate issued." |
| 270 | } |
| 271 | |
| 272 | copy_certs() { |
| 273 | local domain="$1" |
| 274 | |
| 275 | info "Copying certificate files into /etc/squid/certs..." |
| 276 | |
| 277 | mkdir -p /etc/squid/certs |
| 278 | |
| 279 | cp "/etc/letsencrypt/live/$domain/fullchain.pem" /etc/squid/certs/proxy-fullchain.pem |
| 280 | cp "/etc/letsencrypt/live/$domain/privkey.pem" /etc/squid/certs/proxy-privkey.pem |
| 281 | |
| 282 | chown -R "$SQUID_USER:$SQUID_GROUP" /etc/squid/certs |
| 283 | chmod 644 /etc/squid/certs/proxy-fullchain.pem |
| 284 | chmod 600 /etc/squid/certs/proxy-privkey.pem |
| 285 | |
| 286 | log "Certificates copied." |
| 287 | } |
| 288 | |
| 289 | create_auth() { |
| 290 | local username="$1" |
| 291 | local password="$2" |
| 292 | |
| 293 | info "Creating Squid username/password auth..." |
| 294 | |
| 295 | touch /etc/squid/passwd |
| 296 | chown "$SQUID_USER:$SQUID_GROUP" /etc/squid/passwd |
| 297 | chmod 640 /etc/squid/passwd |
| 298 | |
| 299 | printf '%s\n' "$password" | htpasswd -m -i /etc/squid/passwd "$username" >/dev/null |
| 300 | |
| 301 | chown "$SQUID_USER:$SQUID_GROUP" /etc/squid/passwd |
| 302 | chmod 640 /etc/squid/passwd |
| 303 | |
| 304 | log "Proxy user created: $username" |
| 305 | } |
| 306 | |
| 307 | test_auth_helper() { |
| 308 | local username="$1" |
| 309 | local password="$2" |
| 310 | |
| 311 | info "Testing Squid auth helper directly..." |
| 312 | |
| 313 | local result |
| 314 | result="$(printf '%s %s\n' "$username" "$password" | /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd || true)" |
| 315 | |
| 316 | if echo "$result" | grep -q '^OK'; then |
| 317 | log "Squid auth helper accepted the username/password." |
| 318 | else |
| 319 | warn "Squid auth helper did not accept the username/password." |
| 320 | echo "Auth helper output:" |
| 321 | echo "$result" |
| 322 | die "Stopping because proxy auth would fail." |
| 323 | fi |
| 324 | } |
| 325 | |
| 326 | write_squid_config() { |
| 327 | local domain="$1" |
| 328 | local port="$2" |
| 329 | local syntax="$3" |
| 330 | |
| 331 | local https_line |
| 332 | |
| 333 | if [[ "$syntax" == "new" ]]; then |
| 334 | https_line="https_port $port tls-cert=/etc/squid/certs/proxy-fullchain.pem tls-key=/etc/squid/certs/proxy-privkey.pem" |
| 335 | else |
| 336 | https_line="https_port $port cert=/etc/squid/certs/proxy-fullchain.pem key=/etc/squid/certs/proxy-privkey.pem" |
| 337 | fi |
| 338 | |
| 339 | info "Writing Squid config using syntax: $syntax" |
| 340 | |
| 341 | if [[ -f /etc/squid/squid.conf && ! -f /etc/squid/squid.conf.bak-before-https-proxy ]]; then |
| 342 | cp /etc/squid/squid.conf /etc/squid/squid.conf.bak-before-https-proxy |
| 343 | log "Backed up original config to /etc/squid/squid.conf.bak-before-https-proxy" |
| 344 | fi |
| 345 | |
| 346 | cat > /etc/squid/squid.conf <<EOF |
| 347 | # ============================================================ |
| 348 | # Squid HTTPS/TLS encrypted forward proxy |
| 349 | # Generated by setup-https-squid-proxy.sh |
| 350 | # |
| 351 | # Domain: $domain |
| 352 | # Port: $port |
| 353 | # |
| 354 | # IMPORTANT: |
| 355 | # In FoxyProxy, use host "$domain", not the server IP. |
| 356 | # ============================================================ |
| 357 | |
| 358 | $https_line |
| 359 | |
| 360 | auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd |
| 361 | auth_param basic realm HTTPS Proxy |
| 362 | auth_param basic credentialsttl 2 hours |
| 363 | |
| 364 | acl authenticated proxy_auth REQUIRED |
| 365 | |
| 366 | acl SSL_ports port 443 |
| 367 | acl Safe_ports port 80 |
| 368 | acl Safe_ports port 443 |
| 369 | acl Safe_ports port 1025-65535 |
| 370 | acl CONNECT method CONNECT |
| 371 | |
| 372 | http_access deny !Safe_ports |
| 373 | http_access deny CONNECT !SSL_ports |
| 374 | http_access allow authenticated |
| 375 | http_access deny all |
| 376 | |
| 377 | cache deny all |
| 378 | cache_mem 8 MB |
| 379 | maximum_object_size 0 KB |
| 380 | |
| 381 | via off |
| 382 | forwarded_for delete |
| 383 | request_header_access X-Forwarded-For deny all |
| 384 | request_header_access Via deny all |
| 385 | request_header_access Cache-Control deny all |
| 386 | |
| 387 | access_log /var/log/squid/access.log |
| 388 | cache_log /var/log/squid/cache.log |
| 389 | |
| 390 | cache_effective_user $SQUID_USER |
| 391 | cache_effective_group $SQUID_GROUP |
| 392 | |
| 393 | visible_hostname $domain |
| 394 | EOF |
| 395 | } |
| 396 | |
| 397 | find_working_squid_syntax() { |
| 398 | local domain="$1" |
| 399 | local port="$2" |
| 400 | |
| 401 | write_squid_config "$domain" "$port" "new" |
| 402 | |
| 403 | if squid -k parse >/tmp/squid-parse.log 2>&1; then |
| 404 | log "Squid config parsed successfully with tls-cert/tls-key syntax." |
| 405 | return 0 |
| 406 | fi |
| 407 | |
| 408 | warn "tls-cert/tls-key syntax failed. Trying older cert/key syntax..." |
| 409 | cat /tmp/squid-parse.log || true |
| 410 | |
| 411 | write_squid_config "$domain" "$port" "old" |
| 412 | |
| 413 | if squid -k parse >/tmp/squid-parse.log 2>&1; then |
| 414 | log "Squid config parsed successfully with cert/key syntax." |
| 415 | return 0 |
| 416 | fi |
| 417 | |
| 418 | cat /tmp/squid-parse.log || true |
| 419 | die "Squid could not parse either HTTPS config style. Your Squid build may not support https_port/OpenSSL." |
| 420 | } |
| 421 | |
| 422 | write_renewal_hook() { |
| 423 | local domain="$1" |
| 424 | |
| 425 | info "Creating Certbot renewal hook..." |
| 426 | |
| 427 | mkdir -p /etc/letsencrypt/renewal-hooks/deploy |
| 428 | |
| 429 | cat > /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh <<EOF |
| 430 | #!/bin/bash |
| 431 | set -e |
| 432 | |
| 433 | DOMAIN="$domain" |
| 434 | |
| 435 | SRC_CERT="/etc/letsencrypt/live/\$DOMAIN/fullchain.pem" |
| 436 | SRC_KEY="/etc/letsencrypt/live/\$DOMAIN/privkey.pem" |
| 437 | |
| 438 | DST_DIR="/etc/squid/certs" |
| 439 | DST_CERT="\$DST_DIR/proxy-fullchain.pem" |
| 440 | DST_KEY="\$DST_DIR/proxy-privkey.pem" |
| 441 | |
| 442 | mkdir -p "\$DST_DIR" |
| 443 | |
| 444 | cp "\$SRC_CERT" "\$DST_CERT" |
| 445 | cp "\$SRC_KEY" "\$DST_KEY" |
| 446 | |
| 447 | chown $SQUID_USER:$SQUID_GROUP "\$DST_CERT" "\$DST_KEY" |
| 448 | chmod 644 "\$DST_CERT" |
| 449 | chmod 600 "\$DST_KEY" |
| 450 | |
| 451 | systemctl reload squid || systemctl restart squid |
| 452 | EOF |
| 453 | |
| 454 | chmod +x /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh |
| 455 | |
| 456 | log "Renewal hook created at /etc/letsencrypt/renewal-hooks/deploy/squid-cert-copy.sh" |
| 457 | } |
| 458 | |
| 459 | restart_squid_cleanly() { |
| 460 | info "Restarting Squid cleanly..." |
| 461 | |
| 462 | systemctl stop squid >/dev/null 2>&1 || true |
| 463 | sleep 2 |
| 464 | |
| 465 | pkill -9 squid >/dev/null 2>&1 || true |
| 466 | sleep 1 |
| 467 | |
| 468 | systemctl start squid |
| 469 | |
| 470 | if systemctl is-active --quiet squid; then |
| 471 | systemctl enable squid >/dev/null 2>&1 || true |
| 472 | log "Squid is running." |
| 473 | else |
| 474 | systemctl status squid --no-pager -l || true |
| 475 | tail -100 /var/log/squid/cache.log || true |
| 476 | die "Squid failed to start." |
| 477 | fi |
| 478 | } |
| 479 | |
| 480 | test_listener() { |
| 481 | local port="$1" |
| 482 | |
| 483 | info "Checking if Squid is listening on port $port..." |
| 484 | |
| 485 | if ss -tulpn | grep -q ":$port"; then |
| 486 | log "Something is listening on port $port:" |
| 487 | ss -tulpn | grep ":$port" || true |
| 488 | else |
| 489 | warn "Port $port does not appear in ss output." |
| 490 | fi |
| 491 | } |
| 492 | |
| 493 | test_proxy() { |
| 494 | local domain="$1" |
| 495 | local port="$2" |
| 496 | local username="$3" |
| 497 | local password="$4" |
| 498 | |
| 499 | info "Testing proxy with curl..." |
| 500 | |
| 501 | set +e |
| 502 | local result |
| 503 | result="$(curl -4 -sS --max-time 30 \ |
| 504 | --proxy "https://$domain:$port" \ |
| 505 | --proxy-user "$username:$password" \ |
| 506 | https://chy.li/ip 2>&1)" |
| 507 | local rc=$? |
| 508 | set -e |
| 509 | |
| 510 | if [[ $rc -eq 0 ]]; then |
| 511 | log "Proxy test succeeded. Outgoing IP:" |
| 512 | echo "$result" |
| 513 | else |
| 514 | warn "Curl proxy test failed." |
| 515 | echo "$result" |
| 516 | echo |
| 517 | warn "Check logs:" |
| 518 | echo " tail -f /var/log/squid/cache.log" |
| 519 | echo " tail -f /var/log/squid/access.log" |
| 520 | fi |
| 521 | } |
| 522 | |
| 523 | print_summary() { |
| 524 | local domain="$1" |
| 525 | local port="$2" |
| 526 | local username="$3" |
| 527 | |
| 528 | echo |
| 529 | echo "============================================================" |
| 530 | echo " HTTPS Squid Proxy Setup Complete" |
| 531 | echo "============================================================" |
| 532 | echo |
| 533 | echo "FoxyProxy settings:" |
| 534 | echo |
| 535 | echo " Proxy Type: HTTPS" |
| 536 | echo " Host: $domain" |
| 537 | echo " Port: $port" |
| 538 | echo " Username: $username" |
| 539 | echo " Password: the password you entered" |
| 540 | echo |
| 541 | echo "IMPORTANT:" |
| 542 | echo " Use the hostname \"$domain\" in FoxyProxy." |
| 543 | echo " Do NOT use the server IP address." |
| 544 | echo |
| 545 | echo "Test command:" |
| 546 | echo |
| 547 | echo " curl -v \\" |
| 548 | echo " --proxy \"https://$domain:$port\" \\" |
| 549 | echo " --proxy-user \"$username:YOUR_PASSWORD\" \\" |
| 550 | echo " https://chy.li/ip" |
| 551 | echo |
| 552 | echo "Useful logs:" |
| 553 | echo |
| 554 | echo " tail -f /var/log/squid/cache.log" |
| 555 | echo " tail -f /var/log/squid/access.log" |
| 556 | echo |
| 557 | echo "============================================================" |
| 558 | } |
| 559 | |
| 560 | main() { |
| 561 | require_root |
| 562 | |
| 563 | echo |
| 564 | echo "============================================================" |
| 565 | echo " Interactive Squid HTTPS Proxy + Let's Encrypt Installer" |
| 566 | echo "============================================================" |
| 567 | echo |
| 568 | |
| 569 | DOMAIN="$(ask "Proxy domain, e.g. proxy.example.com")" |
| 570 | validate_domain "$DOMAIN" |
| 571 | |
| 572 | PORT="$(ask "HTTPS proxy port" "8443")" |
| 573 | validate_port "$PORT" |
| 574 | |
| 575 | USERNAME="$(ask "Proxy username" "proxyuser")" |
| 576 | if [[ -z "$USERNAME" ]]; then |
| 577 | die "Username cannot be empty." |
| 578 | fi |
| 579 | |
| 580 | PASSWORD="$(ask_password_confirmed)" |
| 581 | |
| 582 | EMAIL="$(ask "Email for Let's Encrypt renewal notices, or leave blank" "")" |
| 583 | |
| 584 | echo |
| 585 | echo "Summary:" |
| 586 | echo " Domain: $DOMAIN" |
| 587 | echo " Port: $PORT" |
| 588 | echo " Username: $USERNAME" |
| 589 | if [[ -n "$EMAIL" ]]; then |
| 590 | echo " LE Email: $EMAIL" |
| 591 | else |
| 592 | echo " LE Email: none" |
| 593 | fi |
| 594 | echo |
| 595 | |
| 596 | read -rp "Continue with install? [Y/n]: " confirm |
| 597 | confirm="${confirm:-Y}" |
| 598 | [[ "${confirm,,}" == "y" ]] || die "Cancelled." |
| 599 | |
| 600 | install_packages |
| 601 | detect_proxy_user_group |
| 602 | |
| 603 | info "Detected Squid user/group: $SQUID_USER:$SQUID_GROUP" |
| 604 | |
| 605 | check_squid_openssl |
| 606 | check_dns "$DOMAIN" |
| 607 | open_firewall "$PORT" |
| 608 | get_certificate "$DOMAIN" "$EMAIL" |
| 609 | copy_certs "$DOMAIN" |
| 610 | create_auth "$USERNAME" "$PASSWORD" |
| 611 | test_auth_helper "$USERNAME" "$PASSWORD" |
| 612 | find_working_squid_syntax "$DOMAIN" "$PORT" |
| 613 | write_renewal_hook "$DOMAIN" |
| 614 | restart_squid_cleanly |
| 615 | test_listener "$PORT" |
| 616 | test_proxy "$DOMAIN" "$PORT" "$USERNAME" "$PASSWORD" |
| 617 | print_summary "$DOMAIN" "$PORT" "$USERNAME" |
| 618 | } |
| 619 | |
| 620 | main "$@" |
| 621 |