detached ee07621

refactor(dgram): persist proxy key fingerprints

commit
ee07621c1787c680979376ea0725dd5fe7937edf
author
brice · · 2026-07-23 20:15 UTC
parent
cb1999a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +64 -20
M internal/adapters/dgram/proxy_registry.go +11 -8
@@ -1,10 +1,10 @@
11 package dgram
22
33 import (
4+ "crypto/sha256"
45 "encoding/base64"
56 "encoding/json"
67 "errors"
7- "fmt"
88 "os"
99 "path/filepath"
1010 "syscall"
@@ -14,14 +14,17 @@ import (
1414 )
1515
1616 type ProxyRecord struct {
17- Session string `json:"session"`
18- PID int `json:"pid"`
19- Port int `json:"port"`
20- Key string `json:"key"`
21- Created time.Time `json:"created"`
17+ Session string `json:"session"`
18+ PID int `json:"pid"`
19+ Port int `json:"port"`
20+ KeyFingerprint string `json:"key_fingerprint"`
21+ Created time.Time `json:"created"`
2222 }
2323
24-func (r ProxyRecord) ReadyLine() string { return fmt.Sprintf("VEV-UDP %d %s\n", r.Port, r.Key) }
24+func KeyFingerprint(key []byte) string {
25+ sum := sha256.Sum256(key)
26+ return base64.StdEncoding.EncodeToString(sum[:])
27+}
2528
2629 type ProxyRegistry struct{ dir string }
2730
@@ -88,7 +91,7 @@ func (r *ProxyRegistry) RemoveOwned(rec ProxyRecord) error {
8891 if err != nil {
8992 return err
9093 }
91- if cur.PID != rec.PID || cur.Port != rec.Port || cur.Key != rec.Key {
94+ if cur.PID != rec.PID || cur.Port != rec.Port || cur.KeyFingerprint != rec.KeyFingerprint {
9295 return nil
9396 }
9497 return r.removeLocked(rec.Session)
@@ -1,10 +1,10 @@
1 package dgram1 package dgram
2 2
3 import (3 import (
4+ "crypto/sha256"
4 "encoding/base64"5 "encoding/base64"
5 "encoding/json"6 "encoding/json"
6 "errors"7 "errors"
7- "fmt"
8 "os"8 "os"
9 "path/filepath"9 "path/filepath"
10 "syscall"10 "syscall"
@@ -14,14 +14,17 @@ import (
14 )14 )
15 15
16 type ProxyRecord struct {16 type ProxyRecord struct {
17- Session string `json:"session"`17+ Session string `json:"session"`
18- PID int `json:"pid"`18+ PID int `json:"pid"`
19- Port int `json:"port"`19+ Port int `json:"port"`
20- Key string `json:"key"`20+ KeyFingerprint string `json:"key_fingerprint"`
21- Created time.Time `json:"created"`21+ Created time.Time `json:"created"`
22 }22 }
23 23
24-func (r ProxyRecord) ReadyLine() string { return fmt.Sprintf("VEV-UDP %d %s\n", r.Port, r.Key) }24+func KeyFingerprint(key []byte) string {
25+ sum := sha256.Sum256(key)
26+ return base64.StdEncoding.EncodeToString(sum[:])
27+}
25 28
26 type ProxyRegistry struct{ dir string }29 type ProxyRegistry struct{ dir string }
27 30
@@ -88,7 +91,7 @@ func (r *ProxyRegistry) RemoveOwned(rec ProxyRecord) error {
88 if err != nil {91 if err != nil {
89 return err92 return err
90 }93 }
91- if cur.PID != rec.PID || cur.Port != rec.Port || cur.Key != rec.Key {94+ if cur.PID != rec.PID || cur.Port != rec.Port || cur.KeyFingerprint != rec.KeyFingerprint {
92 return nil95 return nil
93 }96 }
94 return r.removeLocked(rec.Session)97 return r.removeLocked(rec.Session)
M internal/adapters/dgram/proxy_registry_test.go +52 -11
@@ -1,6 +1,8 @@
11 package dgram
22
33 import (
4+ "bytes"
5+ "encoding/json"
46 "os"
57 "path/filepath"
68 "testing"
@@ -11,10 +13,49 @@ func privateDir(t *testing.T) string {
1113 return filepath.Join(t.TempDir(), "vev")
1214 }
1315
16+func TestKeyFingerprint(t *testing.T) {
17+ const want = "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="
18+ if got := KeyFingerprint([]byte("secret")); got != want {
19+ t.Fatalf("KeyFingerprint()=%q, want %q", got, want)
20+ }
21+}
22+
23+func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) {
24+ r := NewProxyRegistry(privateDir(t))
25+ key := []byte("raw-secret-key-material")
26+ fingerprint := KeyFingerprint(key)
27+ rec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: fingerprint}
28+ if err := r.Publish(rec); err != nil {
29+ t.Fatal(err)
30+ }
31+
32+ data, err := os.ReadFile(r.path("work"))
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ if bytes.Contains(data, key) {
37+ t.Fatalf("registry file contains raw key material: %s", data)
38+ }
39+ var fields map[string]json.RawMessage
40+ if err := json.Unmarshal(data, &fields); err != nil {
41+ t.Fatal(err)
42+ }
43+ if _, ok := fields["key"]; ok {
44+ t.Fatalf("registry file contains raw key field: %s", data)
45+ }
46+ var got string
47+ if err := json.Unmarshal(fields["key_fingerprint"], &got); err != nil {
48+ t.Fatal(err)
49+ }
50+ if got != fingerprint {
51+ t.Fatalf("key_fingerprint=%q, want %q", got, fingerprint)
52+ }
53+}
54+
1455 func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
1556 t.Run("reuses live matching record", func(t *testing.T) {
1657 r := NewProxyRegistry(privateDir(t))
17- want := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "abc"}
58+ want := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "abc"}
1859 if err := r.Publish(want); err != nil {
1960 t.Fatal(err)
2061 }
@@ -22,16 +63,16 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
2263 if !ok {
2364 t.Fatal("Lookup ok=false, want true")
2465 }
25- if got.Port != want.Port || got.Key != want.Key || got.PID != want.PID {
66+ if got.Port != want.Port || got.KeyFingerprint != want.KeyFingerprint || got.PID != want.PID {
2667 t.Fatalf("Lookup()=%+v, want %+v", got, want)
2768 }
2869 })
2970 t.Run("removes stale record", func(t *testing.T) {
3071 r := NewProxyRegistry(privateDir(t))
31- if err := r.Publish(ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "abc"}); err != nil {
72+ if err := r.Publish(ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "abc"}); err != nil {
3273 t.Fatal(err)
3374 }
34- if err := os.WriteFile(r.path("work"), []byte(`{"session":"work","pid":-1,"port":61000,"key":"abc"}`), 0o600); err != nil {
75+ if err := os.WriteFile(r.path("work"), []byte(`{"session":"work","pid":-1,"port":61000,"key_fingerprint":"abc"}`), 0o600); err != nil {
3576 t.Fatal(err)
3677 }
3778 if _, ok := r.Lookup("work"); ok {
@@ -43,11 +84,11 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
4384 })
4485 t.Run("supersedes without pid-based kill and cleanup is owned", func(t *testing.T) {
4586 r := NewProxyRegistry(privateDir(t))
46- old := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "old"}
87+ old := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "old"}
4788 if err := r.Publish(old); err != nil {
4889 t.Fatal(err)
4990 }
50- newRec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61001, Key: "new"}
91+ newRec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61001, KeyFingerprint: "new"}
5192 if err := r.Publish(newRec); err != nil {
5293 t.Fatal(err)
5394 }
@@ -58,7 +99,7 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
5899 if !ok {
59100 t.Fatal("new live record removed by stale owner")
60101 }
61- if got.Port != newRec.Port || got.Key != newRec.Key {
102+ if got.Port != newRec.Port || got.KeyFingerprint != newRec.KeyFingerprint {
62103 t.Fatalf("Lookup()=%+v, want %+v", got, newRec)
63104 }
64105 if err := r.RemoveOwned(newRec); err != nil {
@@ -84,8 +125,8 @@ func TestProxyRegistrySessionPathIsCollisionFree(t *testing.T) {
84125 if r.path(tt.a) == r.path(tt.b) {
85126 t.Fatalf("path(%q) collides with path(%q): %s", tt.a, tt.b, r.path(tt.a))
86127 }
87- first := ProxyRecord{Session: tt.a, PID: os.Getpid(), Port: 61000, Key: "first"}
88- second := ProxyRecord{Session: tt.b, PID: os.Getpid(), Port: 61001, Key: "second"}
128+ first := ProxyRecord{Session: tt.a, PID: os.Getpid(), Port: 61000, KeyFingerprint: "first"}
129+ second := ProxyRecord{Session: tt.b, PID: os.Getpid(), Port: 61001, KeyFingerprint: "second"}
89130 if err := r.Publish(first); err != nil {
90131 t.Fatal(err)
91132 }
@@ -93,11 +134,11 @@ func TestProxyRegistrySessionPathIsCollisionFree(t *testing.T) {
93134 t.Fatal(err)
94135 }
95136 gotFirst, ok := r.Lookup(tt.a)
96- if !ok || gotFirst.Key != first.Key {
137+ if !ok || gotFirst.KeyFingerprint != first.KeyFingerprint {
97138 t.Fatalf("Lookup(%q)=%+v ok=%v, want first record", tt.a, gotFirst, ok)
98139 }
99140 gotSecond, ok := r.Lookup(tt.b)
100- if !ok || gotSecond.Key != second.Key {
141+ if !ok || gotSecond.KeyFingerprint != second.KeyFingerprint {
101142 t.Fatalf("Lookup(%q)=%+v ok=%v, want second record", tt.b, gotSecond, ok)
102143 }
103144 }
@@ -1,6 +1,8 @@
1 package dgram1 package dgram
2 2
3 import (3 import (
4+ "bytes"
5+ "encoding/json"
4 "os"6 "os"
5 "path/filepath"7 "path/filepath"
6 "testing"8 "testing"
@@ -11,10 +13,49 @@ func privateDir(t *testing.T) string {
11 return filepath.Join(t.TempDir(), "vev")13 return filepath.Join(t.TempDir(), "vev")
12 }14 }
13 15
16+func TestKeyFingerprint(t *testing.T) {
17+ const want = "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols="
18+ if got := KeyFingerprint([]byte("secret")); got != want {
19+ t.Fatalf("KeyFingerprint()=%q, want %q", got, want)
20+ }
21+}
22+
23+func TestProxyRegistryFileHoldsNoKeyMaterial(t *testing.T) {
24+ r := NewProxyRegistry(privateDir(t))
25+ key := []byte("raw-secret-key-material")
26+ fingerprint := KeyFingerprint(key)
27+ rec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: fingerprint}
28+ if err := r.Publish(rec); err != nil {
29+ t.Fatal(err)
30+ }
31+
32+ data, err := os.ReadFile(r.path("work"))
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ if bytes.Contains(data, key) {
37+ t.Fatalf("registry file contains raw key material: %s", data)
38+ }
39+ var fields map[string]json.RawMessage
40+ if err := json.Unmarshal(data, &fields); err != nil {
41+ t.Fatal(err)
42+ }
43+ if _, ok := fields["key"]; ok {
44+ t.Fatalf("registry file contains raw key field: %s", data)
45+ }
46+ var got string
47+ if err := json.Unmarshal(fields["key_fingerprint"], &got); err != nil {
48+ t.Fatal(err)
49+ }
50+ if got != fingerprint {
51+ t.Fatalf("key_fingerprint=%q, want %q", got, fingerprint)
52+ }
53+}
54+
14 func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {55 func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
15 t.Run("reuses live matching record", func(t *testing.T) {56 t.Run("reuses live matching record", func(t *testing.T) {
16 r := NewProxyRegistry(privateDir(t))57 r := NewProxyRegistry(privateDir(t))
17- want := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "abc"}58+ want := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "abc"}
18 if err := r.Publish(want); err != nil {59 if err := r.Publish(want); err != nil {
19 t.Fatal(err)60 t.Fatal(err)
20 }61 }
@@ -22,16 +63,16 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
22 if !ok {63 if !ok {
23 t.Fatal("Lookup ok=false, want true")64 t.Fatal("Lookup ok=false, want true")
24 }65 }
25- if got.Port != want.Port || got.Key != want.Key || got.PID != want.PID {66+ if got.Port != want.Port || got.KeyFingerprint != want.KeyFingerprint || got.PID != want.PID {
26 t.Fatalf("Lookup()=%+v, want %+v", got, want)67 t.Fatalf("Lookup()=%+v, want %+v", got, want)
27 }68 }
28 })69 })
29 t.Run("removes stale record", func(t *testing.T) {70 t.Run("removes stale record", func(t *testing.T) {
30 r := NewProxyRegistry(privateDir(t))71 r := NewProxyRegistry(privateDir(t))
31- if err := r.Publish(ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "abc"}); err != nil {72+ if err := r.Publish(ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "abc"}); err != nil {
32 t.Fatal(err)73 t.Fatal(err)
33 }74 }
34- if err := os.WriteFile(r.path("work"), []byte(`{"session":"work","pid":-1,"port":61000,"key":"abc"}`), 0o600); err != nil {75+ if err := os.WriteFile(r.path("work"), []byte(`{"session":"work","pid":-1,"port":61000,"key_fingerprint":"abc"}`), 0o600); err != nil {
35 t.Fatal(err)76 t.Fatal(err)
36 }77 }
37 if _, ok := r.Lookup("work"); ok {78 if _, ok := r.Lookup("work"); ok {
@@ -43,11 +84,11 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
43 })84 })
44 t.Run("supersedes without pid-based kill and cleanup is owned", func(t *testing.T) {85 t.Run("supersedes without pid-based kill and cleanup is owned", func(t *testing.T) {
45 r := NewProxyRegistry(privateDir(t))86 r := NewProxyRegistry(privateDir(t))
46- old := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, Key: "old"}87+ old := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61000, KeyFingerprint: "old"}
47 if err := r.Publish(old); err != nil {88 if err := r.Publish(old); err != nil {
48 t.Fatal(err)89 t.Fatal(err)
49 }90 }
50- newRec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61001, Key: "new"}91+ newRec := ProxyRecord{Session: "work", PID: os.Getpid(), Port: 61001, KeyFingerprint: "new"}
51 if err := r.Publish(newRec); err != nil {92 if err := r.Publish(newRec); err != nil {
52 t.Fatal(err)93 t.Fatal(err)
53 }94 }
@@ -58,7 +99,7 @@ func TestProxyRegistryReuseStaleCleanupAndSupersede(t *testing.T) {
58 if !ok {99 if !ok {
59 t.Fatal("new live record removed by stale owner")100 t.Fatal("new live record removed by stale owner")
60 }101 }
61- if got.Port != newRec.Port || got.Key != newRec.Key {102+ if got.Port != newRec.Port || got.KeyFingerprint != newRec.KeyFingerprint {
62 t.Fatalf("Lookup()=%+v, want %+v", got, newRec)103 t.Fatalf("Lookup()=%+v, want %+v", got, newRec)
63 }104 }
64 if err := r.RemoveOwned(newRec); err != nil {105 if err := r.RemoveOwned(newRec); err != nil {
@@ -84,8 +125,8 @@ func TestProxyRegistrySessionPathIsCollisionFree(t *testing.T) {
84 if r.path(tt.a) == r.path(tt.b) {125 if r.path(tt.a) == r.path(tt.b) {
85 t.Fatalf("path(%q) collides with path(%q): %s", tt.a, tt.b, r.path(tt.a))126 t.Fatalf("path(%q) collides with path(%q): %s", tt.a, tt.b, r.path(tt.a))
86 }127 }
87- first := ProxyRecord{Session: tt.a, PID: os.Getpid(), Port: 61000, Key: "first"}128+ first := ProxyRecord{Session: tt.a, PID: os.Getpid(), Port: 61000, KeyFingerprint: "first"}
88- second := ProxyRecord{Session: tt.b, PID: os.Getpid(), Port: 61001, Key: "second"}129+ second := ProxyRecord{Session: tt.b, PID: os.Getpid(), Port: 61001, KeyFingerprint: "second"}
89 if err := r.Publish(first); err != nil {130 if err := r.Publish(first); err != nil {
90 t.Fatal(err)131 t.Fatal(err)
91 }132 }
@@ -93,11 +134,11 @@ func TestProxyRegistrySessionPathIsCollisionFree(t *testing.T) {
93 t.Fatal(err)134 t.Fatal(err)
94 }135 }
95 gotFirst, ok := r.Lookup(tt.a)136 gotFirst, ok := r.Lookup(tt.a)
96- if !ok || gotFirst.Key != first.Key {137+ if !ok || gotFirst.KeyFingerprint != first.KeyFingerprint {
97 t.Fatalf("Lookup(%q)=%+v ok=%v, want first record", tt.a, gotFirst, ok)138 t.Fatalf("Lookup(%q)=%+v ok=%v, want first record", tt.a, gotFirst, ok)
98 }139 }
99 gotSecond, ok := r.Lookup(tt.b)140 gotSecond, ok := r.Lookup(tt.b)
100- if !ok || gotSecond.Key != second.Key {141+ if !ok || gotSecond.KeyFingerprint != second.KeyFingerprint {
101 t.Fatalf("Lookup(%q)=%+v ok=%v, want second record", tt.b, gotSecond, ok)142 t.Fatalf("Lookup(%q)=%+v ok=%v, want second record", tt.b, gotSecond, ok)
102 }143 }
103 }144 }
M internal/app/run.go +1 -1
@@ -954,7 +954,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e
954954 }
955955 keyText := base64.StdEncoding.EncodeToString(key)
956956 registry := dgram.NewProxyRegistry(filepath.Join(ipc.SocketDir(), "udp-proxies"))
957- record := dgram.ProxyRecord{Session: session, PID: os.Getpid(), Port: addr.Port, Key: keyText}
957+ record := dgram.ProxyRecord{Session: session, PID: os.Getpid(), Port: addr.Port, KeyFingerprint: dgram.KeyFingerprint(key)}
958958 if err := registry.Publish(record); err != nil {
959959 return err
960960 }
@@ -954,7 +954,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e
954 }954 }
955 keyText := base64.StdEncoding.EncodeToString(key)955 keyText := base64.StdEncoding.EncodeToString(key)
956 registry := dgram.NewProxyRegistry(filepath.Join(ipc.SocketDir(), "udp-proxies"))956 registry := dgram.NewProxyRegistry(filepath.Join(ipc.SocketDir(), "udp-proxies"))
957- record := dgram.ProxyRecord{Session: session, PID: os.Getpid(), Port: addr.Port, Key: keyText}957+ record := dgram.ProxyRecord{Session: session, PID: os.Getpid(), Port: addr.Port, KeyFingerprint: dgram.KeyFingerprint(key)}
958 if err := registry.Publish(record); err != nil {958 if err := registry.Publish(record); err != nil {
959 return err959 return err
960 }960 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close