feat(agent): 配对窗口可显式绑定地址,默认仍是内网可达

配对窗口是 agent 唯一持有的监听端口,而它授予的是设备信任:谁能带节点密码打到
/provision,谁就拿到一把 agent 会听命的密钥。此前它固定绑所有接口,且没有任何办法
收窄——一台本身能从公网路由到的节点(公网 VPS 上的 SimAdmin、放在非可信网络里的
设备)在窗口开启期间就把这个面暴露给了整个互联网。

现在加 --pair-bind,只接受 IP 字面量。不接受主机名是刻意的:绑定时解析名字意味着
监听地址取决于 DNS 当时说什么,那不是运维做过的决定。

默认值仍是绑所有接口,这个我改过一次又改回来了,原因值得记下来:我先把默认设成
127.0.0.1 并写了理由,然后发现配对按设计走内网、手机要直接打到节点,回环监听从
另一台机器根本连不上——那不是一个加固措施,而是一个永远不工作的流程,用户对这个
功能的第一次体验会是"连接被拒"且没有解释。所以默认保持内网可达,把"节点能从公网
路由到时请收窄"写进部署文档,并在 --pair-bind 上给出显式入口。

测试断言默认值同时满足两件事:是 IP 字面量(不是名字),且不是回环(手机要能连)。

Go 全包、Node 120 项、Swift 107 项通过。
This commit is contained in:
chick
2026-09-17 07:47:25 +08:00
parent 0f983f046a
commit b5f548f1ad
3 changed files with 55 additions and 7 deletions
+23
View File
@@ -6,6 +6,7 @@ import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"net"
"net/http"
"net/http/httptest"
"os"
@@ -280,3 +281,25 @@ func TestServeDeadlineClosesTheWindow(t *testing.T) {
t.Fatalf("status after the deadline: %d", response.StatusCode)
}
}
// The window's default bind has to satisfy two things at once, and they pull in opposite directions.
//
// It must be reachable from the phone: pairing happens over the LAN by design, so a loopback default
// would be a flow that never works. And it must be a literal rather than a name: a name is resolved at
// bind time, so the listening address would depend on what DNS said, which is not a decision the
// operator made.
func TestTheDefaultBindIsALiteralAndIsReachableFromTheLAN(t *testing.T) {
if net.ParseIP(DefaultAdminHost) == nil {
t.Fatalf("%q is not an IP literal; a name would be resolved at bind time", DefaultAdminHost)
}
ip := net.ParseIP(DefaultAdminHost)
if ip.IsLoopback() {
t.Fatal("a loopback default cannot be reached from the phone, and pairing is a LAN flow")
}
// Any-address is the default because the documented flow requires it; the operator narrows it with
// --pair-bind when the node is routable from somewhere it should not be.
if !ip.IsUnspecified() {
t.Fatalf("the default bind changed to %q; the documented flow and the deployment notes assume %q",
DefaultAdminHost, "0.0.0.0")
}
}