A port that was free yesterday refuses to bind today, a server that works on 127.0.0.1 is invisible on localhost from one tool, a Windows Security Alert pops up for an app that only you will ever call, and a container's database is reachable from the whole Wi-Fi. All four come from the same three mechanisms: the port ranges Windows reserves, the two addresses behind localhost, and the firewall's prompt. This part reads all three on this PC without changing any of them, runs a template web app on a dozen ports to see what happens, and crosses into WSL and Docker in both directions. Every output is quoted; the LAN address is written as 203.0.113.10.
Get-NetTCPConnection -State Listenjoined toGet-Process: every listener with its program namenetsh interface ipv4 show excludedportrange protocol=tcpandshow dynamicport tcpdotnet new web -n PortLab3, thendotnet run --urls http://localhost:57250(inside a listed range),:15930, and a second copy on 15930--urls http://127.0.0.1:15932, thencurl.exe -v http://localhost:15932/andhttp://[::1]:15932/Get-NetFirewallProfilewith and without-PolicyStore ActiveStore;Get-NetFirewallRulefor the rules a prompt left; the firewall event log, ids 2097 and 2099- A throwaway WSL distribution running
python3 -m http.server 15933 --bind 127.0.0.1, curled from Windows; then the reverse from inside WSL docker run -d --rm -p 127.0.0.1:15935:5432 postgres:18, and which process holds the port on Windows
Who listens, and on which address
netstat -ano shows a process id per socket; the PowerShell form joins the name, and on this
PC it listed 58 listening sockets. The developer-relevant rows, with the third-party desktop tools
left out:
| Port | Address | Process | What it means |
|---|---|---|---|
| 5432 | 0.0.0.0 and :: | postgres | The native PostgreSQL 18 service, on every interface. A Docker PostgreSQL must publish another host port. |
| 11434 | 127.0.0.1 | ollama | The Windows Ollama app, loopback only. |
| 53040 | 127.0.0.1 and ::1 | sqlservr | SQL Server Express on a dynamic port, loopback only. Nothing listens on 1433. |
| none | sqlservr | LocalDB, the second sqlservr process: no TCP socket at all, a named pipe instead. | |
| 80, 445, 139 | all, and one per adapter | System | Windows itself: HTTP.sys, SMB, NetBIOS. |
| 15840, 15850 to 15852 | 0.0.0.0 and :: | com.docker.backend | Published ports of containers that had already stopped: Docker Desktop holds them on the host. |
The address column is the one to read. 0.0.0.0 and :: mean every interface,
including the Wi-Fi one; 127.0.0.1 and ::1 mean this PC only. Part 4 picks its
container port from this table.
The ranges Windows keeps for itself
PS> netsh interface ipv4 show excludedportrange protocol=tcp
Protocol tcp Port Exclusion Ranges
Start Port End Port
---------- --------
80 80
50000 50059 *
57219 57318
57424 57523
57562 57661
57819 57918
57919 58018
58019 58118
58119 58218
58231 58330
* - Administered port exclusions.
Eight ranges of exactly a hundred ports, all inside the dynamic range, which
netsh interface ipv4 show dynamicport tcp gave as Start Port : 49152 and
Number of Ports : 16384. An earlier day's notes on this PC listed a different set, so they
move. Microsoft's pages do not say in words which component reserves them; after reading the
container-networking, WSL networking and troubleshooting pages, the honest statement is that
netsh shows them and nothing else does. What matters is what they do to a bind.
The template web app on http://localhost:57250, inside the first range, with no other
process anywhere near the port:
System.IO.IOException: Failed to bind to address http://localhost:57250.
System.Net.Sockets.SocketException (10013): An attempt was made to access a socket in a way forbidden by its access permissions.
The process exited with the unhandled-exception code, and the exception holds the 10013 twice, once for 127.0.0.1 and once for ::1. Compare the genuinely busy port, a second copy of the app on 15930 while the first was serving:
System.IO.IOException: Failed to bind to address http://127.0.0.1:15930: address already in use.
System.Net.Sockets.SocketException (10048): Only one usage of each socket address (protocol/network address/port) is normally permitted.
10013 "forbidden by its access permissions" is a reserved range; 10048 "address already in use" is
another process. Microsoft's page on error 10013 gives the workaround as a port outside the dynamic
range, and the row marked with a star behaves differently: a bind on 50030, inside the administered
range, served HTTP/1.1 200 OK, which matches the old knowledge base note that an
administered exclusion only keeps a port out of random assignment. The rule for a dev PC needs no
source: pick ports below 49152, and this series uses 15xxx. Docker's form of the same refusal,
"ports are not available", has a
Fixes post.
localhost is two addresses
--urls http://localhost:15931 made Kestrel open two sockets, 127.0.0.1:15931 and
::1:15931, and all three spellings of the URL answered 200. --urls
http://127.0.0.1:15932 opened one, and http://[::1]:15932/ failed:
curl: (7) Failed to connect to ::1:15932 after 2030 ms: Could not connect to server
http://localhost:15932/ still worked, and curl -v shows the detour:
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:15932...
* Trying 127.0.0.1:15932...
* Established connection to localhost (127.0.0.1 port 15932) from 127.0.0.1 port 49678
curl tries ::1 first and falls back. A client that does not fall back, or a tool that resolves
localhost to ::1 only, sees a refused connection; one that does fall back pays the wait, and on this
PC every refused loopback connection took about two seconds to report, measured five times and
documented nowhere this part found. The cheap fix is on the server side: write localhost
in --urls and launchSettings.json, and both families are open.
The firewall as it really is
PS> Get-NetFirewallProfile | Select-Object Name, Enabled, DefaultInboundAction, DefaultOutboundAction, NotifyOnListen | Format-Table
Name Enabled DefaultInboundAction DefaultOutboundAction NotifyOnListen
---- ------- -------------------- --------------------- --------------
Domain True NotConfigured NotConfigured True
Private True NotConfigured NotConfigured True
Public True NotConfigured NotConfigured True
"NotConfigured" reads as if nothing were enforced. It is the persistent store's way of saying "the
default"; the same query with -PolicyStore ActiveStore showed the effective values,
inbound Block and outbound Allow on all three profiles, which is what the
documentation describes: "block all incoming traffic, unless solicited or matching a rule" and
"allow all outgoing traffic, unless matching a rule". This PC's one network is on the Public
profile, "the default profile for unidentified networks". NotifyOnListen True is the
prompt. Two programs had met it earlier the same day:
DisplayName Direction Action Profile Enabled
----------- --------- ------ ------- -------
ollama.exe Inbound Allow Public True
ollama.exe Inbound Allow Public True
portlab.exe Inbound Allow Public True
portlab.exe Inbound Allow Public True
One TCP rule and one UDP rule each, named "TCP Query User" plus a GUID and the exe's full path,
any local port, any remote address, Public profile only. The event log shows the prompt as a
sequence: at 10:09:32 two 2097 events, "A rule has been added", both with Action: Block
and the firewall service as the modifying application; three seconds later six 2099 events, "A rule
has been modified", with Action: Allow and the signed-in user's account. That is what
clicking Allow does: it flips two block rules that already existed into allow rules for every port
of that exe, on the profile that was active, and the documentation adds that once the rules exist
"they must be deleted to generate the prompt again." The other half of the story is the absence of
events. This part ran the app on localhost on five ports, a WSL relay on 127.0.0.1 and a Docker
publish on 127.0.0.1, all with NotifyOnListen True, and the log recorded nothing and no
rule names PortLab3. No Microsoft page fetched says so in words; observed on this PC, a server bound
to loopback never prompts, and the two programs that did prompt had listened beyond it.
WSL, in both directions
A throwaway distribution, installed as in Part 2, ran Python's built-in server on
127.0.0.1:15933. The first attempt to start it in the background from one
wsl call left nothing behind:
ls: cannot access '/tmp/http15933.log': No such file or directory
no python3 process
bash returned at once and the session died before the child opened its log file; the same command
with & sleep 1 at the end kept it alive, listed by ss -ltnp as
python3 on 127.0.0.1:15933. From Windows, http://localhost:15933/ and
http://127.0.0.1:15933/ answered HTTP/1.0 200 OK with
Server: SimpleHTTP/0.6 Python/3.12.3, and [::1] did not. On the Windows side
the port belongs to a relay:
Address Port Process
------- ---- -------
127.0.0.1 15933 wslrelay
The relay mirrors the Linux socket's family, so a Linux server on 127.0.0.1 is 127.0.0.1 on Windows
too. The documentation calls this accessing Linux apps from Windows "using localhost (just like you
normally would)". The reverse is not symmetric. With the web app on localhost:15934 on
Windows, from inside the distribution:
curl: (7) Failed to connect to localhost port 15934 after 0 ms: Couldn't connect to server
Inside WSL, localhost is the Linux VM's own loopback. The documented route is the host's address,
from ip route: default via 172.24.48.1 dev eth0, which is the Windows side of
the vEthernet (WSL (Hyper-V firewall)) adapter. That address timed out after ten seconds
rather than refusing, because the app listened on loopback only and nothing answered on the
adapter. Making it work needs what the documentation says: "you may need to bind your application to
0.0.0.0 instead of 127.0.0.1", with its own warning, "Keep security in mind when making these
changes as this will allow connections from your LAN." That is the bind that triggers the prompt
above, and this series does not do it on the host; the mirrored networking mode, which the page
recommends and which was not switched on here, lets both sides use 127.0.0.1.
Docker's share of the ports
A throwaway PostgreSQL container published with an address, -p 127.0.0.1:15935:5432,
appeared on Windows as one socket:
Address Port Process
------- ---- -------
127.0.0.1 15935 com.docker.backend
And that program already has its rules: two Docker Desktop Backend entries, Inbound,
Allow, Public. Put the two facts together with the first table, where an unaddressed
-p 5432:5432 sat on 0.0.0.0 and ::, and the conclusion is that a
plain -p is reachable from the LAN with no prompt at all, because the prompt belongs to
the backend and was answered when Docker Desktop was installed. This was not tested from a second
machine; the listener address and the rule are the evidence. 127.0.0.1: in front of the
host port keeps a development database on this PC, and the
Docker series uses that form throughout.
Three habits: dev ports below 49152, localhost rather than
127.0.0.1 in --urls, and never 0.0.0.0 on the host or an
unaddressed -p in Docker unless the LAN is meant to reach it. The Windows Security
Alert then never appears, and the ports table stays readable.
Frequently asked
- Why does Kestrel fail to bind with error 10013 on a port nothing is using?
- The port is inside a range Windows has excluded; netsh interface ipv4 show excludedportrange protocol=tcp lists them. On this PC eight ranges of a hundred ports sat inside the dynamic range 49152 to 65535 and moved between days. A genuinely busy port gives 10048, address already in use, instead. Pick a port below 49152.
- Why does my app answer on 127.0.0.1 but not on localhost?
- Because localhost resolves to ::1 first on Windows and the app listens on IPv4 only. Binding --urls to http://localhost:port opens both 127.0.0.1 and ::1; a literal 127.0.0.1 opens one socket, and clients that try ::1 first either fall back after a delay, about two seconds here, or fail. Write localhost in --urls and launchSettings.json.
- Why does the Windows Security Alert appear for my .NET app, and what does Allow do?
- The firewall's NotifyOnListen setting prompts when a program listens beyond loopback on a profile with no rule for it. Allow turns the two block rules just created, TCP and UDP, into allow rules for every port of that exe on the active profile; they stay until deleted. A server bound to localhost produced no prompt and no event in this test.
Next: Part 4, databases on a dev PC: LocalDB, SQL Server Express and PostgreSQL in Docker, which one a connection string reaches, and the three errors that cost an afternoon.