pairling 0.2.4 → 0.2.6

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.
Files changed (29) hide show
  1. package/README.md +11 -9
  2. package/bin/pairling.mjs +5 -2
  3. package/package.json +7 -7
  4. package/payload/mac/SOURCE_BRANCH +1 -1
  5. package/payload/mac/SOURCE_REVISION +1 -1
  6. package/payload/mac/VERSION +1 -1
  7. package/payload/mac/companiond/pairling_connectd_status.py +57 -7
  8. package/payload/mac/companiond/pairling_devices.py +35 -0
  9. package/payload/mac/companiond/pairling_pairing.py +67 -20
  10. package/payload/mac/companiond/pairlingd.py +269 -16
  11. package/payload/mac/companiond/push_dispatcher.py +31 -1
  12. package/payload/mac/connectd/cmd/pairling-connectd/identity_test.go +65 -0
  13. package/payload/mac/connectd/cmd/pairling-connectd/main.go +150 -1
  14. package/payload/mac/connectd/cmd/pairling-connectd/peer_identity_test.go +86 -0
  15. package/payload/mac/connectd/cmd/pairling-tailnet-mintd/main.go +121 -0
  16. package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd.go +418 -0
  17. package/payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd_test.go +894 -0
  18. package/payload/mac/connectd/internal/gateway/adversarial_verify_test.go +99 -0
  19. package/payload/mac/connectd/internal/gateway/funnel_bootstrap_test.go +265 -0
  20. package/payload/mac/connectd/internal/gateway/funnel_contract_test.go +56 -0
  21. package/payload/mac/connectd/internal/gateway/proxy.go +233 -19
  22. package/payload/mac/connectd/internal/gateway/proxy_test.go +71 -0
  23. package/payload/mac/connectd/internal/runtime/config.go +19 -0
  24. package/payload/mac/connectd/internal/runtime/config_test.go +25 -0
  25. package/payload/mac/connectd/internal/status/status.go +67 -1
  26. package/payload/mac/connectd/internal/status/status_test.go +138 -0
  27. package/payload/mac/install/install-runtime.sh +421 -41
  28. package/payload/mac/install/render-launchd.py +54 -10
  29. package/payload-manifest.json +63 -21
@@ -8,6 +8,61 @@ import (
8
8
  "testing"
9
9
  )
10
10
 
11
+ func hasRouteKind(routes []AdvertisedRoute, kind string) bool {
12
+ for _, r := range routes {
13
+ if r.Kind == kind {
14
+ return true
15
+ }
16
+ }
17
+ return false
18
+ }
19
+
20
+ func TestFunnelRouteIsAdditiveAndGated(t *testing.T) {
21
+ s := NewStore("pairling-test")
22
+ s.SetListenerRunning(true)
23
+ s.SetUpstreamReachable(true)
24
+ s.SetTailnetIP("100.64.0.1")
25
+ s.SetAuthenticated()
26
+
27
+ // Funnel disabled: only the tailnet route, and no funnel_hostname in the JSON.
28
+ snap := s.Snapshot()
29
+ if hasRouteKind(snap.AdvertisedRoutes, RouteKindFunnel) {
30
+ t.Error("funnel route present while funnel disabled")
31
+ }
32
+ js, _ := json.Marshal(snap)
33
+ if strings.Contains(string(js), "funnel_hostname") {
34
+ t.Error("funnel_hostname must be omitted when empty")
35
+ }
36
+
37
+ // Enable funnel: the route appears with an https base URL and a lower priority.
38
+ s.SetFunnelHostname("pairling-abc.tail1234.ts.net")
39
+ snap = s.Snapshot()
40
+ var funnel, tailnet *AdvertisedRoute
41
+ for i := range snap.AdvertisedRoutes {
42
+ switch snap.AdvertisedRoutes[i].Kind {
43
+ case RouteKindFunnel:
44
+ funnel = &snap.AdvertisedRoutes[i]
45
+ case RouteKindTailnet:
46
+ tailnet = &snap.AdvertisedRoutes[i]
47
+ }
48
+ }
49
+ if funnel == nil || tailnet == nil {
50
+ t.Fatalf("expected tailnet and funnel routes, got %+v", snap.AdvertisedRoutes)
51
+ }
52
+ if funnel.BaseURL != "https://pairling-abc.tail1234.ts.net" {
53
+ t.Errorf("funnel base_url = %q", funnel.BaseURL)
54
+ }
55
+ if funnel.Priority >= tailnet.Priority {
56
+ t.Errorf("funnel priority %d must be below tailnet priority %d", funnel.Priority, tailnet.Priority)
57
+ }
58
+
59
+ // Unhealthy node: no routes advertise, even with a funnel hostname set.
60
+ s.SetUpstreamReachable(false)
61
+ if len(s.Snapshot().AdvertisedRoutes) != 0 {
62
+ t.Error("no routes should advertise when the node is unhealthy")
63
+ }
64
+ }
65
+
11
66
  func TestStoreServesHelperReadableSnapshotWithoutSecrets(t *testing.T) {
12
67
  store := NewStore("pairling-inst-abcdef")
13
68
  store.SetAuthPending("https://login.tailscale.com/a/secret-auth-token")
@@ -46,6 +101,89 @@ func TestStoreServesHelperReadableSnapshotWithoutSecrets(t *testing.T) {
46
101
  }
47
102
  }
