Add native SOCKS4/SOCKS5 support for Squid cache_peer #32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Squid SOCKS Patch Build & Test | |
| on: | |
| push: | |
| paths: | |
| - 'squid_patch/**' | |
| - 'setup/**' | |
| - 'template/**' | |
| - '.github/workflows/squid-build-test.yml' | |
| pull_request: | |
| paths: | |
| - 'squid_patch/**' | |
| - 'setup/**' | |
| - 'template/**' | |
| - '.github/workflows/squid-build-test.yml' | |
| workflow_dispatch: | |
| env: | |
| SQUID_IMAGE: squid-socks:6.10 | |
| jobs: | |
| # ------------------------------------------------------------------ | |
| # 1. Build the custom Squid image with SOCKS patch | |
| # ------------------------------------------------------------------ | |
| build: | |
| name: Build Squid 6.10 + SOCKS Patch | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build Squid image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: ./squid_patch | |
| file: ./squid_patch/Dockerfile | |
| tags: ${{ env.SQUID_IMAGE }} | |
| load: true | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| - name: Verify Squid binary | |
| run: | | |
| docker run --rm ${{ env.SQUID_IMAGE }} squid -v | |
| echo "--- Squid binary OK ---" | |
| - name: Save image for test jobs | |
| run: docker save ${{ env.SQUID_IMAGE }} -o /tmp/squid-image.tar | |
| - name: Upload image artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: squid-image | |
| path: /tmp/squid-image.tar | |
| retention-days: 1 | |
| # ------------------------------------------------------------------ | |
| # 2. Test: Squid config parsing (socks4/socks5 options) | |
| # ------------------------------------------------------------------ | |
| test-config: | |
| name: Test Squid Config Parsing | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: squid-image | |
| path: /tmp | |
| - name: Load image | |
| run: docker load -i /tmp/squid-image.tar | |
| - name: Test SOCKS5 cache_peer config parsing | |
| run: | | |
| cat > /tmp/squid-socks5.conf <<'CONF' | |
| http_port 3128 | |
| acl all src 0.0.0.0/0 | |
| http_access allow all | |
| never_direct allow all | |
| cache_peer 127.0.0.1 parent 1080 0 no-query no-digest round-robin proxy-only originserver name=test_socks5 socks5 socks-user=testuser socks-pass=testpass | |
| CONF | |
| docker run --rm \ | |
| -v /tmp/squid-socks5.conf:/etc/squid/conf.d/squid.conf:ro \ | |
| ${{ env.SQUID_IMAGE }} \ | |
| squid -k parse -f /etc/squid/conf.d/squid.conf 2>&1 | tee /tmp/parse-output.txt | |
| echo "--- SOCKS5 config parse OK ---" | |
| - name: Test SOCKS4 cache_peer config parsing | |
| run: | | |
| cat > /tmp/squid-socks4.conf <<'CONF' | |
| http_port 3128 | |
| acl all src 0.0.0.0/0 | |
| http_access allow all | |
| never_direct allow all | |
| cache_peer 127.0.0.1 parent 1080 0 no-query no-digest round-robin proxy-only originserver name=test_socks4 socks4 | |
| CONF | |
| docker run --rm \ | |
| -v /tmp/squid-socks4.conf:/etc/squid/conf.d/squid.conf:ro \ | |
| ${{ env.SQUID_IMAGE }} \ | |
| squid -k parse -f /etc/squid/conf.d/squid.conf 2>&1 | tee /tmp/parse-output.txt | |
| echo "--- SOCKS4 config parse OK ---" | |
| - name: Test multiple SOCKS peers config | |
| run: | | |
| cat > /tmp/squid-multi.conf <<'CONF' | |
| http_port 3128 | |
| acl all src 0.0.0.0/0 | |
| http_access allow all | |
| never_direct allow all | |
| cache_peer 10.0.0.1 parent 1080 0 no-query no-digest round-robin proxy-only originserver name=socks1 socks5 socks-user=user1 socks-pass=pass1 | |
| cache_peer 10.0.0.2 parent 1080 0 no-query no-digest round-robin proxy-only originserver name=socks2 socks5 socks-user=user2 socks-pass=pass2 | |
| cache_peer 10.0.0.3 parent 1081 0 no-query no-digest round-robin proxy-only originserver name=socks3 socks4 | |
| CONF | |
| docker run --rm \ | |
| -v /tmp/squid-multi.conf:/etc/squid/conf.d/squid.conf:ro \ | |
| ${{ env.SQUID_IMAGE }} \ | |
| squid -k parse -f /etc/squid/conf.d/squid.conf 2>&1 | |
| echo "--- Multiple SOCKS peers config OK ---" | |
| # ------------------------------------------------------------------ | |
| # 3. Test: SOCKS5 proxy end-to-end via Squid | |
| # ------------------------------------------------------------------ | |
| test-socks5-e2e: | |
| name: Test SOCKS5 End-to-End | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: squid-image | |
| path: /tmp | |
| - name: Load image | |
| run: docker load -i /tmp/squid-image.tar | |
| - name: Start local HTTP test server | |
| run: | | |
| mkdir -p /tmp/www | |
| echo '{"origin":"127.0.0.1","test":"ok"}' > /tmp/www/ip | |
| python3 -m http.server 18080 --directory /tmp/www & | |
| sleep 1 | |
| curl -sf http://127.0.0.1:18080/ip | |
| echo "--- Local HTTP server ready ---" | |
| - name: Build and start SOCKS5 test server (microsocks) | |
| run: | | |
| git clone --depth 1 https://github.com/rofl0r/microsocks.git /tmp/microsocks | |
| cd /tmp/microsocks && make -j"$(nproc)" | |
| /tmp/microsocks/microsocks -p 11080 & | |
| sleep 1 | |
| echo "--- Verify SOCKS5 server directly ---" | |
| curl -sf --socks5-hostname 127.0.0.1:11080 http://127.0.0.1:18080/ip || { | |
| echo "ERROR: SOCKS5 server not working" | |
| exit 1 | |
| } | |
| echo "--- SOCKS5 server OK ---" | |
| - name: Create Squid config for SOCKS5 peer | |
| run: | | |
| mkdir -p /tmp/squid-conf | |
| cat > /tmp/squid-conf/squid.conf <<CONF | |
| http_port 3128 | |
| acl all src 0.0.0.0/0 | |
| http_access allow all | |
| never_direct allow all | |
| server_persistent_connections off | |
| client_persistent_connections off | |
| cache_peer 127.0.0.1 parent 11080 0 no-query no-digest connect-fail-limit=2 connect-timeout=8 round-robin proxy-only originserver name=socks_test socks5 | |
| visible_hostname test | |
| cache deny all | |
| access_log stdio:/proc/self/fd/1 combined | |
| CONF | |
| echo "--- Config ---" | |
| cat /tmp/squid-conf/squid.conf | |
| - name: Start Squid with SOCKS5 peer | |
| run: | | |
| # Bypass entrypoint - run squid directly to isolate issues | |
| docker run -d --name squid-test --network host \ | |
| -v /tmp/squid-conf:/etc/squid/conf.d:ro \ | |
| --entrypoint /bin/sh \ | |
| ${{ env.SQUID_IMAGE }} \ | |
| -c ' | |
| squid -z -N -f /etc/squid/conf.d/squid.conf 2>&1 || true | |
| echo "=== Starting Squid ===" | |
| exec squid -N -f /etc/squid/conf.d/squid.conf 2>&1 | |
| ' | |
| sleep 5 | |
| echo "=== Container status ===" | |
| docker ps -a --filter name=squid-test --format '{{.Status}}' | |
| if ! docker ps --filter name=squid-test --filter status=running -q | grep -q .; then | |
| echo "ERROR: Squid container is not running" | |
| docker logs squid-test 2>&1 || true | |
| exit 1 | |
| fi | |
| echo "=== Early Squid logs ===" | |
| docker logs squid-test 2>&1 || true | |
| - name: Wait for Squid to listen | |
| run: | | |
| echo "Waiting for Squid to listen on port 3128..." | |
| for i in $(seq 1 30); do | |
| if bash -c 'echo > /dev/tcp/127.0.0.1/3128' 2>/dev/null; then | |
| echo "Squid is listening after ${i}s" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "ERROR: Squid never started listening" | |
| docker logs squid-test 2>&1 || true | |
| exit 1 | |
| - name: Verify HTTP server is still up | |
| run: curl -sf http://127.0.0.1:18080/ip | |
| - name: Test HTTP request through SOCKS5 peer | |
| run: | | |
| echo "--- Attempting proxy request ---" | |
| HTTP_CODE=$(curl -s -o /tmp/proxy-response.txt -w '%{http_code}' --max-time 15 -x http://127.0.0.1:3128 http://127.0.0.1:18080/ip 2>/tmp/proxy-stderr.txt || true) | |
| echo "HTTP status: ${HTTP_CODE}" | |
| echo "Response body:" | |
| cat /tmp/proxy-response.txt || true | |
| echo "" | |
| echo "Curl stderr:" | |
| cat /tmp/proxy-stderr.txt || true | |
| echo "" | |
| echo "=== Squid logs after request ===" | |
| docker logs squid-test 2>&1 | tail -30 || true | |
| echo "" | |
| # Now assert | |
| [ "${HTTP_CODE}" = "200" ] || { echo "FAIL: expected 200, got ${HTTP_CODE}"; exit 1; } | |
| grep -q "test" /tmp/proxy-response.txt || { echo "FAIL: unexpected response body"; exit 1; } | |
| echo "--- HTTP via SOCKS5 OK ---" | |
| - name: Post Squid logs to PR on failure | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { execSync } = require('child_process'); | |
| let logs = ''; | |
| try { logs = execSync('docker logs squid-test 2>&1', {encoding: 'utf8', maxBuffer: 50*1024}); } catch(e) { logs = e.stdout || e.message; } | |
| const body = `### E2E Test Squid Logs\n\`\`\`\n${logs.slice(-3000)}\n\`\`\``; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: 30, | |
| body: body | |
| }); | |
| - name: Collect logs | |
| if: always() | |
| run: | | |
| mkdir -p /tmp/test-logs | |
| docker logs squid-test > /tmp/test-logs/squid.log 2>&1 || true | |
| cp /tmp/squid-conf/squid.conf /tmp/test-logs/ 2>/dev/null || true | |
| cp /tmp/proxy-response.txt /tmp/test-logs/ 2>/dev/null || true | |
| cp /tmp/proxy-stderr.txt /tmp/test-logs/ 2>/dev/null || true | |
| - name: Upload test logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-test-logs | |
| path: /tmp/test-logs/ | |
| retention-days: 3 | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| docker rm -f squid-test 2>/dev/null || true | |
| pkill microsocks 2>/dev/null || true | |
| kill %1 2>/dev/null || true | |
| # ------------------------------------------------------------------ | |
| # 4. Test: SOCKS5 with authentication | |
| # ------------------------------------------------------------------ | |
| test-socks5-auth: | |
| name: Test SOCKS5 with Auth | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Download image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: squid-image | |
| path: /tmp | |
| - name: Load image | |
| run: docker load -i /tmp/squid-image.tar | |
| - name: Start local HTTP test server | |
| run: | | |
| mkdir -p /tmp/www | |
| echo '{"origin":"127.0.0.1","test":"ok"}' > /tmp/www/ip | |
| python3 -m http.server 18081 --directory /tmp/www & | |
| sleep 1 | |
| curl -sf http://127.0.0.1:18081/ip | |
| - name: Build and start SOCKS5 server with auth (microsocks) | |
| run: | | |
| if [ ! -f /tmp/microsocks/microsocks ]; then | |
| git clone --depth 1 https://github.com/rofl0r/microsocks.git /tmp/microsocks | |
| cd /tmp/microsocks && make -j"$(nproc)" | |
| fi | |
| /tmp/microsocks/microsocks -u testuser -P testpass -p 11081 & | |
| sleep 1 | |
| echo "--- Verify SOCKS5 auth server directly ---" | |
| curl -sf --socks5-hostname testuser:testpass@127.0.0.1:11081 http://127.0.0.1:18081/ip || { | |
| echo "ERROR: SOCKS5 auth server not working" | |
| exit 1 | |
| } | |
| echo "--- SOCKS5 auth server OK ---" | |
| - name: Create Squid config with SOCKS5 auth | |
| run: | | |
| mkdir -p /tmp/squid-conf-auth | |
| cat > /tmp/squid-conf-auth/squid.conf <<CONF | |
| http_port 3128 | |
| acl all src 0.0.0.0/0 | |
| http_access allow all | |
| never_direct allow all | |
| server_persistent_connections off | |
| client_persistent_connections off | |
| cache_peer 127.0.0.1 parent 11081 0 no-query no-digest connect-fail-limit=2 connect-timeout=8 round-robin proxy-only originserver name=socks_auth socks5 socks-user=testuser socks-pass=testpass | |
| visible_hostname test | |
| cache deny all | |
| access_log stdio:/proc/self/fd/1 combined | |
| CONF | |
| echo "--- Config ---" | |
| cat /tmp/squid-conf-auth/squid.conf | |
| - name: Start Squid with SOCKS5 auth peer | |
| run: | | |
| docker run -d --name squid-auth --network host \ | |
| -v /tmp/squid-conf-auth:/etc/squid/conf.d:ro \ | |
| --entrypoint /bin/sh \ | |
| ${{ env.SQUID_IMAGE }} \ | |
| -c ' | |
| squid -z -N -f /etc/squid/conf.d/squid.conf 2>&1 || true | |
| echo "=== Starting Squid ===" | |
| exec squid -N -f /etc/squid/conf.d/squid.conf 2>&1 | |
| ' | |
| sleep 5 | |
| echo "=== Container status ===" | |
| docker ps -a --filter name=squid-auth --format '{{.Status}}' | |
| if ! docker ps --filter name=squid-auth --filter status=running -q | grep -q .; then | |
| echo "ERROR: Squid container is not running" | |
| docker logs squid-auth 2>&1 || true | |
| exit 1 | |
| fi | |
| echo "=== Early Squid logs ===" | |
| docker logs squid-auth 2>&1 || true | |
| - name: Wait for Squid to listen | |
| run: | | |
| for i in $(seq 1 30); do | |
| if bash -c 'echo > /dev/tcp/127.0.0.1/3128' 2>/dev/null; then | |
| echo "Squid is listening after ${i}s" | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| echo "ERROR: Squid never started listening" | |
| docker logs squid-auth 2>&1 || true | |
| exit 1 | |
| - name: Verify HTTP server is still up | |
| run: curl -sf http://127.0.0.1:18081/ip | |
| - name: Test HTTP through authenticated SOCKS5 | |
| run: | | |
| echo "--- Attempting proxy request ---" | |
| HTTP_CODE=$(curl -s -o /tmp/proxy-response.txt -w '%{http_code}' --max-time 15 -x http://127.0.0.1:3128 http://127.0.0.1:18081/ip 2>/tmp/proxy-stderr.txt || true) | |
| echo "HTTP status: ${HTTP_CODE}" | |
| echo "Response body:" | |
| cat /tmp/proxy-response.txt || true | |
| echo "" | |
| echo "Curl stderr:" | |
| cat /tmp/proxy-stderr.txt || true | |
| echo "" | |
| echo "=== Squid logs after request ===" | |
| docker logs squid-auth 2>&1 | tail -30 || true | |
| echo "" | |
| [ "${HTTP_CODE}" = "200" ] || { echo "FAIL: expected 200, got ${HTTP_CODE}"; exit 1; } | |
| grep -q "test" /tmp/proxy-response.txt || { echo "FAIL: unexpected body"; exit 1; } | |
| echo "--- HTTP via SOCKS5 auth OK ---" | |
| - name: Post Squid logs to PR on failure | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { execSync } = require('child_process'); | |
| let logs = ''; | |
| try { logs = execSync('docker logs squid-auth 2>&1', {encoding: 'utf8', maxBuffer: 50*1024}); } catch(e) { logs = e.stdout || e.message; } | |
| const body = `### Auth Test Squid Logs\n\`\`\`\n${logs.slice(-3000)}\n\`\`\``; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: 30, | |
| body: body | |
| }); | |
| - name: Collect logs | |
| if: always() | |
| run: | | |
| mkdir -p /tmp/test-logs-auth | |
| docker logs squid-auth > /tmp/test-logs-auth/squid.log 2>&1 || true | |
| cp /tmp/squid-conf-auth/squid.conf /tmp/test-logs-auth/ 2>/dev/null || true | |
| cp /tmp/proxy-response.txt /tmp/test-logs-auth/ 2>/dev/null || true | |
| cp /tmp/proxy-stderr.txt /tmp/test-logs-auth/ 2>/dev/null || true | |
| - name: Upload test logs | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: auth-test-logs | |
| path: /tmp/test-logs-auth/ | |
| retention-days: 3 | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| docker rm -f squid-auth 2>/dev/null || true | |
| pkill microsocks 2>/dev/null || true | |
| kill %1 2>/dev/null || true | |
| # ------------------------------------------------------------------ | |
| # 5. Test: generate.php produces correct config | |
| # ------------------------------------------------------------------ | |
| test-generate: | |
| name: Test Config Generator | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install PHP dependencies | |
| run: | | |
| docker run --rm -v "$(pwd)/setup:/app" -w /app composer:2 install --no-interaction --quiet | |
| - name: Run generate.php with SOCKS proxy list | |
| run: | | |
| cat > proxyList.txt <<'LIST' | |
| 10.0.0.1:1080:socks5:user1:pass1 | |
| 10.0.0.2:1080:socks5:user2:pass2 | |
| 10.0.0.3:1081:socks4:: | |
| 192.168.1.1:8080 | |
| 10.0.0.4:3128:httpsquid:admin:secret | |
| 10.0.0.5:8080:http:user:pass | |
| LIST | |
| docker run --rm -v "$(pwd):/app" php:8.2-cli php /app/setup/generate.php | |
| - name: Verify generated squid.conf has SOCKS peers | |
| run: | | |
| echo "=== Generated squid.conf ===" | |
| cat config/squid.conf | |
| echo "" | |
| # Check socks5 peers use native SOCKS options | |
| grep -q 'socks5' config/squid.conf || { echo "FAIL: socks5 option not found"; exit 1; } | |
| grep -q 'socks-user=user1' config/squid.conf || { echo "FAIL: socks-user not found"; exit 1; } | |
| grep -q 'socks-pass=pass1' config/squid.conf || { echo "FAIL: socks-pass not found"; exit 1; } | |
| grep -q 'originserver' config/squid.conf || { echo "FAIL: originserver not found"; exit 1; } | |
| # Check socks4 peer | |
| grep -q 'socks4' config/squid.conf || { echo "FAIL: socks4 option not found"; exit 1; } | |
| # Check open proxy (no socks, no gost) | |
| grep -q 'name=public' config/squid.conf || { echo "FAIL: open proxy not found"; exit 1; } | |
| # Check httpsquid peer | |
| grep -q 'name=private' config/squid.conf || { echo "FAIL: httpsquid peer not found"; exit 1; } | |
| echo "--- squid.conf generation OK ---" | |
| - name: Verify generated docker-compose.yml | |
| run: | | |
| echo "=== Generated docker-compose.yml ===" | |
| cat docker-compose.yml | |
| echo "" | |
| # Gost container should only exist for http type (not for socks4/socks5) | |
| # We have 1 http proxy -> 1 gost container | |
| GOST_COUNT=$(grep -c 'ginuerzh/gost' docker-compose.yml || true) | |
| echo "Gost containers: ${GOST_COUNT}" | |
| [ "${GOST_COUNT}" -eq 1 ] || { echo "FAIL: expected 1 gost container, got ${GOST_COUNT}"; exit 1; } | |
| echo "--- docker-compose.yml generation OK ---" | |
| - name: Verify no Gost for SOCKS proxies | |
| run: | | |
| # socks5/socks4 should NOT create gost containers | |
| # Only 'http' type should use gost | |
| if grep -q 'dockergost_1\|dockergost_2\|dockergost_3' docker-compose.yml; then | |
| echo "FAIL: SOCKS proxies should not create Gost containers" | |
| exit 1 | |
| fi | |
| echo "--- No Gost for SOCKS proxies OK ---" |