Windows: daemon fails to bind default port 1900 — port falls inside a winnat excluded range (WSAEACCES 10013)
中英双语:中文在前,English version attached below.
中文版
问题
FreeToken Desktop 报错:
daemon-unreachable: http://127.0.0.1:1900
http://127.0.0.1:1900/checkpoint/start: Connection Fled: Connect error:
No connection could be made because target machine actively refused it. (os error 10061)
这个报错是误导性的。它让人以为 daemon 没启动,实际是 daemon 每次 bind 1900 就立刻失败退出。前台手动启动才能看到真实错误:
$ ft daemon --port 1900 --log-level debug
ERROR: [Errno 13] error while attempting to bind on address ('127.0.0.1', 1900):
[winerror 10013] an attempt was made to access a socket in a way forbidden by its access permissions
为什么
winnat 驱动(由 WSL2 / Docker Desktop / Hyper-V 等网络虚拟化组件触发)会从 Windows 的 TCP 动态端口范围里随机抽取若干端口块保留。落在保留块内的端口,任何进程 bind 都会被直接拒绝。
关键区分点:错误码是 10013 (WSAEACCES),不是 10048 (WSAEADDRINUSE)。10013 表示端口被系统保留,而不是被别的进程占用 —— 两者处理方式完全不同,靠这个指纹即可精确定位。
本机保留范围:
Start Port End Port
---------- --------
1711 1810
1811 1910 <-- 1901 落在这一块
1911 2010
故障是间歇性的:保留块每次 winnat 重启都会重新随机分配。本机三个时点采样,1901 两次被保留、一次侥幸逃过。所以"重启好了、过会儿又坏了"是必然现象,不是玄学。
影响范围不止一个端口 —— 三个默认端口全部落在低位区间(winnat 重灾区),本机实测无一幸免:
| 用途 |
默认端口 |
实测 |
ft daemon 控制端口 |
1900 |
❌ 10013 |
| Desktop 连接 daemon |
1901 |
❌ 10013 |
ft serve 默认端口 |
1919 |
❌ 10013 |
也就是说:装了 WSL2 或 Docker Desktop 的 Windows 开发机上,FreeToken 会随机开箱失败。本地 LLM 用户里这类机器占比很高。
建议
1. 默认端口移到 5 位,避开动态端口范围(核心)
winnat 只会从动态端口范围内抽块。Windows 默认是 49152–65535,但开发机常被改过(本机 1024–15000)。取交集后的安全带是 15000–49151。
具体数字只要落在 15000–49151 即可;若保留 19xx 识别度,可考虑 19000 / 19001 / 19191(本机实测均可绑定)。
2. bind 失败时透传真实原因(重要)
当前 Desktop 把一切失败都折叠成 os error 10061,是这次排查成本高的主要原因。建议:
- daemon 捕获 10013 并明确输出「端口被 Windows 保留,请更换端口」,附上
netsh interface ipv4 show excludedportrange protocol=tcp
- Desktop 展示 daemon 的真实启动失败原因,而不是统一的 connection refused
3. 端口不可用时自动回退
bind 失败(10013 或 10048)时自动顺延尝试下一个端口,把实际端口写回 state 文件供 Desktop 读取,用户无需介入。
4. 改端口后提供「应用并重启」按钮
目前改端口必须手动退出并重启 Desktop 的 exe,链路很长。建议在设置项旁边提供一键应用/重启 daemon 的按钮。
复现步骤
- Windows 11 + WSL2 或 Docker Desktop 运行中
netsh interface ipv4 show excludedportrange protocol=tcp 确认 1900 落入保留块
- 启动 Desktop,或手动
ft daemon --port 1900 --log-level debug
- 出现
winerror 10013,daemon 退出,Desktop 报 daemon-unreachable
附:诊断脚本
diagnose-ports.ps1(PowerShell 5.1 兼容,无需管理员权限)可一键判定端口是否安全并推荐可用端口,供集成到 Desktop 自检时参考:
.\diagnose-ports.ps1 # 自动探测 ft 进程监听的端口
.\diagnose-ports.ps1 -Port 1900 # 检查指定端口
故障态输出:
[2] 端口 1900 判定 / verdict
[FAIL] 1900 落在保留块 1811-1910 内 / falls inside reserved block
[FAIL] bind 失败 WinError 10013 (WSAEACCES) -- 端口被系统保留 / port is RESERVED by Windows
[4] 可用端口推荐 / recommended ports
[OK] 19001 可用 / usable
[5] 结论 / conclusion
[FAIL] 端口 1900 不可用,必须更换 / port 1900 is unusable, you must change it
环境
Windows 11(PS 5.1 / 7 均可复现)· FreeToken Desktop 0.1.2 · Engine freetoken 0.1.2+g932e87e8a(Python 3.12 venv)· RTX 4090 / 驱动 610.88 · 共存 WSL2 (Ubuntu running)、Docker Desktop、VMware
English
Problem
FreeToken Desktop reports:
daemon-unreachable: http://127.0.0.1:1900
http://127.0.0.1:1900/checkpoint/start: Connection Fled: Connect error:
No connection could be made because target machine actively refused it. (os error 10061)
This message is misleading. It suggests the daemon never started, when in fact the daemon fails to bind 1900 and exits immediately, every time. Running it in the foreground reveals the real error:
$ ft daemon --port 1900 --log-level debug
ERROR: [Errno 13] error while attempting to bind on address ('127.0.0.1', 1900):
[winerror 10013] an attempt was made to access a socket in a way forbidden by its access permissions
Why
The winnat driver — triggered by network virtualization components such as WSL2, Docker Desktop and Hyper-V — reserves random blocks of ports drawn from Windows' TCP dynamic port range. Any process binding a port inside a reserved block is refused outright.
The decisive detail: the error is 10013 (WSAEACCES), not 10048 (WSAEADDRINUSE). 10013 means the port is reserved by the system, not occupied by another process — two very different situations, and this fingerprint identifies it precisely.
Reserved ranges on this machine:
Start Port End Port
---------- --------
1711 1810
1811 1910 <-- 1900 falls inside this block
1911 2010
The failure is intermittent: winnat re-rolls the reserved blocks every time it restarts. Across three samples on this machine, 1900 was reserved twice and escaped once. So "rebooting fixes it, then it breaks again" is expected behavior, not a mystery.
It is not limited to one port — all three defaults sit in the low range where winnat strikes most often, and all three fail here:
| Purpose |
Default port |
Measured |
ft daemon control port |
1900 |
❌ 10013 |
| Desktop → daemon port |
1901 |
❌ 10013 |
ft serve default port |
1919 |
❌ 10013 |
In other words: on any Windows dev machine running WSL2 or Docker Desktop, FreeToken can fail out of the box — at random. That is a large share of local-LLM users.
Suggestions
1. Move the default ports to 5 digits, outside the dynamic port range (core fix)
winnat only draws reserved blocks from the dynamic port range. Windows defaults to 49152–65535, but dev machines often have it changed (this one uses 1024–15000). The safe belt is 15000–49151.
The exact numbers are yours to pick — anything in 15000–49151 works. To keep the 19xx identity, 19000 / 19001 / 19191 are verified bindable here.
2. Surface the real reason on bind failure (important)
Desktop currently collapses every failure into os error 10061; that was the main reason this took so long to diagnose. Suggested:
- Catch 10013 in the daemon and print an explicit "port reserved by Windows, choose a different port", referencing
netsh interface ipv4 show excludedportrange protocol=tcp
- Show the daemon's actual startup failure in the Desktop UI instead of a blanket "connection refused"
3. Automatic port fallback
If bind fails (10013 or 10048), try the next port automatically, write the effective port to the state file, and let Desktop read it from there — no manual intervention.
4. Provide an "Apply & restart" button for port changes
Changing the port today requires manually quitting and relaunching the Desktop exe, which is a long loop. Please add a one-click apply/restart-daemon button next to the setting.
Steps to reproduce
- Windows 11 with WSL2 or Docker Desktop running
- Confirm 1900 is inside a reserved block:
netsh interface ipv4 show excludedportrange protocol=tcp
- Launch Desktop, or run
ft daemon --port 1900 --log-level debug
winerror 10013 appears, the daemon exits, Desktop reports daemon-unreachable
Appendix: diagnostic script
diagnose-ports.ps1 (PowerShell 5.1 compatible, no admin rights) determines in one run whether a port is safe and suggests usable ones — offered as a reference if you want to integrate a self-check into Desktop:
.\diagnose-ports.ps1 # auto-detect the port the ft process listens on
.\diagnose-ports.ps1 -Port 1900 # check a specific port
Sample output (failure case):
[2] verdict for port 1900
[FAIL] 1900 falls inside reserved block 1811-1910
[FAIL] bind failed WinError 10013 (WSAEACCES) -- port is RESERVED by Windows
[4] recommended ports
[OK] 19001 usable
[5] conclusion
[FAIL] port 1900 is unusable, you must change it
Environment
Windows 11 (reproduces on PS 5.1 and 7) · FreeToken Desktop 0.1.2 · Engine freetoken 0.1.2+g932e87e8a (Python 3.12 venv) · RTX 4090 / driver 610.88 · co-resident WSL2 (Ubuntu running), Docker Desktop, VMware
Attachment
#requires -Version 5.1
<#
.SYNOPSIS
FreeToken daemon 端口安全诊断器 / FreeToken daemon port safety checker.
.DESCRIPTION
一键判断 FreeToken daemon 的控制端口在当前 Windows 机器上是否可用。
One-shot check: is the FreeToken daemon control port usable on this Windows box?
背景 / WHY THIS EXISTS
----------------------
Windows 的 winnat 驱动(由 WSL2 / Docker Desktop / Hyper-V 触发)会从
"TCP 动态端口范围"中随机抽取若干块端口并保留。落在保留块里的端口,任何进程
bind 都会立刻失败并报 WinError 10013 (WSAEACCES),而不是常见的 10048 (占用)。
保留块会随 winnat 重启/系统重启而**重新随机漂移**,因此同一端口时好时坏。
On Windows, the winnat driver (triggered by WSL2 / Docker Desktop / Hyper-V)
reserves random blocks of ports drawn from the TCP dynamic port range. Binding a
port inside a reserved block fails instantly with WinError 10013 (WSAEACCES) --
NOT the usual 10048 (already in use). The blocks re-roll whenever winnat restarts,
so the SAME port works today and fails tomorrow.
FreeToken 的三个默认端口 1900 (daemon 默认) / 1901 (Desktop 连接 daemon) /
1919 (serve 默认) 都位于低位,极易被抽中。
FreeToken's defaults 1900 / 1901 / 1919 sit in the low range and get hit often.
本脚本会 / THIS SCRIPT:
1. 打印当前被保留的端口范围 print current excluded port ranges
2. 判断目标端口是否落在保留块内 tell you if your port falls in one
3. 实弹 bind 测试,区分 10013/10048 live-bind test, distinguishing 10013 vs 10048
4. 检查 daemon 当前是否在运行 check whether the daemon is running
5. 扫描并推荐可用的 5 位端口 scan and recommend usable 5-digit ports
6. 给出可执行的修复建议 give actionable remediation
.EXAMPLE
.\diagnose-ports.ps1
# 自动探测 daemon 端口(先看 ft 进程实际监听的,否则用默认 1901)
.EXAMPLE
.\diagnose-ports.ps1 -Port 19010
# 显式指定要检查的端口
.NOTES
无需管理员权限。/ Does NOT require administrator rights.
兼容 Windows PowerShell 5.1。/ Windows PowerShell 5.1 compatible.
#>
[CmdletBinding()]
param(
# 要检查的端口;省略则自动探测
[int] $Port = 0
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
# 5 位候选端口池:刻意避开两个危险区 / 5-digit candidates, deliberately outside both danger zones:
# 危险区 A / danger zone A: 1024-15000 当前 Windows 动态端口范围(winnat 抽取区)
# 危险区 B / danger zone B: 49152-65535 IANA/Windows 默认动态端口范围
# => 安全带 / safe belt: 15000-49151
$CandidatePorts = @(19010, 19191, 19001, 19190, 19000, 18080, 21000, 20010)
function Write-Head([string] $m) { Write-Host ''; Write-Host $m -ForegroundColor Cyan }
function Say-OK([string] $m) { Write-Host " [OK] $m" -ForegroundColor Green }
function Say-Bad([string] $m) { Write-Host " [FAIL] $m" -ForegroundColor Red }
function Say-Warn([string] $m) { Write-Host " [WARN] $m" -ForegroundColor Yellow }
function Say-Info([string] $m) { Write-Host " $m" }
# --- 1. 解析保留端口范围 ---------------------------------------------------
function Get-ExcludedRanges {
# PS 5.1 陷阱 / PS 5.1 gotcha: a native command's stderr becomes a terminating
# NativeCommandError under ErrorActionPreference='Stop', which would abort the whole
# script on the first netsh line. Relax it for the call and merge stderr into stdout.
# (Same workaround FreeToken's own install.ps1 documents.)
$prev = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
$raw = ''
try { $raw = & netsh interface ipv4 show excludedportrange protocol=tcp 2>&1 }
finally { $ErrorActionPreference = $prev }
$list = @()
foreach ($line in $raw) {
if ($line -match '^\s+(\d+)\s+(\d+)') {
$list += [pscustomobject]@{
Start = [int] $Matches[1]
End = [int] $Matches[2]
}
}
}
# NB: no unary comma here. `return ,$list` would wrap the array in a one-element
# array, so callers would get a nested array and `$r.Start` would silently
# member-enumerate instead of comparing a scalar.
return $list
}
function Test-PortInRange([int] $p, $ranges) {
foreach ($r in $ranges) {
if ($p -ge $r.Start -and $p -le $r.End) { return $r }
}
return $null
}
# --- 2. 实弹 bind 测试,区分错误码 -----------------------------------------
# 10013 = WSAEACCES 被系统保留(winnat 抽中),无解,只能换端口
# 10048 = WSAEADDRINUSE 被别的进程占用(可能是你自己的 daemon,其实是好事)
function Test-PortBindable([int] $p) {
$sock = $null
try {
$sock = New-Object System.Net.Sockets.Socket(
[System.Net.Sockets.AddressFamily]::InterNetwork,
[System.Net.Sockets.SocketType]::Stream,
[System.Net.Sockets.ProtocolType]::Tcp)
$sock.Bind((New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Loopback, $p)))
return [pscustomobject]@{ Ok = $true; Code = 0 }
} catch {
$code = 0
$ex = $_.Exception
while ($null -ne $ex) {
if ($ex -is [System.Net.Sockets.SocketException]) { $code = $ex.ErrorCode; break }
$ex = $ex.InnerException
}
return [pscustomobject]@{ Ok = $false; Code = $code }
} finally {
if ($null -ne $sock) { $sock.Dispose() }
}
}
# --- 3. 探测 daemon 实际监听端口 -------------------------------------------
function Get-DaemonListenPort {
$procs = @(Get-Process -Name 'ft' -ErrorAction SilentlyContinue)
$ports = @()
foreach ($p in $procs) {
$conns = @(Get-NetTCPConnection -State Listen -OwningProcess $p.Id -ErrorAction SilentlyContinue)
foreach ($c in $conns) { $ports += $c.LocalPort }
}
return $ports
}
Write-Host '==========================================================' -ForegroundColor DarkGray
Write-Host ' FreeToken daemon 端口诊断 / daemon port diagnostics' -ForegroundColor White
Write-Host '==========================================================' -ForegroundColor DarkGray
# --- 端口决策 / decide which port to check ---------------------------------
$daemonPorts = @(Get-DaemonListenPort)
if ($Port -eq 0) {
if ($daemonPorts.Count -gt 0) {
$Port = $daemonPorts[0]
Say-Info "探测到 ft 进程正在监听 / detected ft listening on: $($daemonPorts -join ', ')"
} else {
$Port = 1901
Say-Info "未发现运行中的 ft 进程,检查默认端口 / no ft process found, checking default: 1901"
}
}
Say-Info "目标端口 / target port: $Port"
# --- 排除范围 / excluded ranges --------------------------------------------
Write-Head '[1] 当前被系统保留的端口范围 / excluded port ranges (winnat)'
$ranges = @(Get-ExcludedRanges)
if ($ranges.Count -eq 0) {
Say-OK '没有保留范围 / no excluded ranges'
} else {
foreach ($r in $ranges) { Write-Host (" {0,6} - {1,6}" -f $r.Start, $r.End) }
}
# --- 判定 / verdict --------------------------------------------------------
Write-Head "[2] 端口 $Port 判定 / verdict"
$hit = Test-PortInRange $Port $ranges
if ($null -ne $hit) {
Say-Bad "$Port 落在保留块 $($hit.Start)-$($hit.End) 内 / falls inside reserved block"
} else {
Say-OK "$Port 不在任何保留块内 / not inside any reserved block"
}
$test = Test-PortBindable $Port
switch ($test.Code) {
0 {
Say-OK "bind 成功,端口完全可用 / bind succeeded, port is fully usable"
}
10013 {
Say-Bad "bind 失败 WinError 10013 (WSAEACCES) -- 端口被系统保留 / port is RESERVED by Windows"
Say-Info " 这是 winnat 保留端口,无法释放,只能换端口 / reserved by winnat; you must change the port"
}
10048 {
Say-OK "bind 失败 WinError 10048 -- 已被占用,说明端口本身可用 / in use, which means the port itself is fine"
if ($daemonPorts -contains $Port) {
Say-Info " 占用者正是你的 FreeToken daemon(正常)/ held by your own FreeToken daemon (expected)"
} else {
Say-Warn " 被其他进程占用,需查明 / held by another process, investigate with:"
Say-Info " netstat -ano | findstr `":$Port`""
}
}
default {
Say-Warn "bind 失败,错误码 $($test.Code) / bind failed with code $($test.Code)"
}
}
# --- daemon 健康检查 / daemon health --------------------------------------
Write-Head '[3] daemon 运行状态 / daemon status'
if ($daemonPorts.Count -gt 0) {
Say-OK "daemon 正在运行,监听端口 / running, listening on: $($daemonPorts -join ', ')"
try {
$resp = Invoke-RestMethod -Uri "http://127.0.0.1:$Port/health" -TimeoutSec 5 -ErrorAction Stop
Say-Info (" health: " + ($resp | ConvertTo-Json -Compress))
} catch {
Say-Warn " /health 探测失败(端口可能被其他进程占用)/ health probe failed: $($_.Exception.Message)"
}
} else {
Say-Warn '未发现运行中的 ft daemon 进程 / no running ft daemon process found'
Say-Info ' 若 Desktop 报 daemon-unreachable,很可能就是 daemon bind 失败后自杀了'
Say-Info ' if Desktop says "daemon-unreachable", the daemon most likely killed itself after a bind failure'
}
# --- 可用端口推荐 / recommendations ----------------------------------------
Write-Head '[4] 可用端口推荐 / recommended ports (5-digit, outside both danger zones)'
$good = @()
foreach ($c in $CandidatePorts) {
if ($c -eq $Port) { continue }
$r = Test-PortBindable $c
$inRange = $null -ne (Test-PortInRange $c $ranges)
if ($r.Ok -and -not $inRange) {
$good += $c
Say-OK "$c 可用 / usable"
} else {
$why = if ($inRange) { '在保留块内 / in reserved block' } elseif ($r.Code -eq 10048) { '已占用 / in use' } else { "错误 $($r.Code) / error $($r.Code)" }
Say-Info "$c 跳过 ($why) / skipped ($why)"
}
}
# --- 结论与建议 / conclusion ----------------------------------------------
Write-Head '[5] 结论 / conclusion'
$usable = $test.Ok -or $test.Code -eq 10048
if ($usable -and $null -eq $hit) {
Say-OK "端口 $Port 当前可用 / port $Port is currently usable"
Say-Warn '但注意:保留块会随 winnat 重启重新漂移,今天可用不代表明天可用'
Say-Warn 'CAUTION: blocks re-roll when winnat restarts -- working today does NOT mean working tomorrow'
} else {
Say-Bad "端口 $Port 不可用,必须更换 / port $Port is unusable, you must change it"
if ($good.Count -gt 0) {
Say-Info " 建议改用 / recommended: $($good[0])"
Say-Info " 在 FreeToken Desktop 设置中把 daemon 端口改为上述值,重启 Desktop"
Say-Info " change the daemon port in FreeToken Desktop settings, then restart Desktop"
}
}
Write-Head '[6] 保留块来源提示 / who reserved them'
Say-Info 'winnat 保留块由以下组件触发(本机会同时存在多个)/ triggered by (several can coexist):'
Say-Info ' - WSL2(vmmemWSL 运行时)/ WSL2 while running'
Say-Info ' - Docker Desktop(com.docker.service)/ Docker Desktop'
Say-Info ' - Hyper-V 虚拟交换机 / Hyper-V virtual switches'
Say-Info ' 注:VMware 的 NAT 不走 winnat,不产生保留块'
Say-Info ' note: VMware NAT does not use winnat and produces no such blocks'
Say-Info ''
Say-Info '要确认具体归属,停止某组件后重新运行本脚本,对比消失的块:'
Say-Info ' wsl --shutdown # 停 WSL2'
Say-Info ' 退出 Docker Desktop # quit Docker Desktop'
Say-Info ' 再跑 / re-run: .\diagnose-ports.ps1'
Write-Host ''
中文版
问题
FreeToken Desktop 报错:
这个报错是误导性的。它让人以为 daemon 没启动,实际是 daemon 每次 bind 1900 就立刻失败退出。前台手动启动才能看到真实错误:
为什么
winnat驱动(由 WSL2 / Docker Desktop / Hyper-V 等网络虚拟化组件触发)会从 Windows 的 TCP 动态端口范围里随机抽取若干端口块保留。落在保留块内的端口,任何进程 bind 都会被直接拒绝。关键区分点:错误码是 10013 (WSAEACCES),不是 10048 (WSAEADDRINUSE)。10013 表示端口被系统保留,而不是被别的进程占用 —— 两者处理方式完全不同,靠这个指纹即可精确定位。
本机保留范围:
故障是间歇性的:保留块每次 winnat 重启都会重新随机分配。本机三个时点采样,1901 两次被保留、一次侥幸逃过。所以"重启好了、过会儿又坏了"是必然现象,不是玄学。
影响范围不止一个端口 —— 三个默认端口全部落在低位区间(winnat 重灾区),本机实测无一幸免:
ft daemon控制端口ft serve默认端口也就是说:装了 WSL2 或 Docker Desktop 的 Windows 开发机上,FreeToken 会随机开箱失败。本地 LLM 用户里这类机器占比很高。
建议
1. 默认端口移到 5 位,避开动态端口范围(核心)
winnat 只会从动态端口范围内抽块。Windows 默认是
49152–65535,但开发机常被改过(本机1024–15000)。取交集后的安全带是 15000–49151。具体数字只要落在 15000–49151 即可;若保留
19xx识别度,可考虑19000 / 19001 / 19191(本机实测均可绑定)。2. bind 失败时透传真实原因(重要)
当前 Desktop 把一切失败都折叠成
os error 10061,是这次排查成本高的主要原因。建议:netsh interface ipv4 show excludedportrange protocol=tcp3. 端口不可用时自动回退
bind 失败(10013 或 10048)时自动顺延尝试下一个端口,把实际端口写回 state 文件供 Desktop 读取,用户无需介入。
4. 改端口后提供「应用并重启」按钮
目前改端口必须手动退出并重启 Desktop 的 exe,链路很长。建议在设置项旁边提供一键应用/重启 daemon 的按钮。
复现步骤
netsh interface ipv4 show excludedportrange protocol=tcp确认 1900 落入保留块ft daemon --port 1900 --log-level debugwinerror 10013,daemon 退出,Desktop 报daemon-unreachable附:诊断脚本
diagnose-ports.ps1(PowerShell 5.1 兼容,无需管理员权限)可一键判定端口是否安全并推荐可用端口,供集成到 Desktop 自检时参考:故障态输出:
环境
Windows 11(PS 5.1 / 7 均可复现)· FreeToken Desktop 0.1.2 · Engine
freetoken 0.1.2+g932e87e8a(Python 3.12 venv)· RTX 4090 / 驱动 610.88 · 共存 WSL2 (Ubuntu running)、Docker Desktop、VMwareEnglish
Problem
FreeToken Desktop reports:
This message is misleading. It suggests the daemon never started, when in fact the daemon fails to bind 1900 and exits immediately, every time. Running it in the foreground reveals the real error:
Why
The
winnatdriver — triggered by network virtualization components such as WSL2, Docker Desktop and Hyper-V — reserves random blocks of ports drawn from Windows' TCP dynamic port range. Any process binding a port inside a reserved block is refused outright.The decisive detail: the error is 10013 (WSAEACCES), not 10048 (WSAEADDRINUSE). 10013 means the port is reserved by the system, not occupied by another process — two very different situations, and this fingerprint identifies it precisely.
Reserved ranges on this machine:
The failure is intermittent: winnat re-rolls the reserved blocks every time it restarts. Across three samples on this machine, 1900 was reserved twice and escaped once. So "rebooting fixes it, then it breaks again" is expected behavior, not a mystery.
It is not limited to one port — all three defaults sit in the low range where winnat strikes most often, and all three fail here:
ft daemoncontrol portft servedefault portIn other words: on any Windows dev machine running WSL2 or Docker Desktop, FreeToken can fail out of the box — at random. That is a large share of local-LLM users.
Suggestions
1. Move the default ports to 5 digits, outside the dynamic port range (core fix)
winnat only draws reserved blocks from the dynamic port range. Windows defaults to
49152–65535, but dev machines often have it changed (this one uses1024–15000). The safe belt is 15000–49151.The exact numbers are yours to pick — anything in 15000–49151 works. To keep the
19xxidentity,19000 / 19001 / 19191are verified bindable here.2. Surface the real reason on bind failure (important)
Desktop currently collapses every failure into
os error 10061; that was the main reason this took so long to diagnose. Suggested:netsh interface ipv4 show excludedportrange protocol=tcp3. Automatic port fallback
If bind fails (10013 or 10048), try the next port automatically, write the effective port to the state file, and let Desktop read it from there — no manual intervention.
4. Provide an "Apply & restart" button for port changes
Changing the port today requires manually quitting and relaunching the Desktop exe, which is a long loop. Please add a one-click apply/restart-daemon button next to the setting.
Steps to reproduce
netsh interface ipv4 show excludedportrange protocol=tcpft daemon --port 1900 --log-level debugwinerror 10013appears, the daemon exits, Desktop reportsdaemon-unreachableAppendix: diagnostic script
diagnose-ports.ps1(PowerShell 5.1 compatible, no admin rights) determines in one run whether a port is safe and suggests usable ones — offered as a reference if you want to integrate a self-check into Desktop:Sample output (failure case):
Environment
Windows 11 (reproduces on PS 5.1 and 7) · FreeToken Desktop 0.1.2 · Engine
freetoken 0.1.2+g932e87e8a(Python 3.12 venv) · RTX 4090 / driver 610.88 · co-resident WSL2 (Ubuntu running), Docker Desktop, VMwareAttachment