Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Networking › In front of the web server

Proxies and reverse proxies

How does a server behind a proxy know who sent the request?

A proxy is an intermediate server that receives requests on behalf of someone and passes them on somewhere else. A proxy that stands on the client side and acts for the client is a forward proxy, and a proxy that stands on the server side and acts for the server is a reverse proxy. Either way, it is the proxy that actually opens the connection to the server, so the address the server sees at the other end of the connection is the address of the proxy, not of the client. That is why a proxy passes on the original information in a header such as X-Forwarded-For, but this header is also one that a client can write with any value it likes.

Checked against real execution · Proxy and server built with Node 24 on three loopback addresses (127.0.0.10, 20, 30); 42 combinations run with the Node http and https modules and curl 8.21 and checked. The proxy is a test one built for this purpose; the behavior of products such as nginx is based on the documentation

Concepts in this lab

Forward proxy
A forward proxy is an intermediate server that stands on the client side and sends the client's requests on to outside servers on its behalf. The client sets the address of the proxy and goes through it on purpose, and the remote address that a server sees is the proxy, not the client. It is used to collect and log or block the requests that go out from a company or a school in one place, or to hide the address of the client. It is the opposite of a reverse proxy, which stands on the server side and acts for the server, in terms of whom it acts for.
Reverse proxy
A server that receives client requests on behalf of internal servers, forwards them to those servers, and returns their responses to the client. The client does not know the actual servers directly. Its roles include distributing requests across several servers, handling TLS, caching, and protecting the internal servers.
X-Forwarded-For
X-Forwarded-For is a request header in which a proxy writes the address of the original client when it passes a request on. The remote address that a server behind a proxy sees is the proxy, so the address of the real client is passed on only through this header. When a request goes through several proxies, each proxy appends the address of whoever connected to it at the end, separated by a comma. It is a request header, so a client can also write any value into it; a server must use only the values added by a trusted proxy that it manages (the values on the right), and must not read it at all if it is not behind a proxy. It is a widely used convention, not a standard, and the standard header that does the same job is Forwarded.
X-Forwarded-Proto and TLS termination
TLS termination is a setup in which a reverse proxy or a load balancer receives the HTTPS connection of the client, removes the encryption, and passes the request on to the server behind it as unencrypted HTTP. Certificate management and encryption work can be gathered in one place. In this setup the server behind sees only an HTTP connection, so the proxy tells it the protocol the client originally used with the X-Forwarded-Proto: https header. If the server does not read this header, it may build redirect addresses with http:// or misjudge the conditions for secure cookies. Like X-Forwarded-For, use only the values added by a trusted proxy.
HTTPS and TLS
HTTPS is HTTP communication encrypted with TLS. TLS keeps third parties from seeing the content (confidentiality), keeps it from being changed on the way (integrity), and uses a certificate to check that the server you connected to is the real one (authentication). SSL is the protocol that came before TLS. It is now deprecated and only TLS is actually used, but the name SSL is still used alongside it by habit.

Try these

Running with the default settings

  1. The client (203.0.113.7) requests https://shop.example/whoami. What the name shop.example points to is actually a reverse proxy. The client has not configured anything, and it takes whatever it just connected to as the server. It does not know that the real server is behind it. This client wrote an X-Forwarded-For: 198.51.100.99 header into the request itself. A request header is just text that the sender can fill in however it likes.
  2. The reverse proxy decrypts the HTTPS traffic, opens a new TCP connection to the real server on the internal network (10.0.0.9), and passes the request on as HTTP. This proxy writes the address of whoever connected to it (203.0.113.7) into X-Forwarded-For and passes it on. There is already a value, so it appends the address after it, separated by a comma. It also writes the protocol the client originally used as X-Forwarded-Proto: https.
  3. The server checks the remote address of the connection. It is 10.0.0.5, the address of the proxy, because it is the proxy that opened the TCP connection to the server. The connection alone does not tell who the real client is. The connection it received is also plain HTTP, not encrypted.
  4. The X-Forwarded-For of the request the server received is "198.51.100.99, 203.0.113.7". The left value was written by the client, and the right value was appended by the proxy. You cannot tell the two apart from the header alone; you can tell only by their positions. X-Forwarded-Proto: https also arrived.
  5. The server takes the leftmost value of X-Forwarded-For as the client address. The result is 198.51.100.99. This is a fake address that the client wrote itself. The server trusted a spoofed address.

Result of this run

Code

// Reverse proxy: receives HTTPS and passes requests on to the server behind it (10.0.0.9)
https.createServer(tls, (req, res) => {
    const headers = { ...req.headers };
    const peer = req.socket.remoteAddress;               // address of whoever connected to this proxy
    const old = headers['x-forwarded-for'];
    headers['x-forwarded-for'] = old ? old + ', ' + peer : peer;   // appends it after the received value
    headers['x-forwarded-proto'] = 'https';                  // the original protocol the client used
    const to = { host: '10.0.0.9', port: 8080, path: req.url };
    http.request({ ...to, method: req.method, headers }, up => {
        res.writeHead(up.statusCode, up.headers);
        up.pipe(res);                                      // sends the response of the server back to the client as it is
    }).end();
});

// Server: decides who made the request
http.createServer((req, res) => {
    const xff = req.headers['x-forwarded-for'];
    const client = xff ? xff.split(',')[0].trim()             // leftmost value
                       : req.socket.remoteAddress;
    res.end(client);
});

// Client
// curl -H 'X-Forwarded-For: 198.51.100.99' https://shop.example/whoami

Assumptions of this model

All 42 distinct combinations (proxy settings are not used for a direct connection) were run on the PC used to write this lab, and all five columns were confirmed to match the model. The client, the proxy, and the server were started on three loopback addresses of this PC (127.0.0.10, 127.0.0.20, 127.0.0.30), requests were sent to a small proxy and server built with Node.js 24 by two kinds of client (the http and https modules of Node, and curl 8.21), and the remote address and the headers that the server actually saw were recorded. The results of the two clients were the same in every combination. The reverse proxy received HTTPS with a test certificate and passed requests on to the server as HTTP, and the forward proxy was sent requests with the -x option of curl, which confirmed that the full address is carried in the request line. The addresses on the screen (203.0.113.7, 198.51.100.99, 192.0.2.50) are addresses set aside for documentation examples, and 10.0.0.x are private network addresses; they are written in place of the loopback addresses used in the test. The proxy is a test one built for this purpose, so it may differ from the default behavior of real products. For example, unless you configure it otherwise, nginx passes the received headers on as they are, and in proxy_set_header X-Forwarded-For, $proxy_add_x_forwarded_for corresponds to "appending" and $remote_addr corresponds to "writing a new value" (based on the nginx documentation; not confirmed by execution). X-Forwarded-For and X-Forwarded-Proto are not standards but widely used conventions, and the standard header that carries the same information is Forwarded (RFC 7239). When an HTTPS request is sent through a forward proxy, the proxy only connects a tunnel (CONNECT) whose content it cannot see, so it cannot add headers; the forward proxy in this lab handled only HTTP requests. That the server uses the remote address of the connection instead when there is no header is a rule set in this model. Chains of several proxies and the distribution by a load balancer were not covered.

Previous lab: Load balancers and scale out · Next lab: CDN

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these