proxy_registry.go RawHistoryFolder
1package dgram
2
3import (
4 "crypto/sha256"
5 "encoding/base64"
6 "encoding/json"
7 "errors"
8 "os"
9 "path/filepath"
10 "syscall"
11 "time"
12
13 "github.com/bnema/vev/pkg/safedir"
14)
15
16type ProxyRecord struct {
17 Session string `json:"session"`
18 PID int `json:"pid"`
19 Port int `json:"port"`
20 // KeyFingerprint is a non-secret ownership tag (SHA-256 of the AEAD key).
21 // The raw key must never be persisted by vev.
22 KeyFingerprint string `json:"key_fingerprint"`
23 Created time.Time `json:"created"`
24}
25
26// KeyFingerprint derives the non-secret registry ownership tag.
27func KeyFingerprint(key []byte) string {
28 sum := sha256.Sum256(key)
29 return base64.StdEncoding.EncodeToString(sum[:])
30}
31
32type ProxyRegistry struct{ dir string }
33
34func NewProxyRegistry(dir string) *ProxyRegistry { return &ProxyRegistry{dir: dir} }
35
36func (r *ProxyRegistry) Lookup(session string) (ProxyRecord, bool) {
37 var rec ProxyRecord
38 ok := false
39 _ = r.withLock(func() error {
40 var err error
41 rec, err = r.read(session)
42 if err != nil {
43 return nil
44 }
45 if !processAlive(rec.PID) {
46 _ = r.removeLocked(session)
47 return nil
48 }
49 if rec.PID != os.Getpid() {
50 return nil
51 }
52 ok = true
53 return nil
54 })
55 return rec, ok
56}
57
58func (r *ProxyRegistry) Publish(rec ProxyRecord) error {
59 if rec.Created.IsZero() {
60 rec.Created = time.Now()
61 }
62 return r.withLock(func() error {
63 b, err := json.Marshal(rec)
64 if err != nil {
65 return err
66 }
67 tmp, err := os.CreateTemp(r.dir, ".udp-proxy-*.tmp")
68 if err != nil {
69 return err
70 }
71 tmpName := tmp.Name()
72 defer func() { _ = os.Remove(tmpName) }()
73 if _, err := tmp.Write(b); err != nil {
74 _ = tmp.Close()
75 return err
76 }
77 if err := tmp.Close(); err != nil {
78 return err
79 }
80 return os.Rename(tmpName, r.path(rec.Session))
81 })
82}
83
84func (r *ProxyRegistry) Remove(session string) error {
85 return r.withLock(func() error { return r.removeLocked(session) })
86}
87
88func (r *ProxyRegistry) RemoveOwned(rec ProxyRecord) error {
89 return r.withLock(func() error {
90 cur, err := r.read(rec.Session)
91 if errors.Is(err, os.ErrNotExist) {
92 return nil
93 }
94 if err != nil {
95 return err
96 }
97 if cur.PID != rec.PID || cur.Port != rec.Port || cur.KeyFingerprint != rec.KeyFingerprint {
98 return nil
99 }
100 return r.removeLocked(rec.Session)
101 })
102}
103
104func (r *ProxyRegistry) withLock(fn func() error) error {
105 if err := safedir.EnsurePrivate(r.dir); err != nil {
106 return err
107 }
108 lock, err := os.OpenFile(filepath.Join(r.dir, ".lock"), os.O_CREATE|os.O_RDWR, 0o600)
109 if err != nil {
110 return err
111 }
112 defer func() { _ = lock.Close() }()
113 if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
114 return err
115 }
116 defer func() { _ = syscall.Flock(int(lock.Fd()), syscall.LOCK_UN) }()
117 return fn()
118}
119
120func (r *ProxyRegistry) removeLocked(session string) error {
121 err := os.Remove(r.path(session))
122 if errors.Is(err, os.ErrNotExist) {
123 return nil
124 }
125 return err
126}
127
128func (r *ProxyRegistry) read(session string) (ProxyRecord, error) {
129 b, err := os.ReadFile(r.path(session))
130 if err != nil {
131 return ProxyRecord{}, err
132 }
133 var rec ProxyRecord
134 if err := json.Unmarshal(b, &rec); err != nil {
135 return ProxyRecord{}, err
136 }
137 return rec, nil
138}
139
140func (r *ProxyRegistry) path(session string) string {
141 if session == "" {
142 return filepath.Join(r.dir, "udp-proxy-empty.json")
143 }
144 name := base64.RawURLEncoding.EncodeToString([]byte(session))
145 return filepath.Join(r.dir, "udp-proxy-"+name+".json")
146}
147
148func processAlive(pid int) bool {
149 if pid <= 0 {
150 return false
151 }
152 p, err := os.FindProcess(pid)
153 if err != nil {
154 return false
155 }
156 err = p.Signal(syscall.Signal(0))
157 return err == nil || errors.Is(err, syscall.EPERM)
158}