48
103
 
104
+ func TestSnapshotSerializesTailnetIdentityFields(t *testing.T) {
105
+ store := NewStore("pairling-inst-abcdef")
106
+ store.SetTailnetIdentity("nXb6CNTRL", []string{"tag:pairling-connect"}, []string{"100.79.217.7"})
107
+
108
+ raw, err := json.Marshal(store.Snapshot())
109
+ if err != nil {
110
+ t.Fatal(err)
111
+ }
112
+ body := string(raw)
113
+ for _, want := range []string{
114
+ `"tailnet_node_id":"nXb6CNTRL"`,
115
+ `"tags":["tag:pairling-connect"]`,
116
+ `"tailnet_ips":["100.79.217.7"]`,
117
+ } {
118
+ if !strings.Contains(body, want) {
119
+ t.Fatalf("snapshot missing %s: %s", want, body)
120
+ }
121
+ }
122
+ }
123
+
124
+ func TestSnapshotSerializesKnownTailnetLockStatus(t *testing.T) {
125
+ store := NewStore("pairling-inst-abcdef")
126
+ store.SetTailnetLockEnabled(false)
127
+
128
+ raw, err := json.Marshal(store.Snapshot())
129
+ if err != nil {
130
+ t.Fatal(err)
131
+ }
132
+ body := string(raw)
133
+ if !strings.Contains(body, `"tailnet_lock_enabled":false`) {
134
+ t.Fatalf("snapshot missing known lock status: %s", body)
135
+ }
136
+ }
137
+
138
+ func TestSnapshotOmitsTailnetIdentityWhenUnknown(t *testing.T) {
139
+ raw, err := json.Marshal(NewStore("pairling-inst-abcdef").Snapshot())
140
+ if err != nil {
141
+ t.Fatal(err)
142
+ }
143
+ body := string(raw)
144
+ for _, forbidden := range []string{"tailnet_node_id", "tags", "tailnet_ips"} {
145
+ if strings.Contains(body, forbidden) {
146
+ t.Fatalf("fresh snapshot should omit %q: %s", forbidden, body)
147
+ }
148
+ }
149
+ if !strings.Contains(body, `"auth_state":"starting"`) {
150
+ t.Fatalf("fresh snapshot lost coherent auth state: %s", body)
151
+ }
152
+ }
153
+
154
+ func TestSetTailnetIdentityIsIdempotentAndReplaces(t *testing.T) {
155
+ store := NewStore("pairling-inst-abcdef")
156
+ store.SetTailnetIdentity("old-node", []string{"tag:old"}, []string{"100.64.0.1"})
157
+ store.SetTailnetIdentity("new-node", []string{"tag:pairling-connect"}, []string{"100.79.217.7", "fd7a:115c:a1e0::1"})
158
+
159
+ snapshot := store.Snapshot()
160
+ if snapshot.TailnetNodeID != "new-node" {
161
+ t.Fatalf("node ID = %q, want new-node", snapshot.TailnetNodeID)
162
+ }
163
+ if got, want := strings.Join(snapshot.Tags, ","), "tag:pairling-connect"; got != want {
164
+ t.Fatalf("tags = %q, want %q", got, want)
165
+ }
166
+ if got, want := strings.Join(snapshot.TailnetIPs, ","), "100.79.217.7,fd7a:115c:a1e0::1"; got != want {
167
+ t.Fatalf("tailnet IPs = %q, want %q", got, want)
168
+ }
169
+ }
170
+
171
+ func TestTailnetIdentityCarriesNoSecrets(t *testing.T) {
172
+ store := NewStore("pairling-inst-abcdef")
173
+ store.SetTailnetIdentity("tskey-auth-example", []string{"AuthKey=bad"}, []string{"NLPrivate=bad"})
174
+
175
+ raw, err := json.Marshal(store.Snapshot())
176
+ if err != nil {
177
+ t.Fatal(err)
178
+ }
179
+ body := string(raw)
180
+ for _, forbidden := range []string{"tskey", "authkey", "AuthKey", "NLPrivate"} {
181
+ if strings.Contains(body, forbidden) {
182
+ t.Fatalf("tailnet identity leaked %q: %s", forbidden, body)
183
+ }
184
+ }
185
+ }
186
+
49
187
  func TestStoreAdvertisesRouteOnlyWhenReady(t *testing.T) {
50
188
  store := NewStore("pairling-inst-abcdef")
51
189
  store.SetAuthPending("open https://login.tailscale.com/a/example-token")