Port Forwarding Not Working? Start Here

Before changing forwarding rules, compare the target IP, listening address, LAN access and NAT path. Each step separates what you have verified from what remains unknown.

PPORT ·

What to check next

Choose an observation to narrow the next steps. This does not make another connection or establish a diagnosis.

I have not checked whether the program is listening

First confirm that a program can accept a connection. A familiar port number does not prove that the service uses it.

  1. Use the OS commands below to check LISTEN on the TCP port.
  2. If no result appears, check the process, configured port and logs.
  3. When external and internal ports differ, check the internal port on the device.
It only listens on 127.0.0.1 or ::1

Loopback is reachable only from the same device. Adding a router rule cannot make another device connect directly to that listening socket.

  1. Check the program’s bind/listen address.
  2. Choose the required LAN interface and restrict source addresses and ports.
  3. 0.0.0.0 and :: may cover multiple interfaces; check the resulting exposure.
Another device on the same LAN cannot connect

Resolve local connectivity first to separate it from the internet path. A TCP connection and an application login are different checks.

  1. Confirm the server’s LAN IP and internal TCP port.
  2. Check the listening address, device firewall and guest Wi-Fi isolation.
  3. Test forwarding only after the LAN connection works.
LAN works, but access from outside fails

Compare routing, NAT and firewalls. TIMEOUT alone does not establish ISP blocking.

  1. For IPv4, compare the router WAN address with the connection IP shown by PPORT. Consider VPNs, proxies and double NAT.
  2. Check the TCP mapping from external port to internal IP/port and each firewall.
  3. For IPv6, check the server device’s global address and firewall. PPORT cannot target another device’s IPv6 address.
OPEN, but the website or app does not work

OPEN means TCP connected to that address and port. It does not verify the expected service, certificate or login.

  1. Check the server logs and whether the listening process is the intended program.
  2. For web services, verify http/https, domain and port.
  3. On 443, check certificate identity and validation; if HTTP responds, inspect its status and application errors.

Quick start

  1. Record the target IP and external port.
  2. Compare server, LAN, router and external results in order.
  3. Change one setting at a time and retest under the same conditions.

1. Are you testing the intended IP?

PPORT checks only the public IP of the visiting browser. For a home NAS, use a PC on the same internet connection and compare the displayed IP with the router WAN address. VPNs and proxies can change it. A phone using mobile data usually tests the mobile network IP, not the home connection.

PPORT’s server attempts the external TCP connection. Results describe one path at one time. Separate uplinks or proxies can still make target identity ambiguous. Record the displayed IP, IP version, external port and test time.

2. Is the application listening?

Suppose the internal service uses TCP 8080. Inspect listening sockets on that server. 127.0.0.1:8080 accepts local connections only. 0.0.0.0:8080 binds all IPv4 interfaces, but proves neither firewall permission nor internet exposure. Whether [::]:8080 also accepts IPv4 depends on the OS configuration.

If no listening socket appears, check the application and its port before changing the router. The last number in a Windows LISTENING row is the process ID. Run the command on the device hosting the service.

Windows · PowerShell · TCP 8080
Get-NetTCPConnection -State Listen -LocalPort 8080
Windows · CMD · TCP 8080
netstat -ano | findstr "LISTENING" | findstr /C:":8080 "
Linux · TCP 8080
ss -lnt 'sport = :8080'
macOS · TCP 8080
lsof -nP -iTCP:8080 -sTCP:LISTEN

3. Does LAN access work?

Connect from another LAN device to the server’s private IP and internal port. If this fails, inspect the host firewall, binding, VLANs and wireless isolation. LAN success narrows the server and local path; it does not validate external forwarding.

For external 18080 → internal 192.168.0.20:8080, enter 18080 in PPORT and inspect listening on 8080. A DHCP address change can redirect the rule to another device; check address reservations.

4. Is there another NAT upstream?

A WAN IPv4 address in 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 or 100.64.0.0/10 warrants checking upstream NAT. The first three are private ranges; the last is shared address space. Do not conclude CGNAT from the address alone: check the upstream router or provider.

For two routers you manage, both forwarding paths must agree. With provider NAT, a home-router rule alone may not create an inbound path. Diagnose IPv6 separately using the target global address and firewall, not IPv4 forwarding.

5. Turn the result into a next step

OPEN means TCP connected, not that login, certificates or file transfer work. CLOSED can reflect connection refusal; TIMEOUT means no connection completed within the limit. Neither identifies a particular firewall as the cause.

Record time, IP, port and changes instead of relying on one failed attempt. If listening and LAN access work but external access fails, inspect forwarding and the upstream network. Adjust only the required TCP port and access scope rather than disabling the entire firewall.

