pairling 0.2.6 → 0.2.8

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.
@@ -11,6 +11,11 @@ USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_DAEMON_LABEL.plist"
11
11
  CONNECTD_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_CONNECTD_LABEL.plist"
12
12
  PTYBROKER_USER_PLIST="$HOME/Library/LaunchAgents/$PAIRLING_PTYBROKER_LABEL.plist"
13
13
  SYSTEM_PLIST="/Library/LaunchDaemons/$PAIRLING_GUARDIAN_LABEL.plist"
14
+ # Legacy: the silent-join mint broker, removed from the product. Torn down below.
15
+ MINTD_SYSTEM_LABEL="dev.pairling.mintd"
16
+ MINTD_SYSTEM_PLIST="/Library/LaunchDaemons/$MINTD_SYSTEM_LABEL.plist"
17
+ MINTD_SYSTEM_DIR="/Library/Application Support/Pairling/mint"
18
+ MINTD_SERVICE_ACCOUNT="_pairling_mint"
14
19
  YES="false"
15
20
  DELETE_STATE="false"
16
21
  DELETE_LOGS="false"
@@ -98,6 +103,32 @@ bootout_system() {
98
103
  fi
99
104
  }
100
105
 
106
+ # Legacy teardown: the silent-join mint broker (dev.pairling.mintd) was removed
107
+ # from the product. Machines that ran the old `enable-silent-join` still carry a
108
+ # root LaunchDaemon, a stored Tailscale OAuth secret under the system mint dir,
109
+ # and the _pairling_mint role account. Remove all three. Best-effort, sudo-gated.
110
+ teardown_legacy_mintd() {
111
+ if [[ ! -f "$MINTD_SYSTEM_PLIST" && ! -d "$MINTD_SYSTEM_DIR" ]] \
112
+ && ! id -u "$MINTD_SERVICE_ACCOUNT" >/dev/null 2>&1; then
113
+ return
114
+ fi
115
+ if is_dry_run; then
116
+ printf 'dry-run: would remove the legacy silent-join mint broker (%s, %s, user %s)\n' \
117
+ "$MINTD_SYSTEM_PLIST" "$MINTD_SYSTEM_DIR" "$MINTD_SERVICE_ACCOUNT"
118
+ return
119
+ fi
120
+ if sudo -n true >/dev/null 2>&1; then
121
+ sudo launchctl bootout "system/$MINTD_SYSTEM_LABEL" >/dev/null 2>&1 || true
122
+ sudo launchctl bootout system "$MINTD_SYSTEM_PLIST" >/dev/null 2>&1 || true
123
+ sudo rm -f "$MINTD_SYSTEM_PLIST"
124
+ sudo rm -rf "$MINTD_SYSTEM_DIR"
125
+ sudo /usr/sbin/sysadminctl -deleteUser "$MINTD_SERVICE_ACCOUNT" >/dev/null 2>&1 || true
126
+ printf 'Removed the legacy silent-join mint broker.\n'
127
+ else
128
+ printf 'Skipping legacy mint-broker removal: passwordless sudo is unavailable.\n' >&2
129
+ fi
130
+ }
131
+
101
132
  confirm
102
133
 
103
134
  bootout_user "$PAIRLING_DAEMON_LABEL" "$USER_PLIST"
@@ -107,6 +138,7 @@ rm -f "$USER_PLIST"
107
138
  rm -f "$CONNECTD_USER_PLIST"
108
139
  rm -f "$PTYBROKER_USER_PLIST"
109
140
  bootout_system "$PAIRLING_GUARDIAN_LABEL" "$SYSTEM_PLIST"
141
+ teardown_legacy_mintd
110
142
 
111
143
  rm -rf "$APP_SUPPORT/pair" 2>/dev/null || true
112
144
 
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "connectd": {
3
3
  "darwin-arm64": {
4
- "sha256": "d6d23a4cf72a2c44cde10adfd58d58dc1a29226da10e0bfa7c147b2c0e9bd1a7",
4
+ "sha256": "d4d9439eae6cfa244b491b5086050f932fe78058e5ac18a876b80b20ea8517ad",
5
5
  "team_id": "965AVD34A3"
6
6
  },
7
7
  "darwin-x64": {
8
- "sha256": "d1f7cfb6fcb1a9542ac4852ef41610a8866e010aff299b86a349b06bcc5ec465",
8
+ "sha256": "936c7046bfc3cd84289881444da57ad025d5c734c44a01c2dcbd9eb2afc7cbb8",
9
9
  "team_id": "965AVD34A3"
10
10
  }
11
11
  },
