pairling 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -11
- package/bin/pairling.mjs +1 -4
- package/package.json +3 -3
- package/payload/mac/SOURCE_REVISION +1 -1
- package/payload/mac/VERSION +1 -1
- package/payload/mac/companiond/pairlingd.py +82 -145
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +33 -9
- package/payload/mac/connectd/cmd/pairling-connectd/peer_identity_test.go +145 -5
- package/payload/mac/connectd/internal/gateway/proxy.go +16 -2
- package/payload/mac/connectd/internal/gateway/proxy_test.go +103 -4
- package/payload/mac/install/install-runtime.sh +113 -294
- package/payload/mac/install/render-launchd.py +2 -45
- package/payload/mac/install/uninstall-runtime.sh +32 -0
- package/payload-manifest.json +14 -36
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/main.go +0 -121
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd.go +0 -418
- package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd_test.go +0 -894
|
@@ -34,7 +34,7 @@ func TestPeerNodeResolverUsesWhoIsRemoteAddr(t *testing.T) {
|
|
|
34
34
|
},
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
nodeID, ok := resolver.PeerNodeID(context.Background(), "100.64.0.50:12345")
|
|
37
|
+
nodeID, provenance, ok := resolver.PeerNodeID(context.Background(), "100.64.0.50:12345")
|
|
38
38
|
if !ok {
|
|
39
39
|
t.Fatal("resolver should accept tagged peer")
|
|
40
40
|
}
|
|
@@ -44,10 +44,13 @@ func TestPeerNodeResolverUsesWhoIsRemoteAddr(t *testing.T) {
|
|
|
44
44
|
if nodeID != "nPeerCNTRL" {
|
|
45
45
|
t.Fatalf("node ID = %q", nodeID)
|
|
46
46
|
}
|
|
47
|
+
if provenance != "tagged" {
|
|
48
|
+
t.Fatalf("provenance = %q, want tagged", provenance)
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
func TestWhoIsResolvesPeerStableID(t *testing.T) {
|
|
50
|
-
nodeID, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
53
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
51
54
|
Node: &tailcfg.Node{
|
|
52
55
|
StableID: tailcfg.StableNodeID("nPeerCNTRL"),
|
|
53
56
|
Tags: []string{"tag:pairling-phone"},
|
|
@@ -60,6 +63,9 @@ func TestWhoIsResolvesPeerStableID(t *testing.T) {
|
|
|
60
63
|
if nodeID != "nPeerCNTRL" {
|
|
61
64
|
t.Fatalf("node ID = %q, want nPeerCNTRL", nodeID)
|
|
62
65
|
}
|
|
66
|
+
if provenance != "tagged" {
|
|
67
|
+
t.Fatalf("provenance = %q, want tagged", provenance)
|
|
68
|
+
}
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
func TestAssertsGrantedPhoneTagBeforePersist(t *testing.T) {
|
|
@@ -72,15 +78,149 @@ func TestAssertsGrantedPhoneTagBeforePersist(t *testing.T) {
|
|
|
72
78
|
}
|
|
73
79
|
for _, tc := range cases {
|
|
74
80
|
t.Run(tc.name, func(t *testing.T) {
|
|
75
|
-
nodeID, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
81
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
76
82
|
Node: &tailcfg.Node{
|
|
77
83
|
StableID: tailcfg.StableNodeID("nPeerCNTRL"),
|
|
78
84
|
Tags: tc.tags,
|
|
79
85
|
},
|
|
80
86
|
})
|
|
81
|
-
if ok || nodeID != "" {
|
|
82
|
-
t.Fatalf("wrongly accepted node ID %q with tags %#v", nodeID, tc.tags)
|
|
87
|
+
if ok || nodeID != "" || provenance != "" {
|
|
88
|
+
t.Fatalf("wrongly accepted node ID %q provenance %q with tags %#v", nodeID, provenance, tc.tags)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// TestPeerNodeIDProvenanceFromWhoIs covers the interactive-sign-in provenance
|
|
95
|
+
// path: an untagged, user-owned iOS node whose WhoIs hostname starts with
|
|
96
|
+
// pairling-ios- is admitted as "interactive", while a tagged phone is admitted
|
|
97
|
+
// as "tagged". Non-Pairling untagged nodes and OS-mismatched nodes are rejected.
|
|
98
|
+
func TestPeerNodeIDProvenanceFromWhoIs(t *testing.T) {
|
|
99
|
+
cases := []struct {
|
|
100
|
+
name string
|
|
101
|
+
node *tailcfg.Node
|
|
102
|
+
wantNodeID string
|
|
103
|
+
wantProvenance string
|
|
104
|
+
wantOK bool
|
|
105
|
+
}{
|
|
106
|
+
{
|
|
107
|
+
name: "tagged phone",
|
|
108
|
+
node: &tailcfg.Node{
|
|
109
|
+
StableID: tailcfg.StableNodeID("nPeerCNTRL"),
|
|
110
|
+
Tags: []string{"tag:pairling-phone"},
|
|
111
|
+
},
|
|
112
|
+
wantNodeID: "nPeerCNTRL",
|
|
113
|
+
wantProvenance: "tagged",
|
|
114
|
+
wantOK: true,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "untagged interactive ios computed name",
|
|
118
|
+
node: &tailcfg.Node{
|
|
119
|
+
StableID: tailcfg.StableNodeID("nInteractiveIOS"),
|
|
120
|
+
ComputedName: "pairling-ios-b702bb49",
|
|
121
|
+
},
|
|
122
|
+
wantNodeID: "nInteractiveIOS",
|
|
123
|
+
wantProvenance: "interactive",
|
|
124
|
+
wantOK: true,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "untagged non-pairling laptop",
|
|
128
|
+
node: &tailcfg.Node{
|
|
129
|
+
StableID: tailcfg.StableNodeID("nLaptop"),
|
|
130
|
+
ComputedName: "my-laptop",
|
|
131
|
+
},
|
|
132
|
+
wantNodeID: "",
|
|
133
|
+
wantProvenance: "",
|
|
134
|
+
wantOK: false,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "untagged pairling-ios prefix but macOS hostinfo rejected",
|
|
138
|
+
node: &tailcfg.Node{
|
|
139
|
+
StableID: tailcfg.StableNodeID("nFakeIOS"),
|
|
140
|
+
ComputedName: "pairling-ios-x",
|
|
141
|
+
Hostinfo: (&tailcfg.Hostinfo{OS: "macOS"}).View(),
|
|
142
|
+
},
|
|
143
|
+
wantNodeID: "",
|
|
144
|
+
wantProvenance: "",
|
|
145
|
+
wantOK: false,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "empty stable id",
|
|
149
|
+
node: &tailcfg.Node{
|
|
150
|
+
StableID: tailcfg.StableNodeID(""),
|
|
151
|
+
Tags: []string{"tag:pairling-phone"},
|
|
152
|
+
},
|
|
153
|
+
wantNodeID: "",
|
|
154
|
+
wantProvenance: "",
|
|
155
|
+
wantOK: false,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
for _, tc := range cases {
|
|
159
|
+
t.Run(tc.name, func(t *testing.T) {
|
|
160
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{Node: tc.node})
|
|
161
|
+
if ok != tc.wantOK {
|
|
162
|
+
t.Fatalf("ok = %t, want %t (nodeID=%q provenance=%q)", ok, tc.wantOK, nodeID, provenance)
|
|
163
|
+
}
|
|
164
|
+
if nodeID != tc.wantNodeID {
|
|
165
|
+
t.Fatalf("nodeID = %q, want %q", nodeID, tc.wantNodeID)
|
|
166
|
+
}
|
|
167
|
+
if provenance != tc.wantProvenance {
|
|
168
|
+
t.Fatalf("provenance = %q, want %q", provenance, tc.wantProvenance)
|
|
83
169
|
}
|
|
84
170
|
})
|
|
85
171
|
}
|
|
86
172
|
}
|
|
173
|
+
|
|
174
|
+
// TestPeerNodeIDInteractiveAcceptsIOSHostinfo confirms an untagged Pairling iOS
|
|
175
|
+
// node with Hostinfo OS reported as "iOS" (case-insensitive) is still admitted
|
|
176
|
+
// as interactive — the OS check only rejects a non-empty, non-iOS OS.
|
|
177
|
+
func TestPeerNodeIDInteractiveAcceptsIOSHostinfo(t *testing.T) {
|
|
178
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
179
|
+
Node: &tailcfg.Node{
|
|
180
|
+
StableID: tailcfg.StableNodeID("nIOS"),
|
|
181
|
+
ComputedName: "pairling-ios-abc123",
|
|
182
|
+
Hostinfo: (&tailcfg.Hostinfo{OS: "iOS"}).View(),
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
if !ok {
|
|
186
|
+
t.Fatal("untagged pairling iOS node with iOS Hostinfo should be admitted")
|
|
187
|
+
}
|
|
188
|
+
if nodeID != "nIOS" {
|
|
189
|
+
t.Fatalf("nodeID = %q, want nIOS", nodeID)
|
|
190
|
+
}
|
|
191
|
+
if provenance != "interactive" {
|
|
192
|
+
t.Fatalf("provenance = %q, want interactive", provenance)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// TestPeerNodeIDInteractiveFallsBackToHostinfoHostname confirms that when
|
|
197
|
+
// ComputedName is empty, the WhoIs hostname is derived from a valid Hostinfo's
|
|
198
|
+
// Hostname() and the pairling-ios- prefix is still honored.
|
|
199
|
+
func TestPeerNodeIDInteractiveFallsBackToHostinfoHostname(t *testing.T) {
|
|
200
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(&apitype.WhoIsResponse{
|
|
201
|
+
Node: &tailcfg.Node{
|
|
202
|
+
StableID: tailcfg.StableNodeID("nIOSHost"),
|
|
203
|
+
Hostinfo: (&tailcfg.Hostinfo{Hostname: "pairling-ios-fallback", OS: "iOS"}).View(),
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
if !ok {
|
|
207
|
+
t.Fatal("untagged pairling iOS node identified via Hostinfo hostname should be admitted")
|
|
208
|
+
}
|
|
209
|
+
if nodeID != "nIOSHost" {
|
|
210
|
+
t.Fatalf("nodeID = %q, want nIOSHost", nodeID)
|
|
211
|
+
}
|
|
212
|
+
if provenance != "interactive" {
|
|
213
|
+
t.Fatalf("provenance = %q, want interactive", provenance)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// TestPeerNodeIDRejectsNilNode confirms a nil WhoIs or nil Node yields the
|
|
218
|
+
// empty-rejection tuple.
|
|
219
|
+
func TestPeerNodeIDRejectsNilNode(t *testing.T) {
|
|
220
|
+
for _, who := range []*apitype.WhoIsResponse{nil, {Node: nil}} {
|
|
221
|
+
nodeID, provenance, ok := peerNodeIDFromWhoIs(who)
|
|
222
|
+
if ok || nodeID != "" || provenance != "" {
|
|
223
|
+
t.Fatalf("nil node wrongly accepted: nodeID=%q provenance=%q ok=%t", nodeID, provenance, ok)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -21,6 +21,18 @@ const pairDropSmallFileMaxBodyBytes int64 = 10 * 1024 * 1024
|
|
|
21
21
|
const pairDropUploadChunkMaxBodyBytes int64 = 1024 * 1024
|
|
22
22
|
const peerNodeHeader = "X-Pairling-Peer-Node"
|
|
23
23
|
|
|
24
|
+
// peerProvenanceHeader tells pairlingd how connectd identified the peer node:
|
|
25
|
+
// "tagged" (the old minted tag:pairling-phone path) or "interactive" (an
|
|
26
|
+
// untagged, user-owned Pairling iOS node admitted via the D2 sign-in path).
|
|
27
|
+
// connectd deletes any inbound copy before re-injecting the resolver's value, so
|
|
28
|
+
// a client can never forge it.
|
|
29
|
+
const peerProvenanceHeader = "X-Pairling-Peer-Provenance"
|
|
30
|
+
|
|
31
|
+
const (
|
|
32
|
+
provenanceTagged = "tagged"
|
|
33
|
+
provenanceInteractive = "interactive"
|
|
34
|
+
)
|
|
35
|
+
|
|
24
36
|
// funnelOriginHeader marks a request that arrived over the public Funnel
|
|
25
37
|
// listener. connectd sets it only on the funnel handler and deletes any inbound
|
|
26
38
|
// copy first; every other handler deletes it, so a client can never forge it.
|
|
@@ -50,7 +62,7 @@ type Logger interface {
|
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
type PeerNodeResolver interface {
|
|
53
|
-
PeerNodeID(ctx context.Context, remoteAddr string) (string, bool)
|
|
65
|
+
PeerNodeID(ctx context.Context, remoteAddr string) (nodeID string, provenance string, ok bool)
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
type Event struct {
|
|
@@ -222,14 +234,16 @@ func (h *Handler) rewrite(r *httputil.ProxyRequest) {
|
|
|
222
234
|
r.Out.Host = h.upstream.Host
|
|
223
235
|
r.Out.Header.Del("X-Forwarded-For")
|
|
224
236
|
r.Out.Header.Del(peerNodeHeader)
|
|
237
|
+
r.Out.Header.Del(peerProvenanceHeader)
|
|
225
238
|
r.Out.Header.Del(funnelOriginHeader)
|
|
226
239
|
if h.mode == ExposureModeFunnelBootstrap {
|
|
227
240
|
r.Out.Header.Set(funnelOriginHeader, "1")
|
|
228
241
|
}
|
|
229
242
|
if h.peerNodeResolver != nil {
|
|
230
|
-
if nodeID, ok := h.peerNodeResolver.PeerNodeID(in.Context(), in.RemoteAddr); ok {
|
|
243
|
+
if nodeID, provenance, ok := h.peerNodeResolver.PeerNodeID(in.Context(), in.RemoteAddr); ok {
|
|
231
244
|
if nodeID = strings.TrimSpace(nodeID); nodeID != "" {
|
|
232
245
|
r.Out.Header.Set(peerNodeHeader, nodeID)
|
|
246
|
+
r.Out.Header.Set(peerProvenanceHeader, provenance)
|
|
233
247
|
}
|
|
234
248
|
}
|
|
235
249
|
}
|
|
@@ -21,9 +21,9 @@ type recordingLogger struct {
|
|
|
21
21
|
events []Event
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
type peerNodeResolverFunc func(context.Context, string) (string, bool)
|
|
24
|
+
type peerNodeResolverFunc func(context.Context, string) (string, string, bool)
|
|
25
25
|
|
|
26
|
-
func (f peerNodeResolverFunc) PeerNodeID(ctx context.Context, remoteAddr string) (string, bool) {
|
|
26
|
+
func (f peerNodeResolverFunc) PeerNodeID(ctx context.Context, remoteAddr string) (string, string, bool) {
|
|
27
27
|
return f(ctx, remoteAddr)
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -451,9 +451,11 @@ func TestPairlingConnectStripsForgedPeerNodeHeader(t *testing.T) {
|
|
|
451
451
|
|
|
452
452
|
func TestPairlingConnectSetsPeerNodeHeaderFromResolver(t *testing.T) {
|
|
453
453
|
var forwardedHeader string
|
|
454
|
+
var forwardedProvenance string
|
|
454
455
|
var resolvedRemote string
|
|
455
456
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
456
457
|
forwardedHeader = r.Header.Get("X-Pairling-Peer-Node")
|
|
458
|
+
forwardedProvenance = r.Header.Get("X-Pairling-Peer-Provenance")
|
|
457
459
|
w.WriteHeader(http.StatusOK)
|
|
458
460
|
}))
|
|
459
461
|
defer upstream.Close()
|
|
@@ -465,9 +467,9 @@ func TestPairlingConnectSetsPeerNodeHeaderFromResolver(t *testing.T) {
|
|
|
465
467
|
Upstream: upstreamURL,
|
|
466
468
|
MaxBodyBytes: 1024,
|
|
467
469
|
Mode: ExposureModePairlingConnect,
|
|
468
|
-
PeerNodeResolver: peerNodeResolverFunc(func(_ context.Context, remoteAddr string) (string, bool) {
|
|
470
|
+
PeerNodeResolver: peerNodeResolverFunc(func(_ context.Context, remoteAddr string) (string, string, bool) {
|
|
469
471
|
resolvedRemote = remoteAddr
|
|
470
|
-
return "nPeerCNTRL", true
|
|
472
|
+
return "nPeerCNTRL", "tagged", true
|
|
471
473
|
}),
|
|
472
474
|
})
|
|
473
475
|
if err != nil {
|
|
@@ -489,6 +491,103 @@ func TestPairlingConnectSetsPeerNodeHeaderFromResolver(t *testing.T) {
|
|
|
489
491
|
if forwardedHeader != "nPeerCNTRL" {
|
|
490
492
|
t.Fatalf("peer-node header = %q, want trusted resolver value", forwardedHeader)
|
|
491
493
|
}
|
|
494
|
+
if forwardedProvenance != "tagged" {
|
|
495
|
+
t.Fatalf("peer-provenance header = %q, want tagged", forwardedProvenance)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// TestRewriteStripsForgedProvenanceAndReinjectsFromResolver proves the rewrite
|
|
500
|
+
// step deletes BOTH client-supplied X-Pairling-Peer-Node and
|
|
501
|
+
// X-Pairling-Peer-Provenance headers, then re-injects them from the resolver's
|
|
502
|
+
// trusted return values. A forged "tagged" provenance from the client must not
|
|
503
|
+
// survive when the resolver reports "interactive".
|
|
504
|
+
func TestRewriteStripsForgedProvenanceAndReinjectsFromResolver(t *testing.T) {
|
|
505
|
+
var forwardedHeader string
|
|
506
|
+
var forwardedProvenance string
|
|
507
|
+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
508
|
+
forwardedHeader = r.Header.Get("X-Pairling-Peer-Node")
|
|
509
|
+
forwardedProvenance = r.Header.Get("X-Pairling-Peer-Provenance")
|
|
510
|
+
w.WriteHeader(http.StatusOK)
|
|
511
|
+
}))
|
|
512
|
+
defer upstream.Close()
|
|
513
|
+
upstreamURL, err := url.Parse(upstream.URL)
|
|
514
|
+
if err != nil {
|
|
515
|
+
t.Fatal(err)
|
|
516
|
+
}
|
|
517
|
+
handler, err := NewHandler(Options{
|
|
518
|
+
Upstream: upstreamURL,
|
|
519
|
+
MaxBodyBytes: 1024,
|
|
520
|
+
Mode: ExposureModePairlingConnect,
|
|
521
|
+
PeerNodeResolver: peerNodeResolverFunc(func(_ context.Context, _ string) (string, string, bool) {
|
|
522
|
+
return "nResolverIOS", "interactive", true
|
|
523
|
+
}),
|
|
524
|
+
})
|
|
525
|
+
if err != nil {
|
|
526
|
+
t.Fatal(err)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
req := httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/send-text", strings.NewReader(`{}`))
|
|
530
|
+
req.RemoteAddr = "100.64.0.50:12345"
|
|
531
|
+
req.Header.Set("Authorization", "Bearer device-token")
|
|
532
|
+
req.Header.Set("X-Pairling-Peer-Node", "forged-node")
|
|
533
|
+
req.Header.Set("X-Pairling-Peer-Provenance", "tagged")
|
|
534
|
+
rec := httptest.NewRecorder()
|
|
535
|
+
handler.ServeHTTP(rec, req)
|
|
536
|
+
if rec.Code != http.StatusOK {
|
|
537
|
+
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
|
538
|
+
}
|
|
539
|
+
if forwardedHeader != "nResolverIOS" {
|
|
540
|
+
t.Fatalf("peer-node header = %q, want resolver value nResolverIOS", forwardedHeader)
|
|
541
|
+
}
|
|
542
|
+
if forwardedProvenance != "interactive" {
|
|
543
|
+
t.Fatalf("peer-provenance header = %q, want resolver value interactive", forwardedProvenance)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// TestRewriteInjectsNoHeadersWhenResolverRejects proves that when the resolver
|
|
548
|
+
// returns ok=false, NEITHER the peer-node NOR the peer-provenance header reaches
|
|
549
|
+
// the upstream, and any client-supplied copies are stripped.
|
|
550
|
+
func TestRewriteInjectsNoHeadersWhenResolverRejects(t *testing.T) {
|
|
551
|
+
var sawNode bool
|
|
552
|
+
var sawProvenance bool
|
|
553
|
+
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
554
|
+
_, sawNode = r.Header["X-Pairling-Peer-Node"]
|
|
555
|
+
_, sawProvenance = r.Header["X-Pairling-Peer-Provenance"]
|
|
556
|
+
w.WriteHeader(http.StatusOK)
|
|
557
|
+
}))
|
|
558
|
+
defer upstream.Close()
|
|
559
|
+
upstreamURL, err := url.Parse(upstream.URL)
|
|
560
|
+
if err != nil {
|
|
561
|
+
t.Fatal(err)
|
|
562
|
+
}
|
|
563
|
+
handler, err := NewHandler(Options{
|
|
564
|
+
Upstream: upstreamURL,
|
|
565
|
+
MaxBodyBytes: 1024,
|
|
566
|
+
Mode: ExposureModePairlingConnect,
|
|
567
|
+
PeerNodeResolver: peerNodeResolverFunc(func(_ context.Context, _ string) (string, string, bool) {
|
|
568
|
+
return "", "", false
|
|
569
|
+
}),
|
|
570
|
+
})
|
|
571
|
+
if err != nil {
|
|
572
|
+
t.Fatal(err)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
req := httptest.NewRequest(http.MethodPost, "http://pairling-connect.local/send-text", strings.NewReader(`{}`))
|
|
576
|
+
req.RemoteAddr = "100.64.0.50:12345"
|
|
577
|
+
req.Header.Set("Authorization", "Bearer device-token")
|
|
578
|
+
req.Header.Set("X-Pairling-Peer-Node", "forged-node")
|
|
579
|
+
req.Header.Set("X-Pairling-Peer-Provenance", "interactive")
|
|
580
|
+
rec := httptest.NewRecorder()
|
|
581
|
+
handler.ServeHTTP(rec, req)
|
|
582
|
+
if rec.Code != http.StatusOK {
|
|
583
|
+
t.Fatalf("status = %d body = %s", rec.Code, rec.Body.String())
|
|
584
|
+
}
|
|
585
|
+
if sawNode {
|
|
586
|
+
t.Fatal("peer-node header reached upstream when resolver rejected")
|
|
587
|
+
}
|
|
588
|
+
if sawProvenance {
|
|
589
|
+
t.Fatal("peer-provenance header reached upstream when resolver rejected")
|
|
590
|
+
}
|
|
492
591
|
}
|
|
493
592
|
|
|
494
593
|
func TestPairlingConnectModeMatchesEndpointContract(t *testing.T) {
|