Squid answers a CONNECT request with the first http_access line that matches, and that line can be a denial. This single fact explains why a VPN endpoint exception usually fails on the first attempt, because the request never reaches the allow somebody added at the bottom. A narrow fix keeps the existing port guards and teaches them about one reviewed source, destination and port. Everything else on the proxy keeps behaving exactly as it did before.

How Squid decides a CONNECT request

A CONNECT request names a host and a port, and Squid walks the http_access lines in order until one of them matches. A matching deny ends the evaluation immediately and the client receives a 403 from the proxy. That is why the position of a rule matters more than its wording. Moving the same line one step up can change the outcome completely.

The conventional guards deny anything outside Safe_ports and deny CONNECT outside SSL_ports. Those names are ACLs defined in your own configuration, so their contents belong to you rather than to the protocol. A distribution may permit a wide range of ports as Safe_ports while restricting CONNECT separately. Read the live values before assuming that a port is blocked by design.

Confirm that the proxy is the component refusing the request. A 403 generated by Squid looks nothing like a TLS alert from the VPN server, and merging the two into one problem costs the first hour. Check the access log line for the request, because it records both the action and the rule that produced it.

Build one combined match instead of three loose ACLs

Build the exception from three facts: an approved source range, one approved destination address and the single port the endpoint listens on. Two loose ACLs and one combined match produce very different policy, because the combination is what gets evaluated. Name the combined ACL for what it permits rather than for the method it carries.

The shape below uses documentation ranges that have to be replaced with reviewed values. Squid reserves none of these names, so they are yours to choose and yours to keep consistent. This recipe is scoped to Squid 6 and 7, whose documented configuration supports the all-of group. The current reference marks those directives unavailable in Squid 8, so treat this as a version-scoped recipe rather than a migration path, and check the directive against your own release's reference before applying it.

acl pvpn_src src 192.0.2.128/25
acl pvpn_dst dst 198.51.100.77/32
acl pvpn_port port 1194
acl pvpn_ok all-of CONNECT pvpn_port pvpn_dst pvpn_src

The all-of group matches only when every member matches, which means source, destination and port are checked on one line. The group grants nothing by itself, and a request matching only two of the three facts cannot satisfy it. That property keeps the exception narrow when someone later adds a second gateway. Either the new destination is added deliberately, or the request falls back to the original guards.

Bend the port guards without removing them

At their existing positions, the two conventional guard lines look like this. They usually sit after authentication and before the policy lines that allow anything.

http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

Add the negated combined match to each one, so a matching exception survives a guard that would otherwise deny it. Only the guard lines change, and only by one extra term each.

http_access deny CONNECT !SSL_ports !pvpn_ok
http_access deny !Safe_ports !pvpn_ok

Terms on one access line are combined, so the line now denies only requests that fail both the original check and the exception. A request for the approved endpoint passes through, and every other request keeps the old port behaviour. This is narrower than adding the port to a global SSL_ports list, which would permit that port to every destination. If the port is already present in that list, this ACL narrows nothing and the older, wider permission still applies to other destinations, so read the live list before claiming the exception opened the port. It is also reversible, because two lines go back to their previous form. The access-control reference documents the ordering and the conventional guards this change depends on.

Where the exception must not be placed

The temptation is to put an allow above the guards and be finished. That placement also skips the authentication and local-destination protections that usually sit near the top of a working configuration. The result is not a narrow exception, it is a hole with a comment above it. Keep any new allow at the same reviewed stage as the rest of the client policy.

Check what the request still needs after the port guards. An authenticated account, an allowed client address and a destination policy all still apply, and the exception changes none of them. If your proxy already lets the approved account reach arbitrary destinations, the port guard was never the real control.

Consider whether the endpoint is TCP at all. A CONNECT exception carries TCP streams, and an OpenVPN profile that uses UDP will not become reachable through it. Change the profile to TCP only if the server listens that way and the operator accepts the difference in behaviour.

Check the boundary with the parser and a real request

Parse the configuration before reloading, and keep a second session open while you do it. A syntax error at reload time can leave the proxy on the old configuration or refuse the new one outright. The parser reports the rejected line, which is more than the client error will tell you.

Then reload and send one real CONNECT through the proxy from an approved source. The parser run comes first, because a reload with a typo teaches you nothing about the policy.

squid -k parse
squid -k reconfigure
curl -v -p -x http://proxy.lab.internal:3128 https://vpn.lab.internal:1194/ --max-time 5

Test three cases: an approved source and account, an unapproved source, and a different destination on the same port. The third case is the one that proves the exception is narrow, because it exercises the port without the destination match. Read the access log for all three instead of trusting the client message. A successful CONNECT establishes a TCP stream and nothing more, so the VPN handshake remains a separate test.

Artiglyn mynjee