12
12
  "files": [
13
13
  {
14
14
  "path": "payload/mac/SOURCE_BRANCH",
15
- "sha256": "b7cda4715ebe6bb92ec69b85df0e99668564bde99f03d6e04109b399538000b2"
15
+ "sha256": "34d6a94dacb895403529caac12a19aed745c6caca7a8d0f4ed631999044f76e8"
16
16
  },
17
17
  {
18
18
  "path": "payload/mac/SOURCE_DIRTY",
@@ -20,11 +20,11 @@
20
20
  },
21
21
  {
22
22
  "path": "payload/mac/SOURCE_REVISION",
23
- "sha256": "1746356edd4da4f8f46f7c3481696eb0481563f5d9d24e67be7545fdc4fb4b4b"
23
+ "sha256": "354ac93fcb550a4bf353e3857282bafb747f158ce1ad6e7068a5d85c5cd071bd"
24
24
  },
25
25
  {
26
26
  "path": "payload/mac/VERSION",
27
- "sha256": "be3c6d2c6c406a64d44f0b6464a887e290416dd90c524094485b1be00936d6d7"
27
+ "sha256": "283571d2642fc7b3befd294f45d53b896a1d54d34c86235b13151192b380606a"
28
28
  },
29
29
  {
30
30
  "path": "payload/mac/companiond/app_attest_lan.py",
@@ -96,7 +96,7 @@
96
96
  },
97
97
  {
98
98
  "path": "payload/mac/companiond/pairlingd.py",
99
- "sha256": "ee2a9115531a15f9cc3aff0680be3bb7ab7b42c7cf001eac05b33cbf36755842"
99
+ "sha256": "e69188ec651d0a04af5cad73c9dd0a81dfa6455b6e32f8b733c412d11a4ead9e"
100
100
  },
101
101
  {
102
102
  "path": "payload/mac/companiond/providers/__init__.py",
@@ -210,18 +210,6 @@
210
210
  "path": "payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go",
211
211
  "sha256": "dc5b6d3a8d11f38bcc198287bdbc95f058d35792ca6cf34bc49ca0bed22bfacf"
212
212
  },
213
- {
214
- "path": "payload/mac/connectd/cmd/pairling-tailnet-mintd/main.go",
215
- "sha256": "64fd4527c97397fce10793001af8d59ac3154c75c7ed9e1b532f8f2e4bf88bd4"
216
- },
217
- {
218
- "path": "payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd.go",
219
- "sha256": "ca49b8ab8216eeec770ad6e9e8111aca819a9a88b050d7eefbdf69c0fad20376"
220
- },
221
- {
222
- "path": "payload/mac/connectd/cmd/pairling-tailnet-mintd/mintd_test.go",
223
- "sha256": "14a9af6b9575ba89314ce05f2fe465844fea3bb40b3eba5e0d33d059608bfb1f"
224
- },
225
213
  {
226
214
  "path": "payload/mac/connectd/go.mod",
227
215
  "sha256": "c96748d396598b0952b4c0d43f7f85ca3a56f4019761088267421b22518d5905"
@@ -284,7 +272,7 @@
284
272
  },
285
273
  {
286
274
  "path": "payload/mac/install/install-runtime.sh",
287
- "sha256": "152eed164fd4edfd42874f20e8476d8e42415d5e93e090b716fef2ea7ed6424e"
275
+ "sha256": "40ffa67a3833ce4342c241ca4f1e7dec471a86b0c80359746cd75a8df2e33f60"
288
276
  },
289
277
  {
290
278
  "path": "payload/mac/install/psk_dependency_check.py",
@@ -292,11 +280,11 @@
292
280
  },
293
281
  {
294
282
  "path": "payload/mac/install/render-launchd.py",
295
- "sha256": "5c4c06b578e24726731650ff39d688248ba1bd412b9e21fc368b13d07e8ee0df"
283
+ "sha256": "29db1aa46d62cd23ba27ab283bb990d168bfc08fb82cfc681046dc7b887d85fe"
296
284
  },
297
285
  {
298
286
  "path": "payload/mac/install/uninstall-runtime.sh",
299
- "sha256": "8af7b3bb4de4b053888fe44a9fdb576144d6cb154bafc9b3d02edac90a7055ec"
287
+ "sha256": "0edeb5336a71c235d8796a8e59072c6d8983d196de5aae0cfcb4b0c3560fa33f"
300
288
  },
301
289
  {
302
290
  "path": "payload/mac/mcp/phone_tools.py",
@@ -307,19 +295,9 @@
307
295
  "sha256": "5ebcd63fc53114ace518807c2221e562e65237e57945a76c457f5931a5791cc1"
308
296
  }
309
297
  ],