Windows: LISTENING, but external access fails

  1. Symptom

    PPORT reports TIMEOUT or CLOSED for external TCP 8080. On the service PC, a browser opens http://localhost:8080 without problems. The router forwards external 8080 to 192.168.0.x:8080.

  2. Environment

    Windows 11 Pro with PowerShell 5.1; the service PC's LAN address is 192.168.0.x (last octet masked) and the service uses TCP 8080. The output marked “measured” reproduces the symptom by binding Python's built-in web server to 127.0.0.1 only. The router and external check are an illustrative setup.

    Reproduction command · Windows · PowerShell
    python -m http.server 8080 --bind 127.0.0.1
  3. Check

    On the service PC, check the listening address first. Then, from the same PC, connect once to the loopback address and once to the LAN address. The LAN-address test does not need a second device.

    Commands · Windows · PowerShell
    Get-NetTCPConnection -State Listen | Where-Object LocalPort -EQ 8080 | Select-Object LocalAddress,LocalPort,OwningProcess
    Commands · Windows · CMD
    netstat -ano | findstr "LISTENING" | findstr /C:":8080 "
    Commands · Windows · PowerShell
    Test-NetConnection -ComputerName 127.0.0.1 -Port 8080 | Select-Object RemoteAddress,RemotePort,TcpTestSucceeded
    Test-NetConnection -ComputerName 192.168.0.x -Port 8080 | Select-Object RemoteAddress,RemotePort,PingSucceeded,TcpTestSucceeded
  4. Output and interpretation

    Measured output · PPORT operator's PC (Windows 11 Pro, PowerShell 5.1) · 2026-10-12 · last octet of the private IP masked
    PS> Get-NetTCPConnection -State Listen | Where-Object LocalPort -EQ 8080 | Select-Object LocalAddress,LocalPort,OwningProcess
    
    LocalAddress LocalPort OwningProcess
    ------------ --------- -------------
    127.0.0.1         8080         26752
    
    C:\> netstat -ano | findstr "LISTENING" | findstr /C:":8080 "
      TCP    127.0.0.1:8080         0.0.0.0:0              LISTENING       26752
    
    PS> Test-NetConnection -ComputerName 127.0.0.1 -Port 8080 | Select-Object RemoteAddress,RemotePort,TcpTestSucceeded
    
    RemoteAddress RemotePort TcpTestSucceeded
    ------------- ---------- ----------------
    127.0.0.1           8080             True
    
    PS> Test-NetConnection -ComputerName 192.168.0.x -Port 8080 | Select-Object RemoteAddress,RemotePort,PingSucceeded,TcpTestSucceeded
    TCP connect to (192.168.0.x : 8080) failed
    
    RemoteAddress RemotePort PingSucceeded TcpTestSucceeded
    ------------- ---------- ------------- ----------------
    192.168.0.x         8080          True            False

    LocalAddress is 127.0.0.1, so the program accepts connections only on the loopback interface. That is why the test to 127.0.0.1 returns True and the test to the same PC's LAN address returns False.

    PingSucceeded is True, so the host at 192.168.0.x responds. The cause is the program's listening address, not the line or the router. Until that changes, no firewall or forwarding change can make external connections succeed.

  5. Fix

    Change the listening address in the program's settings to the LAN address (192.168.0.x) or 0.0.0.0, then restart it. 0.0.0.0 accepts connections on every IPv4 interface, including VPN and virtual adapters; naming only the address you need keeps exposure narrower.

    Windows Firewall also checks connections that arrive from other devices. Check the current network profile first: if NetworkCategory is Public, a rule for the Private profile does not apply. If you cancelled the firewall prompt when the program first ran, Windows may have created a block rule for it, and block rules take precedence over allow rules. Then allow only this port from an administrator PowerShell. For a LAN-only service, add -RemoteAddress LocalSubnet to narrow the scope.

    Reproduction command · Windows · PowerShell
    python -m http.server 8080 --bind 192.168.0.x
    Commands · Windows · Administrator PowerShell
    Get-NetConnectionProfile | Select-Object InterfaceAlias,NetworkCategory
    Get-NetFirewallApplicationFilter | Where-Object Program -Like "*python*" | Get-NetFirewallRule | Select-Object DisplayName,Profile,Action,Enabled
    New-NetFirewallRule -DisplayName "Test web TCP 8080" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Allow -Profile Private
  6. Verify

    Check the listening address and the LAN-address connection again. If LocalAddress is 192.168.0.x and TcpTestSucceeded is True, the PC side is fixed.

    Open PPORT in a browser on a PC using the same internet line and test external port 8080. OPEN means you are done. If the LAN test succeeds but PPORT still shows TIMEOUT, the remaining segment is the router path; continue with the case in the port forwarding guide.

    Commands · Windows · PowerShell
    Get-NetTCPConnection -State Listen | Where-Object LocalPort -EQ 8080 | Select-Object LocalAddress,LocalPort,OwningProcess
    Test-NetConnection -ComputerName 192.168.0.x -Port 8080 | Select-Object RemoteAddress,RemotePort,TcpTestSucceeded
    Illustrative output · not measurements from a production server.
    LocalAddress LocalPort OwningProcess
    ------------ --------- -------------
    192.168.0.x       8080         35120
    
    RemoteAddress RemotePort TcpTestSucceeded
    ------------- ---------- ----------------
    192.168.0.x         8080             True

