Forward Proxy

On this page

A forward proxy acts on behalf of the client to access the target. The client first decides whether to delegate a specific connection or request to the proxy, which then forwards it to the target according to the target and policy. Configuring a proxy address does not mean that all programs, DNS, UDP, or system traffic automatically pass through it.

When understanding the proxy path, you must simultaneously track three questions: who selects the proxy, who resolves the target domain name, and whether each segment is encrypted. Looking only at the final target IP or a browser "proxy connected" prompt cannot answer these questions.

ScenarioWhat the client sends to the proxyProxy's main task
Standard HTTP proxy requestHTTP request with absolute-form targetParse HTTP and forward
HTTP CONNECTHost and portEstablish byte tunnel
SOCKS5 CONNECTBinary target address and portEstablish TCP relay
SOCKS5 UDP ASSOCIATEControl association, followed by UDP encapsulationRelay UDP based on datagram target
PACExecute selection rules locallySelect DIRECT or proxy candidate; does not carry traffic

HTTP Request Proxy and CONNECT

When requesting a plaintext site via a standard HTTP proxy, the request target usually looks like GET http://example.com/index.html HTTP/1.1; the proxy can read and process HTTP. When accessing an HTTPS target, the client often first sends CONNECT example.com:443 HTTP/1.1 along with the corresponding Host, and after receiving a successful 2xx response, performs a TLS handshake with the target within the tunnel.

CONNECT establishes a byte tunnel; TLS presence is determined by the protocol inside the tunnel Client Connect to proxy first and submit target Proxy Connect to target according to policy Target Server Accept connection initiated by proxy HTTPS Scenario: Client ↔ Target TLS passes through the tunnel Proxy can still see the CONNECT target and traffic metadata Client-to-proxy and proxy-to-target are usually two separate TCP connections, not the same end-to-end TCP sequence number space.

Bytes after a successful CONNECT are no longer parsed as the body of a standard HTTP message for that request. Typically, client-to-proxy and proxy-to-target are two independent TCP connections; the proxy concatenates byte transmission capabilities, rather than extending a single TCP connection intact to the server.

CONNECT itself does not provide encryption, nor is it limited to HTTPS. The proxy can restrict target ports and destinations; the client cannot equate a successful proxy connection with permission to access any address. Proxy authentication failure usually corresponds to 407 and Proxy-Authenticate, while the target service's 401 is a separate layer. RFC 9110: CONNECT

HTTPS Proxy and HTTPS Target are Two Different Things

http://proxy.example:8080 and https://proxy.example:8443 describe the transport method from client to proxy; the https:// in the target URL describes the target service using TLS. CONNECTing to an HTTPS target on an HTTPS proxy can form an outer proxy TLS and an inner target TLS, with different authentication objects.

A proxy that does not terminate the inner TLS cannot see the target HTTP content, but still knows the explicit CONNECT target, time, and traffic volume. ECH can change the visible content of the inner ClientHello, but it does not erase the CONNECT target already sent to that proxy. If the client trusts the alternative certificate issued by the proxy and allows TLS inspection, the actual identity and confidentiality boundaries have already changed.

HTTP/2 and HTTP/3 can also carry their respective CONNECT mechanisms; do not assume all proxy connections are a single HTTP/1.1 connection building only one tunnel. The standard CONNECT TCP byte tunnel and CONNECT-UDP are different capabilities; the latter has specific UDP proxy protocol conventions. RFC 9298

SOCKS5: Method Selection, Authentication, and Commands Cannot Be Confused with TCP Handshake

First, establish a TCP connection to the proxy, then negotiate available authentication methods; when selecting methods like username/password, there is a sub-negotiation, followed by sending CONNECT, BIND, or UDP ASSOCIATE commands. These stages cannot be collectively referred to as a fixed "three-way handshake."

Field or ValueMeaning
VER=5SOCKS version
METHOD=0x00 / 0x02No authentication / Username-password method
METHOD=0xffNo acceptable methods, close connection
CMD=1 / 2 / 3CONNECT / BIND / UDP ASSOCIATE
ATYP=1 / 3 / 4IPv4 / Domain name / IPv6
REP=0Command successful, subsequent transmission follows corresponding semantics

Domain name addresses contain 1 B length and name bytes, with no trailing NUL; port uses 2 B network byte order. The BND.ADDR/PORT in the CONNECT reply is the binding endpoint the proxy uses for the target connection, not echoing the DST as is. RFC 1928

The username/password sub-negotiation has its own version field as 1, and cannot be written as SOCKS main protocol version 5. This method transmits credential bytes directly and does not automatically provide encryption; security depends on the outer transport and deployment boundaries. RFC 1929

Complete Address Example: Domain Name CONNECT and IPv4 Header

import ipaddress

name = b'example.com'
request = bytes([5, 1, 0, 3, len(name)]) + name + (443).to_bytes(2, 'big')
assert request.hex() == '050100030b6578616d706c652e636f6d01bb'
assert len(request) == 18
n = request[4]
assert request[5:5+n].decode('ascii') == 'example.com'
assert int.from_bytes(request[5+n:7+n], 'big') == 443
# No SOCKS fragmentation, IPv4 target. Here DATA is just teaching bytes, not actual DNS packets.
udp_head = bytes([0, 0, 0, 1]) + ipaddress.ip_address('192.0.2.53').packed + (53).to_bytes(2, 'big')
packet = udp_head + b'DATA'
assert len(udp_head) == 10 and len(packet) == 14
assert udp_head.hex() == '00000001c00002350035'
assert int.from_bytes(packet[8:10], 'big') == 53
print('CONNECT request / UDP encapsulation header:', len(request), len(udp_head))

