Last active 1 month ago

Revision cd47f8411e58d0fb136e09614f4299cc76720c70

readme.md Raw

wget -qO- 'https://beanman.net/gist/beanman109/setup-https-squid-proxy/raw/HEAD/setup-https-squid-proxy.sh' | bash

setup-https-squid-proxy.sh Raw
1#!/usr/bin/env bash
2set -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
15RED="\033[31m"
16GREEN="\033[32m"
17YELLOW="\033[33m"
18BLUE="\033[34m"
19RESET="\033[0m"
20
21log() { echo -e "${GREEN}[OK]${RESET} $*"; }
22info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
23warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
24die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
25
26SQUID_USER="proxy"
27SQUID_GROUP="proxy"
28STOPPED_SERVICES=()
29
30require_root() {
31 if [[ "${EUID}" -ne 0 ]]; then
32 die "Run this script as root."
33 fi
34}
35
36ask() {
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
50ask_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
75validate_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
87validate_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
99detect_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
117install_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
133check_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
144check_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
175open_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
196stop_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
220restart_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
235get_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
272copy_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
289create_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
307test_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
326write_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
360auth_param basic program /usr/lib/squid/basic_ncsa_auth /etc/squid/passwd
361auth_param basic realm HTTPS Proxy
362auth_param basic credentialsttl 2 hours
363
364acl authenticated proxy_auth REQUIRED
365
366acl SSL_ports port 443
367acl Safe_ports port 80
368acl Safe_ports port 443
369acl Safe_ports port 1025-65535
370acl CONNECT method CONNECT
371
372http_access deny !Safe_ports
373http_access deny CONNECT !SSL_ports
374http_access allow authenticated
375http_access deny all
376
377cache deny all
378cache_mem 8 MB
379maximum_object_size 0 KB
380
381via off
382forwarded_for delete
383request_header_access X-Forwarded-For deny all
384request_header_access Via deny all
385request_header_access Cache-Control deny all
386
387access_log /var/log/squid/access.log
388cache_log /var/log/squid/cache.log
389
390cache_effective_user $SQUID_USER
391cache_effective_group $SQUID_GROUP
392
393visible_hostname $domain
394EOF
395}
396
397find_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
422write_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
431set -e
432
433DOMAIN="$domain"
434
435SRC_CERT="/etc/letsencrypt/live/\$DOMAIN/fullchain.pem"
436SRC_KEY="/etc/letsencrypt/live/\$DOMAIN/privkey.pem"
437
438DST_DIR="/etc/squid/certs"
439DST_CERT="\$DST_DIR/proxy-fullchain.pem"
440DST_KEY="\$DST_DIR/proxy-privkey.pem"
441
442mkdir -p "\$DST_DIR"
443
444cp "\$SRC_CERT" "\$DST_CERT"
445cp "\$SRC_KEY" "\$DST_KEY"
446
447chown $SQUID_USER:$SQUID_GROUP "\$DST_CERT" "\$DST_KEY"
448chmod 644 "\$DST_CERT"
449chmod 600 "\$DST_KEY"
450
451systemctl reload squid || systemctl restart squid
452EOF
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
459restart_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
480test_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
493test_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
523print_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
560main() {
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
620main "$@"
621