Ubuntu/Linux: active, but external access fails

  1. Symptom

    A website on a cloud VM responds from inside the server but does not open in an external browser. systemctl shows both nginx and the app service as active.

  2. Environment

    An Ubuntu 24.04 VM on OCI. Nginx accepts external 80/443, and the app listens on 127.0.0.1:8000 for requests from Nginx only. This is the same layout as the PPORT production server.

    The “measured” ss output was taken on the PPORT production server on 2026-10-12. The firewall output is an illustrative example of the default iptables rules shipped in OCI Ubuntu images. Replace example.com with your domain.

  3. Check

    Check in this order: listening sockets → the HTTPS response inside the server → the VM firewall. --resolve sends a request for the same domain to this server's Nginx without going through DNS or the external path.

    Commands · Ubuntu · run on the server
    sudo ss -ltnp | grep -E ':8000|:80 |:443 '
    curl -sS -o /dev/null -w '%{http_code}\n' --resolve example.com:443:127.0.0.1 https://example.com/
    sudo ufw status
    sudo iptables -S INPUT
  4. Output and interpretation

    Measured output · PPORT production server (OCI, Ubuntu 24.04) · 2026-10-12
    $ sudo ss -ltnp | grep -E ':8000|:80 |:443 '
    LISTEN 0      511          0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=2796421,fd=9),("nginx",pid=2796419,fd=9),("nginx",pid=924555,fd=9))
    LISTEN 0      2048       127.0.0.1:8000      0.0.0.0:*    users:(("gunicorn",pid=2931560,fd=3),("gunicorn",pid=998637,fd=3),("gunicorn",pid=998629,fd=3))
    LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=2796421,fd=7),("nginx",pid=2796419,fd=7),("nginx",pid=924555,fd=7))
    LISTEN 0      511             [::]:443          [::]:*    users:(("nginx",pid=2796421,fd=10),("nginx",pid=2796419,fd=10),("nginx",pid=924555,fd=10))
    LISTEN 0      511             [::]:80           [::]:*    users:(("nginx",pid=2796421,fd=8),("nginx",pid=2796419,fd=8),("nginx",pid=924555,fd=8))
    Illustrative output · not measurements from a production server.
    $ curl -sS -o /dev/null -w '%{http_code}\n' --resolve example.com:443:127.0.0.1 https://example.com/
    200
    $ sudo ufw status
    Status: inactive
    $ sudo iptables -S INPUT
    -P INPUT ACCEPT
    -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
    -A INPUT -p icmp -j ACCEPT
    -A INPUT -i lo -j ACCEPT
    -A INPUT -p udp -m udp --sport 123 -j ACCEPT
    -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
    -A INPUT -j REJECT --reject-with icmp-host-prohibited

    Nginx listens on 80/443 on 0.0.0.0 and [::]; the app listens on 127.0.0.1:8000. A loopback-only 8000 is intended and should not be opened externally. The --resolve request inside the server returns 200, so Nginx, the certificate and the app work. What remains is the path into the server from outside.

    iptables rules apply even when UFW is inactive. Before the final REJECT, the only new TCP connections allowed are on port 22, so 80/443 connections are rejected. In that case an external curl fails immediately. If it waits several seconds and then times out, packets were more likely dropped before reaching the VM, by an OCI security list or NSG.

  5. Fix

    Insert allow rules for 80/443 before REJECT and save them. Rule numbers count the -A lines from the top, excluding the -P line. In the example REJECT is rule 6, so insert at position 6. Without netfilter-persistent save, the rules disappear after a reboot.

    In the OCI console, add ingress rules for TCP 80/443 to the security list of the instance's subnet or to the NSG attached to its VNIC. Do not add the backend port 8000 or database ports.

    Commands · Ubuntu · run on the server
    sudo iptables -I INPUT 6 -p tcp -m state --state NEW --dport 80 -j ACCEPT
    sudo iptables -I INPUT 6 -p tcp -m state --state NEW --dport 443 -j ACCEPT
    sudo netfilter-persistent save
  6. Verify

    If the allow rules sit above REJECT and an external request returns 200, you are done. Without a PC outside the server, use PPORT's HTTP status checker and SSL certificate checker; both connect from the PPORT server to public 80/443.

    Commands · Ubuntu · run on the server
    sudo iptables -S INPUT
    Commands · run from a PC outside the server
    curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
    Illustrative output · not measurements from a production server.
    $ sudo iptables -S INPUT
    -P INPUT ACCEPT
    -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
    -A INPUT -p icmp -j ACCEPT
    -A INPUT -i lo -j ACCEPT
    -A INPUT -p udp -m udp --sport 123 -j ACCEPT
    -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
    -A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT
    -A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT
    -A INPUT -j REJECT --reject-with icmp-host-prohibited
    $ curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
    200