310
- "mintd": {
311
- "darwin-arm64": {
312
- "sha256": "08b815ae499550ffe2634315a7e8c2233a6a557f75d51476716da00c082e42d6",
313
- "team_id": "965AVD34A3"
314
- },
315
- "darwin-x64": {
316
- "sha256": "3acd15acb4474793aa9862f3f4a7957570621e3034803a9991fe8445426b7301",
317
- "team_id": "965AVD34A3"
318
- }
319
- },
320
298
  "package": "pairling",
321
- "package_version": "0.2.6",
299
+ "package_version": "0.2.8",
322
300
  "schema_version": 1,
323
301
  "source_dirty": false,
324
- "source_revision": "7f2b4a22"
302
+ "source_revision": "c07a6fd"
325
303
  }
@@ -1,121 +0,0 @@
1
- package main
2
-
3
- import (
4
- "context"
5
- "encoding/json"
6
- "errors"
7
- "flag"
8
- "fmt"
9
- "log"
10
- "net/http"
11
- "os"
12
- "os/exec"
13
- "os/signal"
14
- "strconv"
15
- "strings"
16
- "syscall"
17
- "time"
18
- )
19
-
20
- func main() {
21
- var (
22
- secretPath = flag.String("secret-path", "/Library/Application Support/Pairling/mint/client_secret.json", "OAuth client credential JSON")
23
- socketPath = flag.String("socket-path", "/Library/Application Support/Pairling/run/mintd/mintd.sock", "Unix socket path")
24
- statePath = flag.String("state-path", "/Library/Application Support/Pairling/mint/state.json", "persistent rate-limit state JSON")
25
- auditPath = flag.String("audit-path", "/Library/Application Support/Pairling/mint/audit.jsonl", "audit JSONL path")
26
- alertPath = flag.String("alert-path", "/Library/Application Support/Pairling/run/mintd/alerts.jsonl", "health-readable alert JSONL path")
27
- apiBaseURL = flag.String("api-base-url", "https://api.tailscale.com/api/v2", "Tailscale API base URL")
28
- oauthURL = flag.String("oauth-url", "https://api.tailscale.com/api/v2/oauth/token", "Tailscale OAuth token URL")
29
- authorizedUID = flag.Int("authorized-uid", -1, "only this peer uid may request mints")
30
- )
31
- flag.Parse()
32
-
33
- b, err := NewBroker(BrokerConfig{
34
- SecretPath: *secretPath,
35
- StatePath: *statePath,
36
- AuditPath: *auditPath,
37
- AlertPath: *alertPath,
38
- OAuthURL: *oauthURL,
39
- APIBaseURL: *apiBaseURL,
40
- LockStatus: defaultLockStatus,
41
- })
42
- if err != nil {
43
- log.Fatal(err)
44
- }
45
- ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
46
- defer stop()
47
- if err := b.ServeUnix(ctx, *socketPath, *authorizedUID); err != nil {
48
- log.Fatal(err)
49
- }
50
- }
51
-
52
- func defaultLockStatus(ctx context.Context) (bool, error) {
53
- if locked, err := lockStatusFromConnectdStatus(ctx, "http://127.0.0.1:7774/status"); err == nil {
54
- return locked, nil
55
- }
56
- return lockStatusFromCandidates(ctx, []string{
57
- "/opt/homebrew/bin/tailscale",
58
- "/Applications/Tailscale.app/Contents/MacOS/Tailscale",
59
- "tailscale",
60
- })
61
- }
62
-
63
- func lockStatusFromConnectdStatus(ctx context.Context, statusURL string) (bool, error) {
64
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, statusURL, nil)
65
- if err != nil {
66
- return false, err
67
- }
68
- resp, err := (&http.Client{Timeout: 2 * time.Second}).Do(req)
69
- if err != nil {
70
- return false, err
71
- }
72
- defer resp.Body.Close()
73
- if resp.StatusCode != http.StatusOK {
74
- return false, fmt.Errorf("connectd status returned %s", resp.Status)
75
- }
76
- var body struct {
77
- TailnetLockEnabled *bool `json:"tailnet_lock_enabled"`
78
- }
79
- if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
80
- return false, err
81
- }
82
- if body.TailnetLockEnabled == nil {
83
- return false, errors.New("connectd status omitted tailnet_lock_enabled")
84
- }
85
- return *body.TailnetLockEnabled, nil
86
- }
87
-
88
- func lockStatusFromCandidates(ctx context.Context, candidates []string) (bool, error) {
89
- var errs []error
90
- for _, bin := range candidates {
91
- out, err := exec.CommandContext(ctx, bin, "lock", "status").CombinedOutput()
92
- if err != nil {
93
- errs = append(errs, fmt.Errorf("%s: %w", bin, err))
94
- continue
95
- }
96
- text := strings.ToLower(string(out))
97
- if strings.Contains(text, "tailscale gui failed to start") {
98
- errs = append(errs, fmt.Errorf("%s: gui unavailable", bin))
99
- continue
100
- }
101
- if strings.Contains(text, "not enabled") || strings.Contains(text, "disabled") {
102
- return false, nil
103
- }
104
- if strings.Contains(text, "enabled") {
105
- return true, nil
106
- }
107
- return false, fmt.Errorf("unrecognized tailscale lock status: %q", strings.TrimSpace(string(out)))
108
- }
109
- return false, errors.New("tailscale lock status unavailable: " + joinErrors(errs))
110
- }
111
-
112
- func joinErrors(errs []error) string {
113
- parts := make([]string, 0, len(errs))
114
- for _, err := range errs {
115
- parts = append(parts, err.Error())
116
- }
117
- if len(parts) == 0 {
118
- return "no candidates"
119
- }
120
- return strconv.Quote(strings.Join(parts, "; "))
121
- }
@@ -1,418 +0,0 @@
1
- package main
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- "encoding/json"
7
- "fmt"
8
- "net"
9
- "net/http"
10
- "os"
11
- "path/filepath"
12
- "regexp"
13
- "strconv"
14
- "sync"
15
- "time"
16
-
17
- "github.com/tailscale/peercred"
18
- "golang.org/x/oauth2"
19
- "golang.org/x/oauth2/clientcredentials"
20
- )
21
-
22
- const phoneTag = "tag:pairling-phone"
23
-
24
- var pairIDPattern = regexp.MustCompile(`^pair_[A-Za-z0-9][A-Za-z0-9_-]{0,127}$`)
25
-
26
- var peerUID = func(conn net.Conn) (string, error) {
27
- creds, err := peercred.Get(conn)
28
- if err != nil {
29
- return "", err
30
- }
31
- uid, ok := creds.UserID()
32
- if !ok {
33
- return "", fmt.Errorf("peer uid unavailable")
34
- }
35
- return uid, nil
36
- }
37
-
38
- type BrokerConfig struct {
39
- SecretPath string
40
- StatePath string
41
- AuditPath string
42
- AlertPath string
43
- OAuthURL string
44
- APIBaseURL string
45
- Now func() time.Time
46
- LockStatus func(context.Context) (bool, error)
47
- HTTPClient *http.Client
48
- }
49
-
50
- type Broker struct {
51
- cfg BrokerConfig
52
- mu sync.Mutex
53
- tokenSource oauth2.TokenSource
54
- }
55
-
56
- type clientSecret struct {
57
- ClientID string `json:"client_id"`
58
- ClientSecret string `json:"client_secret"`
59
- Scopes []string `json:"scopes,omitempty"`
60
- Tags []string `json:"tags,omitempty"`
61
- }
62
-
63
- type MintResult struct {
64
- AuthKey string `json:"authkey"`
65
- KeyID string `json:"key_id"`
66
- ExpiresAt int64 `json:"expires_at"`
67
- }
68
-
69
- type socketRequest struct {
70
- Op string `json:"op"`
71
- PairID string `json:"pair_id"`
72
- }
73
-
74
- type socketResponse struct {
75
- OK bool `json:"ok"`
76
- AuthKey string `json:"authkey,omitempty"`
77
- KeyID string `json:"key_id,omitempty"`
78
- ExpiresAt int64 `json:"expires_at,omitempty"`
79
- Error string `json:"error,omitempty"`
80
- }
81
-
82
- type brokerState struct {
83
- SuccessfulPairs map[string]int64 `json:"successful_pairs,omitempty"`
84
- SuccessfulMints []int64 `json:"successful_mints,omitempty"`
85
- }
86
-
87
- func NewBroker(cfg BrokerConfig) (*Broker, error) {
88
- if cfg.OAuthURL == "" {
89
- cfg.OAuthURL = "https://api.tailscale.com/api/v2/oauth/token"
90
- }
91
- if cfg.APIBaseURL == "" {
92
- cfg.APIBaseURL = "https://api.tailscale.com/api/v2"
93
- }
94
- if cfg.Now == nil {
95
- cfg.Now = time.Now
96
- }
97
- if cfg.HTTPClient == nil {
98
- cfg.HTTPClient = http.DefaultClient
99
- }
100
- return &Broker{cfg: cfg}, nil
101
- }
102
-
103
- func writeClientSecret(path string, secret clientSecret) error {
104
- data, err := json.MarshalIndent(secret, "", " ")
105
- if err != nil {
106
- return err
107
- }
108
- data = append(data, '\n')
109
- return os.WriteFile(path, data, 0o600)
110
- }
111
-
112
- func readClientSecret(path string) (clientSecret, error) {
113
- data, err := os.ReadFile(path)
114
- if err != nil {
115
- return clientSecret{}, err
116
- }
117
- var secret clientSecret
118
- if err := json.Unmarshal(data, &secret); err != nil {
119
- return clientSecret{}, err
120
- }
121
- if secret.ClientID == "" || secret.ClientSecret == "" {
122
- return clientSecret{}, fmt.Errorf("missing client_id or client_secret")
123
- }
124
- return secret, nil
125
- }
126
-
127
- func (b *Broker) MintPhoneKey(ctx context.Context, pairID string) (MintResult, error) {
128
- if !pairIDPattern.MatchString(pairID) {
129
- return MintResult{}, fmt.Errorf("invalid pair_id")
130
- }
131
- b.mu.Lock()
132
- defer b.mu.Unlock()
133
- state, err := b.loadState()
134
- if err != nil {
135
- return MintResult{}, err
136
- }
137
- now := b.cfg.Now().Unix()
138
- if state.SuccessfulPairs[pairID] != 0 {
139
- _ = b.alert(map[string]any{
140
- "event": "duplicate_pair_id",
141
- "ts": now,
142
- "pair_id": pairID,
143
- })
144
- return MintResult{}, fmt.Errorf("pair_id already minted")
145
- }
146
- state.SuccessfulMints = pruneSince(state.SuccessfulMints, now-24*60*60)
147
- if countSince(state.SuccessfulMints, now-10*60) >= 3 {
148
- _ = b.alert(map[string]any{
149
- "event": "mint_rate_limited",
150
- "ts": now,
151
- "pair_id": pairID,
152
- "window": "10m",
153
- })
154
- return MintResult{}, fmt.Errorf("mint rate limited")
155
- }
156
- if len(state.SuccessfulMints) >= 12 {
157
- _ = b.alert(map[string]any{
158
- "event": "mint_rate_limited",
159
- "ts": now,
160
- "pair_id": pairID,
161
- "window": "24h",
162
- })
163
- return MintResult{}, fmt.Errorf("mint rate limited")
164
- }
165
- locked, err := b.lockEnabled(ctx)
166
- if err != nil {
167
- return MintResult{}, err
168
- }
169
- if locked {
170
- return MintResult{}, fmt.Errorf("tailnet lock enabled; unsigned minting is disabled")
171
- }
172
- secret, err := readClientSecret(b.cfg.SecretPath)
173
- if err != nil {
174
- return MintResult{}, err
175
- }
176
- token, err := b.token(ctx, secret)
177
- if err != nil {
178
- return MintResult{}, err
179
- }
180
- body := map[string]any{
181
- "capabilities": map[string]any{
182
- "devices": map[string]any{
183
- "create": map[string]any{
184
- "reusable": false,
185
- "ephemeral": false,
186
- "preauthorized": true,
187
- "tags": []string{phoneTag},
188
- },
189
- },
190
- },
191
- "expirySeconds": 600,
192
- }
193
- payload, err := json.Marshal(body)
194
- if err != nil {
195
- return MintResult{}, err
196
- }
197
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.cfg.APIBaseURL+"/tailnet/-/keys", bytes.NewReader(payload))
198
- if err != nil {
199
- return MintResult{}, err
200
- }
201
- req.Header.Set("Authorization", "Bearer "+token.AccessToken)
202
- req.Header.Set("Content-Type", "application/json")
203
- resp, err := b.cfg.HTTPClient.Do(req)
204
- if err != nil {
205
- return MintResult{}, err
206
- }
207
- defer resp.Body.Close()
208
- if resp.StatusCode != http.StatusOK {
209
- return MintResult{}, fmt.Errorf("tailscale key mint failed: %s", resp.Status)
210
- }
211
- var out struct {
212
- ID string `json:"id"`
213
- Key string `json:"key"`
214
- Expires string `json:"expires"`
215
- }
216
- if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
217
- return MintResult{}, err
218
- }
219
- exp, _ := time.Parse(time.RFC3339, out.Expires)
220
- result := MintResult{AuthKey: out.Key, KeyID: out.ID, ExpiresAt: exp.Unix()}
221
- state.SuccessfulPairs[pairID] = now
222
- state.SuccessfulMints = append(state.SuccessfulMints, now)
223
- if err := b.saveState(state); err != nil {
224
- return MintResult{}, err
225
- }
226
- if err := b.audit(map[string]any{
227
- "event": "mint_success",
228
- "ts": now,
229
- "pair_id": pairID,
230
- "key_id": out.ID,
231
- }); err != nil {
232
- return MintResult{}, err
233
- }
234
- return result, nil
235
- }
236
-
237
- func pruneSince(times []int64, cutoff int64) []int64 {
238
- kept := times[:0]
239
- for _, ts := range times {
240
- if ts >= cutoff {
241
- kept = append(kept, ts)
242
- }
243
- }
244
- return kept
245
- }
246
-
247
- func countSince(times []int64, cutoff int64) int {
248
- count := 0
249
- for _, ts := range times {
250
- if ts >= cutoff {
251
- count++
252
- }
253
- }
254
- return count
255
- }
256
-
257
- func (b *Broker) token(ctx context.Context, secret clientSecret) (*oauth2.Token, error) {
258
- if b.tokenSource == nil {
259
- oauth := clientcredentials.Config{
260
- ClientID: secret.ClientID,
261
- ClientSecret: secret.ClientSecret,
262
- TokenURL: b.cfg.OAuthURL,
263
- Scopes: []string{"auth_keys"},
264
- }
265
- ctx = context.WithValue(ctx, oauth2.HTTPClient, b.cfg.HTTPClient)
266
- b.tokenSource = oauth2.ReuseTokenSource(nil, oauth.TokenSource(ctx))
267
- }
268
- return b.tokenSource.Token()
269
- }
270
-
271
- func (b *Broker) lockEnabled(ctx context.Context) (bool, error) {
272
- if b.cfg.LockStatus == nil {
273
- return false, nil
274
- }
275
- return b.cfg.LockStatus(ctx)
276
- }
277
-
278
- func (b *Broker) ServeUnix(ctx context.Context, socketPath string, authorizedUID int) error {
279
- if authorizedUID < 0 {
280
- return fmt.Errorf("authorized uid required")
281
- }
282
- if err := os.MkdirAll(filepath.Dir(socketPath), 0o750); err != nil {
283
- return err
284
- }
285
- _ = os.Remove(socketPath)
286
- l, err := net.Listen("unix", socketPath)
287
- if err != nil {
288
- return err
289
- }
290
- defer func() {
291
- _ = l.Close()
292
- _ = os.Remove(socketPath)
293
- }()
294
- _ = os.Chmod(socketPath, 0o660)
295
- go func() {
296
- <-ctx.Done()
297
- _ = l.Close()
298
- }()
299
- for {
300
- conn, err := l.Accept()
301
- if err != nil {
302
- if ctx.Err() != nil {
303
- return nil
304
- }
305
- return err
306
- }
307
- go b.handleConn(ctx, conn, authorizedUID)
308
- }
309
- }
310
-
311
- func (b *Broker) handleConn(ctx context.Context, conn net.Conn, authorizedUID int) {
312
- defer conn.Close()
313
- uid, err := peerUID(conn)
314
- if err != nil {
315
- _ = json.NewEncoder(conn).Encode(socketResponse{OK: false, Error: "peercred_unavailable"})
316
- return
317
- }
318
- if err := authorizePeer(uid, authorizedUID); err != nil {
319
- _ = b.alert(map[string]any{"event": "unexpected_peer_uid", "ts": b.cfg.Now().Unix(), "uid": uid})
320
- _ = json.NewEncoder(conn).Encode(socketResponse{OK: false, Error: "unauthorized_peer"})
321
- return
322
- }
323
- var req socketRequest
324
- if err := json.NewDecoder(conn).Decode(&req); err != nil {
325
- _ = json.NewEncoder(conn).Encode(socketResponse{OK: false, Error: "bad_json"})
326
- return
327
- }
328
- if req.Op != "mint_phone_key" || req.PairID == "" {
329
- _ = json.NewEncoder(conn).Encode(socketResponse{OK: false, Error: "bad_request"})
330
- return
331
- }
332
- res, err := b.MintPhoneKey(ctx, req.PairID)
333
- if err != nil {
334
- _ = json.NewEncoder(conn).Encode(socketResponse{OK: false, Error: err.Error()})
335
- return
336
- }
337
- _ = json.NewEncoder(conn).Encode(socketResponse{
338
- OK: true,
339
- AuthKey: res.AuthKey,
340
- KeyID: res.KeyID,
341
- ExpiresAt: res.ExpiresAt,
342
- })
343
- }
344
-
345
- func authorizePeer(uid string, authorizedUID int) error {
346
- if uid != strconv.Itoa(authorizedUID) {
347
- return fmt.Errorf("unexpected peer uid")
348
- }
349
- return nil
350
- }
351
-
352
- func (b *Broker) loadState() (brokerState, error) {
353
- state := brokerState{SuccessfulPairs: map[string]int64{}}
354
- if b.cfg.StatePath == "" {
355
- return state, nil
356
- }
357
- data, err := os.ReadFile(b.cfg.StatePath)
358
- if os.IsNotExist(err) {
359
- return state, nil
360
- }
361
- if err != nil {
362
- return state, err
363
- }
364
- if err := json.Unmarshal(data, &state); err != nil {
365
- return state, err
366
- }
367
- if state.SuccessfulPairs == nil {
368
- state.SuccessfulPairs = map[string]int64{}
369
- }
370
- return state, nil
371
- }
372
-
373
- func (b *Broker) saveState(state brokerState) error {
374
- if b.cfg.StatePath == "" {
375
- return nil
376
- }
377
- if err := os.MkdirAll(filepath.Dir(b.cfg.StatePath), 0o700); err != nil {
378
- return err
379
- }
380
- data, err := json.MarshalIndent(state, "", " ")
381
- if err != nil {
382
- return err
383
- }
384
- data = append(data, '\n')
385
- return os.WriteFile(b.cfg.StatePath, data, 0o600)
386
- }
387
-
388
- func (b *Broker) audit(record map[string]any) error {
389
- return appendJSONL(b.cfg.AuditPath, record, 0o700, 0o600)
390
- }
391
-
392
- func (b *Broker) alert(record map[string]any) error {
393
- if err := b.audit(record); err != nil {
394
- return err
395
- }
396
- return appendJSONL(b.cfg.AlertPath, record, 0o750, 0o640)
397
- }
398
-
399
- func appendJSONL(path string, record map[string]any, dirMode, fileMode os.FileMode) error {
400
- if path == "" {
401
- return nil
402
- }
403
- if err := os.MkdirAll(filepath.Dir(path), dirMode); err != nil {
404
- return err
405
- }
406
- data, err := json.Marshal(record)
407
- if err != nil {
408
- return err
409
- }
410
- data = append(data, '\n')
411
- f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, fileMode)
412
- if err != nil {
413
- return err
414
- }
415
- defer f.Close()
416
- _, err = f.Write(data)
417
- return err
418
- }