How does an internet server respond to a request from a private IP?

Within a computer, between processes

First let's see how a single computer distinguishes between concurrent connections.

Most transport protocols such as TCP, UDP, SCTP use two ports, source and destination – i.e. one on either end of the connection. That is, packets do not simply walk "through" a port; instead they travel from port X to port Y.

The destination port is usually well-known (80 for HTTP, 53 for DNS…) but the source port is usually randomly chosen by the OS itself, which also ensures that the src/dst combination is unique.

So when your browser makes multiple connections "to Yahoo's port 80", all of them actually have different source ports, and the OS keeps a socket table such as:

PROCESS       PROTO  LOCAL                REMOTE            STATE
9894/firefox  tcp    192.168.6.175:39163  google.server:80  established
9894/firefox  tcp    192.168.6.175:52909  yahoo.server:80   established
17463/chrome  tcp    192.168.6.175:64981  yahoo.server:80   established
9894/firefox  udp    192.168.6.175:4984   8.8.8.8:53        --

So when the OS receives a TCP packet from yahoo.server:80 to local port 52909, it can map it to the specific connection made by Firefox.

Important to note that this has nothing to do with NAT yet, and happens the same way even if you're directly connected. (NAT will make use of it, though.)

(You can see this table using netstat -n, or various graphical tools on Windows. Often "local/remote" are labelled "source/destination", although that's not entirely accurate.)


Within a NATed network, between computers

The answer to your question about NAT is very similar, only everything is done at a larger scale.

The router performing NAT keeps a "state" table containing both the internal and external addresses & ports. For example, if your two HTTP requests used separate TCP connections, they might be tracked as:

PROTO   ORIG-SRC             ORIG-DST         REPLY-SRC        REPLY-DST
6/tcp   192.168.6.42:52909   yahoo.server:80  yahoo.server:80  your.public.ip.addr:52909
6/tcp   192.168.6.175:39163  yahoo.server:80  yahoo.server:80  your.public.ip.addr:39163
6/tcp   192.168.6.175:52909  yahoo.server:80  yahoo.server:80  your.public.ip.addr:28330
17/udp  192.168.6.175:4984   8.8.8.8:53       8.8.8.8:53       your.public.ip.addr:4984

When the router receives a packet from REPLY-SRC (Yahoo) addressed to REPLY-DST (your public IP address), it knows that the real destination must be taken from the ORIG-SRC column in order to undo the NAT.

(If there's no matching state, then manually configured port-forwarding rules are processed. If still none match, then the packet was really meant for the router itself.)

Note how the state table contains addresses and ports, allowing multiple connections to the same server to be distinguished by the port combination. In my example, two computers accidentally used the same port combination, so the 2nd connection had its ports translated as well.

(In fact, some NATs look only at the port and ignore the source address completely; this reduces the number of possible connections, but makes it a lot easier for peer-to-peer programs to perform "NAT hole-punching".)

Such state is kept even for connectionless protocols such as UDP or ICMP, so the entries expire after some interval of inactivity even though there's no explicit "close connection" packet. (The state table is actually part of the firewall, so even if NAT isn't done, the router might still use it to distinguish between "active" connections and stray packets.)

(If your router is Linux-based, conntrack -L or cat /proc/net/nf_conntrack would show this table. For OpenBSD or pfSense, try pfctl -s state.)