feat(agent): 配对尝试记录来源 IP

"十次失败"说明窗口关了,"十次来自 10.0.0.7 的失败"说明有人在试——后者才是可行动
的那句话,而它是尝试次数上限无法提供的信息。每次尝试与每次成功现在都带 peer。

只记 IP 不记端口:端口每次都是新的临时号,带上会让同一台机器的两次尝试看起来像
两台,而"有几台机器在试"正是要回答的问题。

测试断言失败尝试带上了 peer,且是去掉端口后的主机。变异验证:去掉 peer,测试失败。

Go 全包、Node 120 项、Swift 107 项通过。
This commit is contained in:
chick
2026-09-17 07:58:34 +08:00
parent b5f548f1ad
commit 5feb5bfe70
2 changed files with 61 additions and 5 deletions
+41
View File
@@ -12,6 +12,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@@ -303,3 +304,43 @@ func TestTheDefaultBindIsALiteralAndIsReachableFromTheLAN(t *testing.T) {
DefaultAdminHost, "0.0.0.0")
}
}
// The log has to name the caller. "Ten failed pairings" tells an operator the window closed; "ten
// failed pairings from 10.0.0.7" tells them someone is trying, which is the only actionable form of
// the same fact and the one thing the attempt ceiling cannot supply.
func TestAPairingAttemptIsLoggedWithItsPeer(t *testing.T) {
path, cfg, _ := agentConfig(t)
var mu sync.Mutex
var attempts []map[string]any
h, err := New(path, cfg, func(event string, fields map[string]any) {
if event != "pair_attempt" {
return
}
mu.Lock()
defer mu.Unlock()
attempts = append(attempts, fields)
})
if err != nil {
t.Fatalf("New: %v", err)
}
signPub, kxPub := phoneKeys(t)
request := httptest.NewRequest("POST", "/provision", provisionBody(t, "wrong", signPub, kxPub))
// A caller behind a NAT, which is the shape a real attempt has.
request.RemoteAddr = "10.0.0.7:54321"
h.Routes().ServeHTTP(httptest.NewRecorder(), request)
mu.Lock()
defer mu.Unlock()
if len(attempts) != 1 {
t.Fatalf("expected one logged attempt, got %d", len(attempts))
}
if attempts[0]["outcome"] != "bad_secret" {
t.Fatalf("outcome is %v, want bad_secret", attempts[0]["outcome"])
}
// The host without its ephemeral port: the port is a fresh number on every attempt, so keeping it
// would make two attempts from one host look like two hosts.
if attempts[0]["peer"] != "10.0.0.7" {
t.Fatalf("peer is %v, want the host alone", attempts[0]["peer"])
}
}