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 }