Refactor relay server management by introducing separate managers for BPS, IP, and approval handling
Kim committed
Dec 12, 2025 at 18:00 UTC
3412a20d1a54dd6daf1c79bc603d3128e3c4a9ba
9 files changed
+541
-514
cmd/relay-server/admin.go
+232
-274
@@ -13,11 +13,178 @@ import (
13
14
"github.com/rs/zerolog/log"
15
16
+ "gosuda.org/portal/cmd/relay-server/manager"
17
"gosuda.org/portal/portal"
18
"gosuda.org/portal/utils"
19
)
20
20
-func handleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
21
+// Admin manages approval state and persistence for relay-server.
22
+type Admin struct {
23
+ settingsPath string
24
+ settingsMu sync.Mutex
25
+
26
+ approveManager *manager.ApproveManager
27
+ bpsManager *manager.BPSManager
28
+ ipManager *manager.IPManager
29
+}
30
+
31
+func NewAdmin(defaultLeaseBPS int64) *Admin {
32
+ bpsManager := manager.NewBPSManager()
33
+ if defaultLeaseBPS > 0 {
34
+ bpsManager.SetDefaultBPS(defaultLeaseBPS)
35
+ }
36
+ return &Admin{
37
+ settingsPath: "admin_settings.json",
38
+ approveManager: manager.NewApproveManager(),
39
+ bpsManager: bpsManager,
40
+ ipManager: manager.NewIPManager(),
41
+ }
42
+}
43
+
44
+// GetApproveManager exposes the approval manager.
45
+func (a *Admin) GetApproveManager() *manager.ApproveManager {
46
+ return a.approveManager
47
+}
48
+
49
+// GetBPSManager exposes the BPS manager.
50
+func (a *Admin) GetBPSManager() *manager.BPSManager {
51
+ return a.bpsManager
52
+}
53
+
54
+// GetIPManager exposes the IP manager.
55
+func (a *Admin) GetIPManager() *manager.IPManager {
56
+ return a.ipManager
57
+}
58
+
59
+// adminSettings stores persistent admin configuration
60
+type adminSettings struct {
61
+ BannedLeases []string `json:"banned_leases"`
62
+ BPSLimits map[string]int64 `json:"bps_limits"`
63
+ ApprovalMode manager.ApprovalMode `json:"approval_mode"`
64
+ ApprovedLeases []string `json:"approved_leases,omitempty"`
65
+ DeniedLeases []string `json:"denied_leases,omitempty"`
66
+ BannedIPs []string `json:"banned_ips,omitempty"`
67
+}
68
+
69
+func (a *Admin) SetSettingsPath(path string) {
70
+ a.settingsMu.Lock()
71
+ defer a.settingsMu.Unlock()
72
+ a.settingsPath = path
73
+}
74
+
75
+func (a *Admin) SaveSettings(serv *portal.RelayServer) {
76
+ a.settingsMu.Lock()
77
+ defer a.settingsMu.Unlock()
78
+
79
+ lm := serv.GetLeaseManager()
80
+
81
+ bannedBytes := lm.GetBannedLeases()
82
+ banned := make([]string, len(bannedBytes))
83
+ for i, b := range bannedBytes {
84
+ banned[i] = string(b)
85
+ }
86
+
87
+ bpsLimits := map[string]int64{}
88
+ if a.bpsManager != nil {
89
+ bpsLimits = a.bpsManager.GetAllBPSLimits()
90
+ }
91
+
92
+ var bannedIPs []string
93
+ if a.ipManager != nil {
94
+ bannedIPs = a.ipManager.GetBannedIPs()
95
+ }
96
+
97
+ settings := adminSettings{
98
+ BannedLeases: banned,
99
+ BPSLimits: bpsLimits,
100
+ ApprovalMode: a.approveManager.GetApprovalMode(),
101
+ ApprovedLeases: a.approveManager.GetApprovedLeases(),
102
+ DeniedLeases: a.approveManager.GetDeniedLeases(),
103
+ BannedIPs: bannedIPs,
104
+ }
105
+
106
+ data, err := json.MarshalIndent(settings, "", " ")
107
+ if err != nil {
108
+ log.Error().Err(err).Msg("[Admin] Failed to marshal admin settings")
109
+ return
110
+ }
111
+
112
+ dir := filepath.Dir(a.settingsPath)
113
+ if dir != "" && dir != "." {
114
+ if err := os.MkdirAll(dir, 0755); err != nil {
115
+ log.Error().Err(err).Msg("[Admin] Failed to create settings directory")
116
+ return
117
+ }
118
+ }
119
+
120
+ if err := os.WriteFile(a.settingsPath, data, 0644); err != nil {
121
+ log.Error().Err(err).Msg("[Admin] Failed to save admin settings")
122
+ return
123
+ }
124
+
125
+ log.Debug().Str("path", a.settingsPath).Msg("[Admin] Saved admin settings")
126
+}
127
+
128
+func (a *Admin) LoadSettings(serv *portal.RelayServer) {
129
+ a.settingsMu.Lock()
130
+ defer a.settingsMu.Unlock()
131
+
132
+ data, err := os.ReadFile(a.settingsPath)
133
+ if err != nil {
134
+ if os.IsNotExist(err) {
135
+ log.Debug().Msg("[Admin] No admin settings file found, starting fresh")
136
+ return
137
+ }
138
+ log.Error().Err(err).Msg("[Admin] Failed to read admin settings")
139
+ return
140
+ }
141
+
142
+ var settings adminSettings
143
+ if err := json.Unmarshal(data, &settings); err != nil {
144
+ log.Error().Err(err).Msg("[Admin] Failed to parse admin settings")
145
+ return
146
+ }
147
+
148
+ lm := serv.GetLeaseManager()
149
+
150
+ for _, leaseID := range settings.BannedLeases {
151
+ lm.BanLease(leaseID)
152
+ }
153
+
154
+ for leaseID, bps := range settings.BPSLimits {
155
+ if a.bpsManager != nil {
156
+ a.bpsManager.SetBPSLimit(leaseID, bps)
157
+ }
158
+ }
159
+
160
+ if settings.ApprovalMode != "" {
161
+ a.approveManager.SetApprovalMode(settings.ApprovalMode)
162
+ }
163
+
164
+ for _, leaseID := range settings.ApprovedLeases {
165
+ a.approveManager.ApproveLease(leaseID)
166
+ }
167
+
168
+ for _, leaseID := range settings.DeniedLeases {
169
+ a.approveManager.DenyLease(leaseID)
170
+ }
171
+
172
+ if a.ipManager != nil && len(settings.BannedIPs) > 0 {
173
+ a.ipManager.SetBannedIPs(settings.BannedIPs)
174
+ }
175
+
176
+ log.Info().
177
+ Int("banned_count", len(settings.BannedLeases)).
178
+ Int("bps_limits_count", len(settings.BPSLimits)).
179
+ Str("approval_mode", string(a.approveManager.GetApprovalMode())).
180
+ Int("approved_count", len(settings.ApprovedLeases)).
181
+ Int("denied_count", len(settings.DeniedLeases)).
182
+ Int("banned_ips_count", len(settings.BannedIPs)).
183
+ Msg("[Admin] Loaded admin settings")
184
+}
185
+
186
+// HandleAdminRequest routes /admin/* requests.
187
+func (a *Admin) HandleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
188
if !utils.IsLocalhost(r) {
189
http.Error(w, "Forbidden", http.StatusForbidden)
190
return
@@ -27,9 +194,9 @@ func handleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.Rel
194
195
switch {
196
case route == "":
30
- serveAppStatic(w, r, "", serv)
197
+ serveAppStatic(w, r, "", serv, a)
198
case route == "leases" && r.Method == http.MethodGet:
32
- writeJSON(w, convertLeaseEntriesToAdminRows(serv))
199
+ writeJSON(w, a.convertLeaseEntriesToAdminRows(serv))
200
case route == "leases/banned" && r.Method == http.MethodGet:
201
writeJSON(w, serv.GetLeaseManager().GetBannedLeases())
202
case route == "stats" && r.Method == http.MethodGet:
@@ -38,25 +205,25 @@ func handleAdminRequest(w http.ResponseWriter, r *http.Request, serv *portal.Rel
205
"uptime": "TODO",
206
})
207
case route == "settings" && r.Method == http.MethodGet:
41
- handleGetSettings(w)
208
+ a.handleGetSettings(w)
209
case route == "settings/approval-mode":
43
- handleApprovalModeRequest(w, r, serv)
210
+ a.handleApprovalModeRequest(w, r, serv)
211
case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/ban"):
45
- handleLeaseBanRequest(w, r, serv, route)
212
+ a.handleLeaseBanRequest(w, r, serv, route)
213
case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/bps"):
47
- handleLeaseBPSRequest(w, r, serv, route)
214
+ a.handleLeaseBPSRequest(w, r, serv, route)
215
case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/approve"):
49
- handleLeaseApproveRequest(w, r, serv, route)
216
+ a.handleLeaseApproveRequest(w, r, serv, route)
217
case strings.HasPrefix(route, "leases/") && strings.HasSuffix(route, "/deny"):
51
- handleLeaseDenyRequest(w, r, serv, route)
218
+ a.handleLeaseDenyRequest(w, r, serv, route)
219
case strings.HasPrefix(route, "ips/") && strings.HasSuffix(route, "/ban"):
53
- handleIPBanRequest(w, r, serv, route)
220
+ a.handleIPBanRequest(w, r, serv, route)
221
default:
222
http.NotFound(w, r)
223
}
224
}
225
59
-func handleLeaseBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
226
+func (a *Admin) handleLeaseBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
227
parts := strings.Split(route, "/")
228
if len(parts) != 3 {
229
http.NotFound(w, r)
@@ -72,30 +239,30 @@ func handleLeaseBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.
239
switch r.Method {
240
case http.MethodPost:
241
serv.GetLeaseManager().BanLease(leaseID)
75
- saveAdminSettings(serv, globalBPSManager)
242
+ a.SaveSettings(serv)
243
w.WriteHeader(http.StatusOK)
244
case http.MethodDelete:
245
serv.GetLeaseManager().UnbanLease(leaseID)
79
- saveAdminSettings(serv, globalBPSManager)
246
+ a.SaveSettings(serv)
247
w.WriteHeader(http.StatusOK)
248
default:
249
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
250
}
251
}
252
86
-func handleGetSettings(w http.ResponseWriter) {
253
+func (a *Admin) handleGetSettings(w http.ResponseWriter) {
254
writeJSON(w, map[string]interface{}{
88
- "approval_mode": getApprovalMode(),
89
- "approved_leases": getApprovedLeases(),
90
- "denied_leases": getDeniedLeases(),
255
+ "approval_mode": a.approveManager.GetApprovalMode(),
256
+ "approved_leases": a.approveManager.GetApprovedLeases(),
257
+ "denied_leases": a.approveManager.GetDeniedLeases(),
258
})
259
}
260
94
-func handleApprovalModeRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
261
+func (a *Admin) handleApprovalModeRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
262
switch r.Method {
263
case http.MethodGet:
264
writeJSON(w, map[string]interface{}{
98
- "approval_mode": getApprovalMode(),
265
+ "approval_mode": a.approveManager.GetApprovalMode(),
266
})
267
case http.MethodPost:
268
var req struct {
@@ -105,13 +272,13 @@ func handleApprovalModeRequest(w http.ResponseWriter, r *http.Request, serv *por
272
http.Error(w, "Invalid request body", http.StatusBadRequest)
273
return
274
}
108
- mode := ApprovalMode(req.Mode)
109
- if mode != ApprovalModeAuto && mode != ApprovalModeManual {
275
+ mode := manager.ApprovalMode(req.Mode)
276
+ if mode != manager.ApprovalModeAuto && mode != manager.ApprovalModeManual {
277
http.Error(w, "Invalid mode (must be 'auto' or 'manual')", http.StatusBadRequest)
278
return
279
}
113
- setApprovalMode(mode)
114
- saveAdminSettings(serv, globalBPSManager)
280
+ a.approveManager.SetApprovalMode(mode)
281
+ a.SaveSettings(serv)
282
log.Info().Str("mode", string(mode)).Msg("[Admin] Approval mode changed")
283
writeJSON(w, map[string]interface{}{
284
"approval_mode": mode,
@@ -121,7 +288,7 @@ func handleApprovalModeRequest(w http.ResponseWriter, r *http.Request, serv *por
288
}
289
}
290
124
-func handleLeaseApproveRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
291
+func (a *Admin) handleLeaseApproveRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
292
parts := strings.Split(route, "/")
293
if len(parts) != 3 {
294
http.NotFound(w, r)
@@ -136,14 +303,14 @@ func handleLeaseApproveRequest(w http.ResponseWriter, r *http.Request, serv *por
303
304
switch r.Method {
305
case http.MethodPost:
139
- approveLease(leaseID)
140
- undenyLease(leaseID) // Remove from denied if exists
141
- saveAdminSettings(serv, globalBPSManager)
306
+ a.approveManager.ApproveLease(leaseID)
307
+ a.approveManager.UndenyLease(leaseID) // Remove from denied if exists
308
+ a.SaveSettings(serv)
309
log.Info().Str("lease_id", leaseID).Msg("[Admin] Lease approved")
310
w.WriteHeader(http.StatusOK)
311
case http.MethodDelete:
145
- revokeLease(leaseID)
146
- saveAdminSettings(serv, globalBPSManager)
312
+ a.approveManager.RevokeLease(leaseID)
313
+ a.SaveSettings(serv)
314
log.Info().Str("lease_id", leaseID).Msg("[Admin] Lease approval revoked")
315
w.WriteHeader(http.StatusOK)
316
default:
@@ -151,7 +318,7 @@ func handleLeaseApproveRequest(w http.ResponseWriter, r *http.Request, serv *por
318
}
319
}
320
154
-func handleLeaseDenyRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
321
+func (a *Admin) handleLeaseDenyRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
322
parts := strings.Split(route, "/")
323
if len(parts) != 3 {
324
http.NotFound(w, r)
@@ -166,13 +333,13 @@ func handleLeaseDenyRequest(w http.ResponseWriter, r *http.Request, serv *portal
333
334
switch r.Method {
335
case http.MethodPost:
169
- denyLease(leaseID)
170
- saveAdminSettings(serv, globalBPSManager)
336
+ a.approveManager.DenyLease(leaseID)
337
+ a.SaveSettings(serv)
338
log.Info().Str("lease_id", leaseID).Msg("[Admin] Lease denied")
339
w.WriteHeader(http.StatusOK)
340
case http.MethodDelete:
174
- undenyLease(leaseID)
175
- saveAdminSettings(serv, globalBPSManager)
341
+ a.approveManager.UndenyLease(leaseID)
342
+ a.SaveSettings(serv)
343
log.Info().Str("lease_id", leaseID).Msg("[Admin] Lease denial removed")
344
w.WriteHeader(http.StatusOK)
345
default:
@@ -180,7 +347,7 @@ func handleLeaseDenyRequest(w http.ResponseWriter, r *http.Request, serv *portal
347
}
348
}
349
183
-func handleLeaseBPSRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
350
+func (a *Admin) handleLeaseBPSRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
351
parts := strings.Split(route, "/")
352
if len(parts) != 3 {
353
http.NotFound(w, r)
@@ -202,30 +369,38 @@ func handleLeaseBPSRequest(w http.ResponseWriter, r *http.Request, serv *portal.
369
http.Error(w, "Invalid request body", http.StatusBadRequest)
370
return
371
}
205
- oldBPS := globalBPSManager.GetBPSLimit(leaseID)
206
- globalBPSManager.SetBPSLimit(leaseID, req.BPS)
372
+ if a.bpsManager == nil {
373
+ http.Error(w, "BPS manager not initialized", http.StatusInternalServerError)
374
+ return
375
+ }
376
+ oldBPS := a.bpsManager.GetBPSLimit(leaseID)
377
+ a.bpsManager.SetBPSLimit(leaseID, req.BPS)
378
log.Info().
379
Str("lease_id", leaseID).
380
Int64("old_bps", oldBPS).
381
Int64("new_bps", req.BPS).
382
Msg("[Admin] BPS limit updated")
212
- saveAdminSettings(serv, globalBPSManager)
383
+ a.SaveSettings(serv)
384
w.WriteHeader(http.StatusOK)
385
case http.MethodDelete:
215
- oldBPS := globalBPSManager.GetBPSLimit(leaseID)
216
- globalBPSManager.SetBPSLimit(leaseID, 0)
386
+ if a.bpsManager == nil {
387
+ http.Error(w, "BPS manager not initialized", http.StatusInternalServerError)
388
+ return
389
+ }
390
+ oldBPS := a.bpsManager.GetBPSLimit(leaseID)
391
+ a.bpsManager.SetBPSLimit(leaseID, 0)
392
log.Info().
393
Str("lease_id", leaseID).
394
Int64("old_bps", oldBPS).
395
Msg("[Admin] BPS limit removed (now unlimited)")
221
- saveAdminSettings(serv, globalBPSManager)
396
+ a.SaveSettings(serv)
397
w.WriteHeader(http.StatusOK)
398
default:
399
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
400
}
401
}
402
228
-func handleIPBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
403
+func (a *Admin) handleIPBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, route string) {
404
// Route format: ips/{ip}/ban
405
parts := strings.Split(route, "/")
406
if len(parts) != 3 {
@@ -239,20 +414,20 @@ func handleIPBanRequest(w http.ResponseWriter, r *http.Request, serv *portal.Rel
414
return
415
}
416
242
- if globalIPManager == nil {
417
+ if a.ipManager == nil {
418
http.Error(w, "IP manager not initialized", http.StatusInternalServerError)
419
return
420
}
421
422
switch r.Method {
423
case http.MethodPost:
249
- globalIPManager.BanIP(ip)
250
- saveAdminSettings(serv, globalBPSManager)
424
+ a.ipManager.BanIP(ip)
425
+ a.SaveSettings(serv)
426
log.Info().Str("ip", ip).Msg("[Admin] IP banned")
427
w.WriteHeader(http.StatusOK)
428
case http.MethodDelete:
254
- globalIPManager.UnbanIP(ip)
255
- saveAdminSettings(serv, globalBPSManager)
429
+ a.ipManager.UnbanIP(ip)
430
+ a.SaveSettings(serv)
431
log.Info().Str("ip", ip).Msg("[Admin] IP unbanned")
432
w.WriteHeader(http.StatusOK)
433
default:
@@ -272,7 +447,7 @@ func decodeLeaseID(encoded string) (string, bool) {
447
}
448
449
// convertLeaseEntriesToAdminRows converts LeaseEntry data to leaseRow format for admin API
275
-func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
450
+func (a *Admin) convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
451
leaseEntries := serv.GetAllLeaseEntries()
452
rows := []leaseRow{}
453
now := time.Now()
@@ -346,15 +521,18 @@ func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
521
}
522
link := fmt.Sprintf("//%s.%s/", lease.Name, utils.StripWildCard(utils.StripScheme(base)))
523
349
- bps := globalBPSManager.GetBPSLimit(identityID)
524
+ var bps int64
525
+ if a.bpsManager != nil {
526
+ bps = a.bpsManager.GetBPSLimit(identityID)
527
+ }
528
529
// Get IP info for this lease
530
var ip string
531
var isIPBanned bool
354
- if globalIPManager != nil {
355
- ip = globalIPManager.GetLeaseIP(identityID)
532
+ if a.ipManager != nil {
533
+ ip = a.ipManager.GetLeaseIP(identityID)
534
if ip != "" {
357
- isIPBanned = globalIPManager.IsIPBanned(ip)
535
+ isIPBanned = a.ipManager.IsIPBanned(ip)
536
}
537
}
538
@@ -373,8 +551,8 @@ func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
551
Hide: leaseEntry.ParsedMetadata != nil && leaseEntry.ParsedMetadata.Hide,
552
Metadata: lease.Metadata,
553
BPS: bps,
376
- IsApproved: getApprovalMode() == ApprovalModeAuto || isLeaseApproved(identityID),
377
- IsDenied: isLeaseDenied(identityID),
554
+ IsApproved: a.approveManager.GetApprovalMode() == manager.ApprovalModeAuto || a.approveManager.IsLeaseApproved(identityID),
555
+ IsDenied: a.approveManager.IsLeaseDenied(identityID),
556
IP: ip,
557
IsIPBanned: isIPBanned,
558
})
@@ -382,223 +560,3 @@ func convertLeaseEntriesToAdminRows(serv *portal.RelayServer) []leaseRow {
560
561
return rows
562
}
385
-
386
-// ApprovalMode represents the approval mode for new connections
387
-type ApprovalMode string
388
-
389
-const (
390
- ApprovalModeAuto ApprovalMode = "auto"
391
- ApprovalModeManual ApprovalMode = "manual"
392
-)
393
-
394
-// AdminSettings stores persistent admin configuration
395
-type AdminSettings struct {
396
- BannedLeases []string `json:"banned_leases"`
397
- BPSLimits map[string]int64 `json:"bps_limits"`
398
- ApprovalMode ApprovalMode `json:"approval_mode"`
399
- ApprovedLeases []string `json:"approved_leases,omitempty"`
400
- DeniedLeases []string `json:"denied_leases,omitempty"`
401
- BannedIPs []string `json:"banned_ips,omitempty"`
402
-}
403
-
404
-// Global approval mode (default: auto)
405
-var (
406
- globalApprovalMode ApprovalMode = ApprovalModeAuto
407
- globalApprovedLeases = make(map[string]struct{})
408
- globalDeniedLeases = make(map[string]struct{})
409
- approvalMu sync.RWMutex
410
-)
411
-
412
-func getApprovalMode() ApprovalMode {
413
- approvalMu.RLock()
414
- defer approvalMu.RUnlock()
415
- return globalApprovalMode
416
-}
417
-
418
-func setApprovalMode(mode ApprovalMode) {
419
- approvalMu.Lock()
420
- defer approvalMu.Unlock()
421
- globalApprovalMode = mode
422
-}
423
-
424
-func isLeaseApproved(leaseID string) bool {
425
- approvalMu.RLock()
426
- defer approvalMu.RUnlock()
427
- _, ok := globalApprovedLeases[leaseID]
428
- return ok
429
-}
430
-
431
-func approveLease(leaseID string) {
432
- approvalMu.Lock()
433
- defer approvalMu.Unlock()
434
- globalApprovedLeases[leaseID] = struct{}{}
435
-}
436
-
437
-func revokeLease(leaseID string) {
438
- approvalMu.Lock()
439
- defer approvalMu.Unlock()
440
- delete(globalApprovedLeases, leaseID)
441
-}
442
-
443
-func getApprovedLeases() []string {
444
- approvalMu.RLock()
445
- defer approvalMu.RUnlock()
446
- result := make([]string, 0, len(globalApprovedLeases))
447
- for id := range globalApprovedLeases {
448
- result = append(result, id)
449
- }
450
- return result
451
-}
452
-
453
-func isLeaseDenied(leaseID string) bool {
454
- approvalMu.RLock()
455
- defer approvalMu.RUnlock()
456
- _, ok := globalDeniedLeases[leaseID]
457
- return ok
458
-}
459
-
460
-func denyLease(leaseID string) {
461
- approvalMu.Lock()
462
- defer approvalMu.Unlock()
463
- globalDeniedLeases[leaseID] = struct{}{}
464
- // Remove from approved if exists
465
- delete(globalApprovedLeases, leaseID)
466
-}
467
-
468
-func undenyLease(leaseID string) {
469
- approvalMu.Lock()
470
- defer approvalMu.Unlock()
471
- delete(globalDeniedLeases, leaseID)
472
-}
473
-
474
-func getDeniedLeases() []string {
475
- approvalMu.RLock()
476
- defer approvalMu.RUnlock()
477
- result := make([]string, 0, len(globalDeniedLeases))
478
- for id := range globalDeniedLeases {
479
- result = append(result, id)
480
- }
481
- return result
482
-}
483
-
484
-var (
485
- adminSettingsPath = "admin_settings.json"
486
- adminSettingsMu sync.Mutex
487
-)
488
-
489
-// SetAdminSettingsPath sets the path for admin settings file
490
-func SetAdminSettingsPath(path string) {
491
- adminSettingsMu.Lock()
492
- defer adminSettingsMu.Unlock()
493
- adminSettingsPath = path
494
-}
495
-
496
-func saveAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager) {
497
- adminSettingsMu.Lock()
498
- defer adminSettingsMu.Unlock()
499
-
500
- lm := serv.GetLeaseManager()
501
-
502
- bannedBytes := lm.GetBannedLeases()
503
- banned := make([]string, len(bannedBytes))
504
- for i, b := range bannedBytes {
505
- banned[i] = string(b)
506
- }
507
-
508
- bpsLimits := bpsManager.GetAllBPSLimits()
509
-
510
- var bannedIPs []string
511
- if globalIPManager != nil {
512
- bannedIPs = globalIPManager.GetBannedIPs()
513
- }
514
-
515
- settings := AdminSettings{
516
- BannedLeases: banned,
517
- BPSLimits: bpsLimits,
518
- ApprovalMode: getApprovalMode(),
519
- ApprovedLeases: getApprovedLeases(),
520
- DeniedLeases: getDeniedLeases(),
521
- BannedIPs: bannedIPs,
522
- }
523
-
524
- data, err := json.MarshalIndent(settings, "", " ")
525
- if err != nil {
526
- log.Error().Err(err).Msg("[Admin] Failed to marshal admin settings")
527
- return
528
- }
529
-
530
- dir := filepath.Dir(adminSettingsPath)
531
- if dir != "" && dir != "." {
532
- if err := os.MkdirAll(dir, 0755); err != nil {
533
- log.Error().Err(err).Msg("[Admin] Failed to create settings directory")
534
- return
535
- }
536
- }
537
-
538
- if err := os.WriteFile(adminSettingsPath, data, 0644); err != nil {
539
- log.Error().Err(err).Msg("[Admin] Failed to save admin settings")
540
- return
541
- }
542
-
543
- log.Debug().Str("path", adminSettingsPath).Msg("[Admin] Saved admin settings")
544
-}
545
-
546
-func loadAdminSettings(serv *portal.RelayServer, bpsManager *BPSManager, ipManager *IPManager) {
547
- adminSettingsMu.Lock()
548
- defer adminSettingsMu.Unlock()
549
-
550
- data, err := os.ReadFile(adminSettingsPath)
551
- if err != nil {
552
- if os.IsNotExist(err) {
553
- log.Debug().Msg("[Admin] No admin settings file found, starting fresh")
554
- return
555
- }
556
- log.Error().Err(err).Msg("[Admin] Failed to read admin settings")
557
- return
558
- }
559
-
560
- var settings AdminSettings
561
- if err := json.Unmarshal(data, &settings); err != nil {
562
- log.Error().Err(err).Msg("[Admin] Failed to parse admin settings")
563
- return
564
- }
565
-
566
- lm := serv.GetLeaseManager()
567
-
568
- for _, leaseID := range settings.BannedLeases {
569
- lm.BanLease(leaseID)
570
- }
571
-
572
- for leaseID, bps := range settings.BPSLimits {
573
- bpsManager.SetBPSLimit(leaseID, bps)
574
- }
575
-
576
- // Load approval mode
577
- if settings.ApprovalMode != "" {
578
- setApprovalMode(settings.ApprovalMode)
579
- }
580
-
581
- // Load approved leases
582
- for _, leaseID := range settings.ApprovedLeases {
583
- approveLease(leaseID)
584
- }
585
-
586
- // Load denied leases
587
- for _, leaseID := range settings.DeniedLeases {
588
- denyLease(leaseID)
589
- }
590
-
591
- // Load banned IPs
592
- if ipManager != nil && len(settings.BannedIPs) > 0 {
593
- ipManager.SetBannedIPs(settings.BannedIPs)
594
- }
595
-
596
- log.Info().
597
- Int("banned_count", len(settings.BannedLeases)).
598
- Int("bps_limits_count", len(settings.BPSLimits)).
599
- Str("approval_mode", string(getApprovalMode())).
600
- Int("approved_count", len(settings.ApprovedLeases)).
601
- Int("denied_count", len(settings.DeniedLeases)).
602
- Int("banned_ips_count", len(settings.BannedIPs)).
603
- Msg("[Admin] Loaded admin settings")
604
-}
cmd/relay-server/bps_manager.go
deleted
-152
@@ -1,152 +0,0 @@
1
-package main
2
-
3
-import (
4
- "io"
5
- "sync"
6
-
7
- "github.com/hashicorp/yamux"
8
- "github.com/rs/zerolog/log"
9
- "gosuda.org/portal/cmd/relay-server/ratelimit"
10
-)
11
-
12
-// BPSManager manages per-lease bytes-per-second rate limiting
13
-type BPSManager struct {
14
- mu sync.Mutex
15
- bpsLimits map[string]int64 // leaseID -> bytes-per-second (0 = unlimited)
16
- bpsBuckets map[string]*ratelimit.Bucket // leaseID -> rate limit bucket
17
- defaultBPS int64 // default bytes-per-second for new leases
18
-}
19
-
20
-// NewBPSManager creates a new BPS manager
21
-func NewBPSManager() *BPSManager {
22
- return &BPSManager{
23
- bpsLimits: make(map[string]int64),
24
- bpsBuckets: make(map[string]*ratelimit.Bucket),
25
- defaultBPS: 0,
26
- }
27
-}
28
-
29
-// SetBPSLimit sets the BPS limit for a lease
30
-func (m *BPSManager) SetBPSLimit(leaseID string, bps int64) {
31
- m.mu.Lock()
32
- defer m.mu.Unlock()
33
- if bps <= 0 {
34
- delete(m.bpsLimits, leaseID)
35
- delete(m.bpsBuckets, leaseID)
36
- return
37
- }
38
- m.bpsLimits[leaseID] = bps
39
- // Reset bucket to apply new rate
40
- delete(m.bpsBuckets, leaseID)
41
-}
42
-
43
-// GetBPSLimit returns the BPS limit for a lease (0 = unlimited)
44
-func (m *BPSManager) GetBPSLimit(leaseID string) int64 {
45
- m.mu.Lock()
46
- defer m.mu.Unlock()
47
- if v, ok := m.bpsLimits[leaseID]; ok {
48
- return v
49
- }
50
- return 0
51
-}
52
-
53
-// GetAllBPSLimits returns a copy of all BPS limits
54
-func (m *BPSManager) GetAllBPSLimits() map[string]int64 {
55
- m.mu.Lock()
56
- defer m.mu.Unlock()
57
- result := make(map[string]int64, len(m.bpsLimits))
58
- for k, v := range m.bpsLimits {
59
- result[k] = v
60
- }
61
- return result
62
-}
63
-
64
-// SetDefaultBPS sets the default BPS limit for new leases
65
-func (m *BPSManager) SetDefaultBPS(bps int64) {
66
- m.mu.Lock()
67
- defer m.mu.Unlock()
68
- if bps < 0 {
69
- bps = 0
70
- }
71
- m.defaultBPS = bps
72
-}
73
-
74
-// GetDefaultBPS returns the default BPS limit
75
-func (m *BPSManager) GetDefaultBPS() int64 {
76
- m.mu.Lock()
77
- defer m.mu.Unlock()
78
- return m.defaultBPS
79
-}
80
-
81
-// GetBucket returns a rate limit bucket for a lease, creating one if needed
82
-func (m *BPSManager) GetBucket(leaseID string) *ratelimit.Bucket {
83
- m.mu.Lock()
84
- defer m.mu.Unlock()
85
-
86
- bps, ok := m.bpsLimits[leaseID]
87
- if !ok || bps <= 0 {
88
- return nil // No limit
89
- }
90
-
91
- if bucket, exists := m.bpsBuckets[leaseID]; exists {
92
- return bucket
93
- }
94
-
95
- // Create new bucket
96
- bucket := ratelimit.NewBucket(bps, bps)
97
- m.bpsBuckets[leaseID] = bucket
98
- log.Debug().
99
- Str("lease_id", leaseID).
100
- Int64("bps", bps).
101
- Msg("[BPS] Created rate limit bucket")
102
- return bucket
103
-}
104
-
105
-// CleanupLease removes BPS data for a lease
106
-func (m *BPSManager) CleanupLease(leaseID string) {
107
- m.mu.Lock()
108
- defer m.mu.Unlock()
109
- delete(m.bpsLimits, leaseID)
110
- delete(m.bpsBuckets, leaseID)
111
-}
112
-
113
-// Copy copies data with rate limiting
114
-func (m *BPSManager) Copy(dst io.Writer, src io.Reader, leaseID string) (int64, error) {
115
- bucket := m.GetBucket(leaseID)
116
- return ratelimit.Copy(dst, src, bucket)
117
-}
118
-
119
-// establishRelayWithBPS sets up bidirectional relay with BPS limiting.
120
-// Connection tracking is handled by RelayServer's event loop (cmdCheckAndIncLimit/cmdDecLimit).
121
-func establishRelayWithBPS(clientStream, leaseStream *yamux.Stream, leaseID string, bpsManager *BPSManager) {
122
- bpsLimit := bpsManager.GetBPSLimit(leaseID)
123
- log.Info().
124
- Str("lease_id", leaseID).
125
- Int64("bps_limit", bpsLimit).
126
- Msg("[Relay] Starting relay connection")
127
-
128
- defer func() {
129
- log.Info().
130
- Str("lease_id", leaseID).
131
- Msg("[Relay] Relay connection closed")
132
- }()
133
-
134
- var wg sync.WaitGroup
135
- wg.Add(2)
136
-
137
- // Client -> Lease
138
- go func() {
139
- defer wg.Done()
140
- bpsManager.Copy(leaseStream, clientStream, leaseID)
141
- leaseStream.Close()
142
- }()
143
-
144
- // Lease -> Client
145
- go func() {
146
- defer wg.Done()
147
- bpsManager.Copy(clientStream, leaseStream, leaseID)
148
- clientStream.Close()
149
- }()
150
-
151
- wg.Wait()
152
-}
cmd/relay-server/main.go
+12
-14
@@ -14,6 +14,7 @@ import (
14
"github.com/rs/zerolog"
15
"github.com/rs/zerolog/log"
16
17
+ "gosuda.org/portal/cmd/relay-server/manager"
18
"gosuda.org/portal/portal"
19
"gosuda.org/portal/sdk"
20
"gosuda.org/portal/utils"
@@ -83,32 +84,29 @@ func runServer() error {
84
serv.SetMaxRelayedPerLease(flagMaxLease)
85
}
86
86
- // Create BPS manager for rate limiting
87
- bpsManager := NewBPSManager()
88
- if flagLeaseBPS > 0 {
89
- bpsManager.SetDefaultBPS(int64(flagLeaseBPS))
90
- }
91
-
92
- // Create IP manager for IP-based bans
93
- ipManager := NewIPManager()
94
- globalIPManager = ipManager
87
+ // Create Admin instance for approvals/settings (also initializes managers)
88
+ admin := NewAdmin(int64(flagLeaseBPS))
89
90
// Load persisted admin settings (ban list, BPS limits, IP bans)
97
- loadAdminSettings(serv, bpsManager, ipManager)
91
+ admin.LoadSettings(serv)
92
93
// Register relay callback for BPS handling and IP tracking
94
serv.SetEstablishRelayCallback(func(clientStream, leaseStream *yamux.Stream, leaseID string) {
95
// Associate pending IP with this lease
102
- if ip := popPendingIP(); ip != "" && globalIPManager != nil {
103
- globalIPManager.RegisterLeaseIP(leaseID, ip)
96
+ ipManager := admin.GetIPManager()
97
+ if ipManager != nil {
98
+ if ip := ipManager.PopPendingIP(); ip != "" {
99
+ ipManager.RegisterLeaseIP(leaseID, ip)
100
+ }
101
}
105
- establishRelayWithBPS(clientStream, leaseStream, leaseID, bpsManager)
102
+ bpsManager := admin.GetBPSManager()
103
+ manager.EstablishRelayWithBPS(clientStream, leaseStream, leaseID, bpsManager)
104
})
105
106
serv.Start()
107
defer serv.Stop()
108
111
- httpSrv := serveHTTP(fmt.Sprintf(":%d", flagPort), serv, bpsManager, cred.ID(), flagBootstraps, flagNoIndex, stop)
109
+ httpSrv := serveHTTP(fmt.Sprintf(":%d", flagPort), serv, admin, flagNoIndex, stop)
110
111
<-ctx.Done()
112
log.Info().Msg("[server] shutting down...")
cmd/relay-server/manager/approve_manager.go
new
+99
@@ -0,0 +1,99 @@
1
+package manager
2
+
3
+import "sync"
4
+
5
+// ApprovalMode represents the approval mode for new connections.
6
+type ApprovalMode string
7
+
8
+const (
9
+ ApprovalModeAuto ApprovalMode = "auto"
10
+ ApprovalModeManual ApprovalMode = "manual"
11
+)
12
+
13
+// ApproveManager manages approval/denial state for leases.
14
+type ApproveManager struct {
15
+ mu sync.RWMutex
16
+ approvalMode ApprovalMode
17
+ approvedLeases map[string]struct{}
18
+ deniedLeases map[string]struct{}
19
+}
20
+
21
+func NewApproveManager() *ApproveManager {
22
+ return &ApproveManager{
23
+ approvalMode: ApprovalModeAuto,
24
+ approvedLeases: make(map[string]struct{}),
25
+ deniedLeases: make(map[string]struct{}),
26
+ }
27
+}
28
+
29
+func (m *ApproveManager) GetApprovalMode() ApprovalMode {
30
+ m.mu.RLock()
31
+ defer m.mu.RUnlock()
32
+ return m.approvalMode
33
+}
34
+
35
+func (m *ApproveManager) SetApprovalMode(mode ApprovalMode) {
36
+ m.mu.Lock()
37
+ defer m.mu.Unlock()
38
+ m.approvalMode = mode
39
+}
40
+
41
+func (m *ApproveManager) IsLeaseApproved(leaseID string) bool {
42
+ m.mu.RLock()
43
+ defer m.mu.RUnlock()
44
+ _, ok := m.approvedLeases[leaseID]
45
+ return ok
46
+}
47
+
48
+func (m *ApproveManager) ApproveLease(leaseID string) {
49
+ m.mu.Lock()
50
+ defer m.mu.Unlock()
51
+ m.approvedLeases[leaseID] = struct{}{}
52
+ delete(m.deniedLeases, leaseID)
53
+}
54
+
55
+func (m *ApproveManager) RevokeLease(leaseID string) {
56
+ m.mu.Lock()
57
+ defer m.mu.Unlock()
58
+ delete(m.approvedLeases, leaseID)
59
+}
60
+
61
+func (m *ApproveManager) GetApprovedLeases() []string {
62
+ m.mu.RLock()
63
+ defer m.mu.RUnlock()
64
+ result := make([]string, 0, len(m.approvedLeases))
65
+ for id := range m.approvedLeases {
66
+ result = append(result, id)
67
+ }
68
+ return result
69
+}
70
+
71
+func (m *ApproveManager) IsLeaseDenied(leaseID string) bool {
72
+ m.mu.RLock()
73
+ defer m.mu.RUnlock()
74
+ _, ok := m.deniedLeases[leaseID]
75
+ return ok
76
+}
77
+
78
+func (m *ApproveManager) DenyLease(leaseID string) {
79
+ m.mu.Lock()
80
+ defer m.mu.Unlock()
81
+ m.deniedLeases[leaseID] = struct{}{}
82
+ delete(m.approvedLeases, leaseID)
83
+}
84
+
85
+func (m *ApproveManager) UndenyLease(leaseID string) {
86
+ m.mu.Lock()
87
+ defer m.mu.Unlock()
88
+ delete(m.deniedLeases, leaseID)
89
+}
90
+
91
+func (m *ApproveManager) GetDeniedLeases() []string {
92
+ m.mu.RLock()
93
+ defer m.mu.RUnlock()
94
+ result := make([]string, 0, len(m.deniedLeases))
95
+ for id := range m.deniedLeases {
96
+ result = append(result, id)
97
+ }
98
+ return result
99
+}
cmd/relay-server/manager/bps_manager.go
renamed
+144
-1
@@ -1,4 +1,4 @@
1
-package ratelimit
1
+package manager
2
3
import (
4
"io"
@@ -6,9 +6,152 @@ import (
6
"sync/atomic"
7
"time"
8
9
+ "github.com/hashicorp/yamux"
10
"github.com/rs/zerolog/log"
11
)
12
13
+// BPSManager manages per-lease bytes-per-second rate limiting
14
+type BPSManager struct {
15
+ mu sync.Mutex
16
+ bpsLimits map[string]int64 // leaseID -> bytes-per-second (0 = unlimited)
17
+ bpsBuckets map[string]*Bucket // leaseID -> rate limit bucket
18
+ defaultBPS int64 // default bytes-per-second for new leases
19
+}
20
+
21
+// NewBPSManager creates a new BPS manager
22
+func NewBPSManager() *BPSManager {
23
+ return &BPSManager{
24
+ bpsLimits: make(map[string]int64),
25
+ bpsBuckets: make(map[string]*Bucket),
26
+ defaultBPS: 0,
27
+ }
28
+}
29
+
30
+// SetBPSLimit sets the BPS limit for a lease
31
+func (m *BPSManager) SetBPSLimit(leaseID string, bps int64) {
32
+ m.mu.Lock()
33
+ defer m.mu.Unlock()
34
+ if bps <= 0 {
35
+ delete(m.bpsLimits, leaseID)
36
+ delete(m.bpsBuckets, leaseID)
37
+ return
38
+ }
39
+ m.bpsLimits[leaseID] = bps
40
+ // Reset bucket to apply new rate
41
+ delete(m.bpsBuckets, leaseID)
42
+}
43
+
44
+// GetBPSLimit returns the BPS limit for a lease (0 = unlimited)
45
+func (m *BPSManager) GetBPSLimit(leaseID string) int64 {
46
+ m.mu.Lock()
47
+ defer m.mu.Unlock()
48
+ if v, ok := m.bpsLimits[leaseID]; ok {
49
+ return v
50
+ }
51
+ return 0
52
+}
53
+
54
+// GetAllBPSLimits returns a copy of all BPS limits
55
+func (m *BPSManager) GetAllBPSLimits() map[string]int64 {
56
+ m.mu.Lock()
57
+ defer m.mu.Unlock()
58
+ result := make(map[string]int64, len(m.bpsLimits))
59
+ for k, v := range m.bpsLimits {
60
+ result[k] = v
61
+ }
62
+ return result
63
+}
64
+
65
+// SetDefaultBPS sets the default BPS limit for new leases
66
+func (m *BPSManager) SetDefaultBPS(bps int64) {
67
+ m.mu.Lock()
68
+ defer m.mu.Unlock()
69
+ if bps < 0 {
70
+ bps = 0
71
+ }
72
+ m.defaultBPS = bps
73
+}
74
+
75
+// GetDefaultBPS returns the default BPS limit
76
+func (m *BPSManager) GetDefaultBPS() int64 {
77
+ m.mu.Lock()
78
+ defer m.mu.Unlock()
79
+ return m.defaultBPS
80
+}
81
+
82
+// GetBucket returns a rate limit bucket for a lease, creating one if needed
83
+func (m *BPSManager) GetBucket(leaseID string) *Bucket {
84
+ m.mu.Lock()
85
+ defer m.mu.Unlock()
86
+
87
+ bps, ok := m.bpsLimits[leaseID]
88
+ if !ok || bps <= 0 {
89
+ return nil // No limit
90
+ }
91
+
92
+ if bucket, exists := m.bpsBuckets[leaseID]; exists {
93
+ return bucket
94
+ }
95
+
96
+ // Create new bucket
97
+ bucket := NewBucket(bps, bps)
98
+ m.bpsBuckets[leaseID] = bucket
99
+ log.Debug().
100
+ Str("lease_id", leaseID).
101
+ Int64("bps", bps).
102
+ Msg("[BPS] Created rate limit bucket")
103
+ return bucket
104
+}
105
+
106
+// CleanupLease removes BPS data for a lease
107
+func (m *BPSManager) CleanupLease(leaseID string) {
108
+ m.mu.Lock()
109
+ defer m.mu.Unlock()
110
+ delete(m.bpsLimits, leaseID)
111
+ delete(m.bpsBuckets, leaseID)
112
+}
113
+
114
+// Copy copies data with rate limiting
115
+func (m *BPSManager) Copy(dst io.Writer, src io.Reader, leaseID string) (int64, error) {
116
+ bucket := m.GetBucket(leaseID)
117
+ return Copy(dst, src, bucket)
118
+}
119
+
120
+// EstablishRelayWithBPS sets up bidirectional relay with BPS limiting.
121
+// Connection tracking is handled by RelayServer's event loop (cmdCheckAndIncLimit/cmdDecLimit).
122
+func EstablishRelayWithBPS(clientStream, leaseStream *yamux.Stream, leaseID string, bpsManager *BPSManager) {
123
+ bpsLimit := bpsManager.GetBPSLimit(leaseID)
124
+ log.Info().
125
+ Str("lease_id", leaseID).
126
+ Int64("bps_limit", bpsLimit).
127
+ Msg("[Relay] Starting relay connection")
128
+
129
+ defer func() {
130
+ log.Info().
131
+ Str("lease_id", leaseID).
132
+ Msg("[Relay] Relay connection closed")
133
+ }()
134
+
135
+ var wg sync.WaitGroup
136
+ wg.Add(2)
137
+
138
+ // Client -> Lease
139
+ go func() {
140
+ defer wg.Done()
141
+ bpsManager.Copy(leaseStream, clientStream, leaseID)
142
+ leaseStream.Close()
143
+ }()
144
+
145
+ // Lease -> Client
146
+ go func() {
147
+ defer wg.Done()
148
+ bpsManager.Copy(clientStream, leaseStream, leaseID)
149
+ clientStream.Close()
150
+ }()
151
+
152
+ wg.Wait()
153
+}
154
+
155
// Bucket is a thread-safe rate limiter that supports multiple concurrent connections
156
// sharing the same bandwidth limit. It uses a token bucket algorithm where tokens
157
// represent bytes, and the bucket refills at the configured rate.
cmd/relay-server/manager/ip_manager.go
renamed
+27
-28
@@ -1,4 +1,4 @@
1
-package main
1
+package manager
2
3
import (
4
"net"
@@ -13,14 +13,20 @@ type IPManager struct {
13
bannedIPs map[string]struct{} // set of banned IPs
14
leaseToIP map[string]string // lease ID -> IP address
15
ipToLeases map[string][]string // IP -> list of lease IDs (for lookup)
16
+
17
+ // pendingIPs stores recent connection IPs in a circular buffer for lease association
18
+ pendingIPsMu sync.Mutex
19
+ pendingIPsQueue []string
20
+ pendingIPsMax int // Keep last N IPs
21
}
22
23
// NewIPManager creates a new IP manager
24
func NewIPManager() *IPManager {
25
return &IPManager{
21
- bannedIPs: make(map[string]struct{}),
22
- leaseToIP: make(map[string]string),
23
- ipToLeases: make(map[string][]string),
26
+ bannedIPs: make(map[string]struct{}),
27
+ leaseToIP: make(map[string]string),
28
+ ipToLeases: make(map[string][]string),
29
+ pendingIPsMax: 100,
30
}
31
}
32
@@ -135,34 +141,27 @@ func ExtractClientIP(r *http.Request) string {
141
return ip
142
}
143
138
-// Global IP manager instance
139
-var globalIPManager *IPManager
140
-
141
-// pendingIPs stores recent connection IPs in a circular buffer for lease association
142
-var (
143
- pendingIPsMu sync.Mutex
144
- pendingIPsQueue []string
145
- pendingIPsMax = 100 // Keep last 100 IPs
146
-)
147
-
148
-// storePendingIP stores a client IP for later association with a lease
149
-func storePendingIP(ip string) {
150
- pendingIPsMu.Lock()
151
- defer pendingIPsMu.Unlock()
152
- pendingIPsQueue = append(pendingIPsQueue, ip)
153
- if len(pendingIPsQueue) > pendingIPsMax {
154
- pendingIPsQueue = pendingIPsQueue[1:]
144
+// StorePendingIP stores a client IP for later association with a lease.
145
+func (m *IPManager) StorePendingIP(ip string) {
146
+ if ip == "" {
147
+ return
148
+ }
149
+ m.pendingIPsMu.Lock()
150
+ defer m.pendingIPsMu.Unlock()
151
+ m.pendingIPsQueue = append(m.pendingIPsQueue, ip)
152
+ if len(m.pendingIPsQueue) > m.pendingIPsMax {
153
+ m.pendingIPsQueue = m.pendingIPsQueue[1:]
154
}
155
}
156
158
-// popPendingIP retrieves and removes the oldest pending IP
159
-func popPendingIP() string {
160
- pendingIPsMu.Lock()
161
- defer pendingIPsMu.Unlock()
162
- if len(pendingIPsQueue) == 0 {
157
+// PopPendingIP retrieves and removes the oldest pending IP.
158
+func (m *IPManager) PopPendingIP() string {
159
+ m.pendingIPsMu.Lock()
160
+ defer m.pendingIPsMu.Unlock()
161
+ if len(m.pendingIPsQueue) == 0 {
162
return ""
163
}
165
- ip := pendingIPsQueue[0]
166
- pendingIPsQueue = pendingIPsQueue[1:]
164
+ ip := m.pendingIPsQueue[0]
165
+ m.pendingIPsQueue = m.pendingIPsQueue[1:]
166
return ip
167
}
cmd/relay-server/serve.go
+7
-7
@@ -43,7 +43,7 @@ func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
43
}
44
45
// servePortalHTMLWithSSR serves portal.html with SSR data injection
46
-func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer) {
46
+func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal.RelayServer, admin *Admin) {
47
utils.SetCORSHeaders(w)
48
49
// Initialize cache on first use
@@ -59,7 +59,7 @@ func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal
59
}
60
61
// Inject SSR data into cached template
62
- injectedHTML := injectServerData(string(cachedPortalHTML), serv)
62
+ injectedHTML := injectServerData(string(cachedPortalHTML), serv, admin)
63
64
// Set headers
65
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -73,9 +73,9 @@ func servePortalHTMLWithSSR(w http.ResponseWriter, r *http.Request, serv *portal
73
}
74
75
// injectServerData injects server data into HTML for SSR
76
-func injectServerData(htmlContent string, serv *portal.RelayServer) string {
76
+func injectServerData(htmlContent string, serv *portal.RelayServer, admin *Admin) string {
77
// Get server data from lease manager
78
- rows := convertLeaseEntriesToRows(serv)
78
+ rows := convertLeaseEntriesToRows(serv, admin)
79
80
// Marshal to JSON
81
jsonData, err := json.Marshal(rows)
@@ -116,7 +116,7 @@ func servePortalStaticFile(w http.ResponseWriter, r *http.Request, filePath stri
116
117
// serveAppStatic serves static files for app UI (React app) from embedded FS
118
// Falls back to portal.html with SSR when path is root or file not found
119
-func serveAppStatic(w http.ResponseWriter, r *http.Request, appPath string, serv *portal.RelayServer) {
119
+func serveAppStatic(w http.ResponseWriter, r *http.Request, appPath string, serv *portal.RelayServer, admin *Admin) {
120
// Prevent directory traversal
121
if strings.Contains(appPath, "..") {
122
http.Error(w, "Invalid path", http.StatusBadRequest)
@@ -127,7 +127,7 @@ func serveAppStatic(w http.ResponseWriter, r *http.Request, appPath string, serv
127
128
// If path is empty or "/", serve portal.html with SSR
129
if appPath == "" || appPath == "/" {
130
- servePortalHTMLWithSSR(w, r, serv)
130
+ servePortalHTMLWithSSR(w, r, serv, admin)
131
return
132
}
133
@@ -137,7 +137,7 @@ func serveAppStatic(w http.ResponseWriter, r *http.Request, appPath string, serv
137
if err != nil {
138
// File not found - fallback to portal.html with SSR for SPA routing
139
log.Debug().Err(err).Str("path", appPath).Msg("app static file not found, falling back to SSR")
140
- servePortalHTMLWithSSR(w, r, serv)
140
+ servePortalHTMLWithSSR(w, r, serv, admin)
141
return
142
}
143
cmd/relay-server/view.go
+18
-15
@@ -11,6 +11,7 @@ import (
11
12
"github.com/rs/zerolog/log"
13
14
+ "gosuda.org/portal/cmd/relay-server/manager"
15
"gosuda.org/portal/portal"
16
"gosuda.org/portal/sdk"
17
"gosuda.org/portal/utils"
@@ -19,12 +20,8 @@ import (
20
//go:embed dist/*
21
var distFS embed.FS
22
22
-// Package-level BPS manager reference for admin handlers
23
-var globalBPSManager *BPSManager
24
-
23
// serveHTTP builds the HTTP mux and returns the server.
26
-func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, nodeID string, bootstraps []string, noIndex bool, cancel context.CancelFunc) *http.Server {
27
- globalBPSManager = bpsManager
24
+func serveHTTP(addr string, serv *portal.RelayServer, admin *Admin, noIndex bool, cancel context.CancelFunc) *http.Server {
25
if addr == "" {
26
addr = ":0"
27
}
@@ -58,7 +55,7 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
55
return
56
}
57
p := strings.TrimPrefix(r.URL.Path, "/app/")
61
- serveAppStatic(w, r, p, serv)
58
+ serveAppStatic(w, r, p, serv, admin)
59
})
60
61
// Portal frontend files (for unified caching)
@@ -93,8 +90,9 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
90
}
91
92
// Check if IP is banned
96
- clientIP := ExtractClientIP(r)
97
- if globalIPManager != nil && globalIPManager.IsIPBanned(clientIP) {
93
+ clientIP := manager.ExtractClientIP(r)
94
+ ipManager := admin.GetIPManager()
95
+ if ipManager != nil && ipManager.IsIPBanned(clientIP) {
96
log.Warn().Str("ip", clientIP).Msg("[server] connection rejected: IP banned")
97
http.Error(w, "forbidden", http.StatusForbidden)
98
return
@@ -107,8 +105,8 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
105
}
106
107
// Store pending IP for lease association (will be linked when lease is registered)
110
- if globalIPManager != nil && clientIP != "" {
111
- storePendingIP(clientIP)
108
+ if ipManager != nil && clientIP != "" {
109
+ ipManager.StorePendingIP(clientIP)
110
}
111
112
if err := serv.HandleConnection(stream); err != nil {
@@ -122,7 +120,7 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
120
appMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
121
// serveAppStatic handles both "/" and 404 fallback with SSR
122
p := strings.TrimPrefix(r.URL.Path, "/")
125
- serveAppStatic(w, r, p, serv)
123
+ serveAppStatic(w, r, p, serv, admin)
124
})
125
126
appMux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
@@ -132,7 +130,7 @@ func serveHTTP(addr string, serv *portal.RelayServer, bpsManager *BPSManager, no
130
131
// Admin API
132
appMux.HandleFunc("/admin/", func(w http.ResponseWriter, r *http.Request) {
135
- handleAdminRequest(w, r, serv)
133
+ admin.HandleAdminRequest(w, r, serv)
134
})
135
136
// Create portal frontend mux (routes only)
@@ -222,7 +220,7 @@ type leaseRow struct {
220
}
221
222
// convertLeaseEntriesToRows converts LeaseEntry data from LeaseManager to leaseRow format for the app page
225
-func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
223
+func convertLeaseEntriesToRows(serv *portal.RelayServer, admin *Admin) []leaseRow {
224
// Get all lease entries directly from the lease manager
225
leaseEntries := serv.GetAllLeaseEntries()
226
@@ -255,7 +253,8 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
253
}
254
255
// Skip unapproved leases in manual mode for user-facing list
258
- if getApprovalMode() == ApprovalModeManual && !isLeaseApproved(identityID) {
256
+ approveManager := admin.GetApproveManager()
257
+ if approveManager.GetApprovalMode() == manager.ApprovalModeManual && !approveManager.IsLeaseApproved(identityID) {
258
continue
259
}
260
@@ -339,7 +338,11 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
338
link := fmt.Sprintf("//%s.%s/", lease.Name, utils.StripWildCard(utils.StripScheme(base)))
339
340
// Get BPS limit for this lease from BPSManager
342
- bps := globalBPSManager.GetBPSLimit(identityID)
341
+ var bps int64
342
+ bpsMgr := admin.GetBPSManager()
343
+ if bpsMgr != nil {
344
+ bps = bpsMgr.GetBPSLimit(identityID)
345
+ }
346
347
row := leaseRow{
348
Peer: identityID,
utils/utils.go
+2
-23
@@ -307,7 +307,7 @@ func IsLocalhost(r *http.Request) bool {
307
// Try resolving hostnames to IPs (best-effort).
308
if addrs, err := net.LookupIP(host); err == nil {
309
for _, a := range addrs {
310
- if a.IsLoopback() || isPrivateOrDockerIP(a) {
310
+ if a.IsLoopback() || a.IsPrivate() {
311
return true
312
}
313
}
@@ -315,26 +315,5 @@ func IsLocalhost(r *http.Request) bool {
315
return false
316
}
317
318
- return ip.IsLoopback() || isPrivateOrDockerIP(ip)
319
-}
320
-
321
-func isPrivateOrDockerIP(ip net.IP) bool {
322
- // Common Docker bridge/Desktop ranges only.
323
- ranges := []string{
324
- "172.16.0.0/12", // Docker user-defined bridges (default pool)
325
- "172.17.0.0/16", // Linux Docker default bridge
326
- "192.168.64.0/24", // Docker Desktop macOS/Windows
327
- "192.168.65.0/24",
328
- }
329
-
330
- for _, cidr := range ranges {
331
- _, subnet, err := net.ParseCIDR(cidr)
332
- if err != nil || subnet == nil {
333
- continue
334
- }
335
- if subnet.Contains(ip) {
336
- return true
337
- }
338
- }
339
- return false
318
+ return ip.IsLoopback() || ip.IsPrivate()
319
}