This example only checks encoding, not implementing proxy connection, authentication, or relay. SOCKS5 does not provide general IP-layer forwarding; for example, ICMP is not sent as TCP content directly through CONNECT.

UDP ASSOCIATE: Control Connection is Not UDP-over-TCP

SOCKS5 UDP: TCP control association and UDP data path coexist Client SOCKS5 Proxy UDP Target TCP: UDP ASSOCIATE / Return relay address UDP: SOCKS header + application data UDP: Application data UDP: Response data UDP: SOCKS header + response data Association depends on TCP control connection survival; UDP data does not gain reliable transmission just because this TCP connection exists.

The client establishes the association via TCP command; the address and port in the command describe the client UDP endpoint expected to be used, which can be filled with zeros if unknown. The proxy replies with an available relay endpoint; the client then sends each UDP datagram with a SOCKS header to the relay, specifying the actual target in the header; do not treat the zero address in the association request as the final destination for each datagram.

Returned data is also sent by the relay with a SOCKS header to the client, used to identify the remote source. The association terminates when the TCP control connection closes; UDP may still be lost or reordered, and the TCP control channel does not retransmit for it. FRAG=0 means unfragmented; SOCKS layer fragmentation support is optional, and implementations that do not support it discard packets with non-zero FRAG; this is also different from IP fragmentation.

The SOCKS UDP header for IPv4 targets is 10 B, for IPv6 targets is 22 B, and for domain name targets is 7 + name byte count, plus outer UDP/IP overhead. A TCP proxy being reachable does not prove UDP relay is reachable, and client applications may not implement UDP ASSOCIATE at all.

Where is DNS: Address Type and Client Implementation Jointly Determine

When the client resolves locally first and sends an IP, the proxy cannot revoke the local DNS query that has already occurred. When sending ATYP=Domain name, it is handed to the proxy side to resolve or route according to its own policy; this does not mean the proxy must use a specific public DNS.

curl distinguishes between local resolution for socks5:// and proxy-side resolution for socks5h://. The following is for manual observation only when a corresponding local proxy is available; execution will initiate real target requests:

curl --proxy socks5h://127.0.0.1:1080 --connect-timeout 5 https://example.com/

--connect-timeout is also not the total timeout for the entire download. Proxy host discovery itself, application pre-resolution, DoH, and other processes not using this proxy still need to be checked separately; you cannot assert that the entire system has no local DNS traffic based solely on one successful curl. curl proxy options

Changing the domain name to an IP changes more than just packet length

Both paths can ultimately access the same target, but the evidence received by the proxy is different: the IP path has no original domain name, while the domain name path has length bytes and the name itself. This demonstrates the responsibility boundary of a command, and does not declare that all system DNS passes through the proxy based on this.

Preparing the visual
Changing the domain name to an IP changes more than just packet length

ATYP determines whether the request carries a name or an address. DNS queries already made locally do not disappear just because they later enter an encrypted tunnel.

PAC: Select Route by Target, DIRECT is an Explicit Fallback

PAC's FindProxyForURL returns a proxy candidate string. It is JavaScript in a restricted environment, not a normal script that can arbitrarily access browser page state. Domain name rules are often easier to predict than calling DNS helper functions; isInNet etc. may trigger local resolution, which conflicts exactly with the intent to avoid local resolution.

function FindProxyForURL(url, host) {
    // Only hand over explicitly controlled internal domain names to direct connection, do not guess server location by country suffix.
    host = host.toLowerCase();
    if (host === 'intranet.example' || host.slice(-17) === '.intranet.example') {
        return 'DIRECT';
    }
    return 'PROXY 127.0.0.1:7890; SOCKS5 127.0.0.1:1080';
}

The PAC syntax and helper functions supported by the implementation need to be verified by the client. The example does not have DIRECT at the end, meaning there is no candidate that explicitly allows "proxy failure directly access." If DIRECT is appended, it changes the exit strategy on failure, and cannot be treated as a harmless fixed template.

PAC does not guarantee full re-execution for every request, and you cannot assume HTTPS URLs always contain a path. Taking Chromium as an example, PAC-visible URLs have privacy trimming, proxy selection has caching and failure handling; its SOCKS5 implementation also has its own capability scope. Domain suffix .cn does not prove the target's physical location, and .local involves local name resolution semantics, so it is inappropriate to use it as a rough representative of all intranets. Chromium Proxy Documentation

Locating Failures: Record Evidence by Phase

PhaseFailure SignalConfirm First
Proxy SelectionDid not pass through expected proxyWhether the application reads settings, PAC/bypass rules
Connection to ProxyTimeout, Connection RefusedAddress, listening, network, and outer TLS
Proxy Authentication407, SOCKS method rejectedMethod and credential layers
Target EstablishmentCONNECT non-2xx, SOCKS REP failureProxy policy, target resolution, and egress
Inner TLSName or chain failureActual target identity and trust material
Business HTTP401, 403, 5xxTarget service authentication and business processing
UDP PathTCP successful but UDP no returnAssociation, relay reachability, encapsulation, and application support

System proxy, environment variables, and application-internal configurations may not override each other; there may also be different authentication and resolution locations in the proxy chain. Separating routes and error phases avoids misjudgments like "all problems change to a different DNS" or "all traffic has been taken over."

Continue Reading