OPEN, but the website does not work

  1. Symptom

    PPORT reports OPEN for 443, but the browser shows 502 Bad Gateway.

  2. Environment

    Nginx terminates TLS on 443, and the app (systemd service myapp) listens on 127.0.0.1:8000. This is an illustrative example for Ubuntu 24.04. Replace example.com and 192.0.2.10 with your domain and server IP.

  3. Check

    OPEN confirms the TCP connection only. Check the remaining layers from the outside in. --resolve rules out DNS pointing to a different server and sends the request to the server you specify with the same domain in Host and SNI.

    Commands · run from a PC outside the server
    curl -sS -I --resolve example.com:443:192.0.2.10 https://example.com/
    Commands · Ubuntu · run on the server
    curl -sS -i http://127.0.0.1:8000/
    sudo tail -n 1 /var/log/nginx/error.log
  4. Output and interpretation

    Illustrative output · not measurements from a production server.
    $ curl -sS -I --resolve example.com:443:192.0.2.10 https://example.com/
    HTTP/1.1 502 Bad Gateway
    Server: nginx
    
    $ curl -sS -i http://127.0.0.1:8000/
    curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server
    
    $ sudo tail -n 1 /var/log/nginx/error.log
    2026/10/12 10:15:32 [error] 1234#1234: *56 connect() failed (111: Connection refused) while connecting to upstream, client: 198.51.100.7, server: example.com, request: "HEAD / HTTP/1.1", upstream: "http://127.0.0.1:8000/", host: "example.com"

    The external request reached Nginx and received 502, so TCP and TLS work. A direct connection to 8000 inside the server is refused, and the Nginx log records an upstream connection refusal (111: Connection refused). The app is not listening on 8000. curl's wording differs slightly between versions, but (7) means the connection failed.

    If the request to 8000 inside the server returns 200 and only external requests get 502, check the Nginx configuration instead of the app: whether proxy_pass uses the app's port and http/https, and whether the log shows upstream timed out (110).

  5. Fix

    Find out why the app stopped from the service status and recent logs. In the example it exited right after starting because an environment variable was missing. Add the value to the settings file and restart the service.

    Commands · Ubuntu · run on the server
    systemctl status myapp --no-pager
    sudo journalctl -u myapp -n 20 --no-pager
    Illustrative output · not measurements from a production server.
    $ systemctl status myapp --no-pager
    × myapp.service - Example web app
         Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
         Active: failed (Result: exit-code) since Mon 2026-10-12 10:14:58 KST; 1min 12s ago
    
    $ sudo journalctl -u myapp -n 20 --no-pager
    Oct 12 10:14:58 web1 myapp[2210]: RuntimeError: DATABASE_URL is not set
    Oct 12 10:14:58 web1 systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
    Oct 12 10:14:58 web1 systemd[1]: myapp.service: Failed with result 'exit-code'.
    Commands · Ubuntu · run on the server
    sudoedit /etc/myapp.env
    sudo systemctl restart myapp
  6. Verify

    Confirm that the app answers on 8000 again, then repeat the external request. Two 200 responses mean you are done. From outside the server, PPORT's HTTP status checker shows the response code and the SSL certificate checker shows the certificate name and expiry.

    Commands · Ubuntu · run on the server
    curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/
    curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/
    Illustrative output · not measurements from a production server.
    200
    200

Scope and limits

Use this workflow for TCP services you administer. UDP, VPN tunnels and reverse-proxy application faults need separate investigation. An intentionally private service may be correctly configured to reject internet access.

Official references

Tools for this guide