Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+525
@@ -0,0 +1,525 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/wormhole/wormhole/internal/store"
|
||||
)
|
||||
|
||||
type OnlineAgent struct {
|
||||
ClientID uint
|
||||
Version string
|
||||
ConnectedAt time.Time
|
||||
LastPing time.Time
|
||||
}
|
||||
|
||||
type CompiledRule struct {
|
||||
Rule store.IPRule
|
||||
CIDR *net.IPNet
|
||||
Regex *regexp.Regexp
|
||||
ExactIP net.IP
|
||||
}
|
||||
|
||||
type FlowDelta struct {
|
||||
Inlet int64
|
||||
Export int64
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
clients map[uint]*store.Client
|
||||
clientsByKey map[string]*store.Client
|
||||
tunnels map[uint]*store.Tunnel
|
||||
tunnelsByPort map[string]*store.Tunnel
|
||||
hosts map[uint]*store.Host
|
||||
hostIndex []hostEntry
|
||||
|
||||
onlineAgents map[uint]*OnlineAgent
|
||||
|
||||
ipRules []CompiledRule
|
||||
authPolicies map[string]*store.AuthPolicy
|
||||
visitorBindings map[string][]uint
|
||||
|
||||
flowCounters map[string]*FlowDelta
|
||||
}
|
||||
|
||||
type hostEntry struct {
|
||||
host *store.Host
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
clients: make(map[uint]*store.Client),
|
||||
clientsByKey: make(map[string]*store.Client),
|
||||
tunnels: make(map[uint]*store.Tunnel),
|
||||
tunnelsByPort: make(map[string]*store.Tunnel),
|
||||
hosts: make(map[uint]*store.Host),
|
||||
onlineAgents: make(map[uint]*OnlineAgent),
|
||||
authPolicies: make(map[string]*store.AuthPolicy),
|
||||
visitorBindings: make(map[string][]uint),
|
||||
flowCounters: make(map[string]*FlowDelta),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) ReloadAll(st *store.Store) error {
|
||||
clients, err := st.ListClients(0, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tunnels, err := st.ListTunnels(0, 0, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hosts, err := st.ListHosts(0, 0, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rules, err := st.ListIPRules("", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies, err := st.ListAuthPolicies("", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
visitors, err := st.ListAllResourceVisitors()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.clients = make(map[uint]*store.Client)
|
||||
m.clientsByKey = make(map[string]*store.Client)
|
||||
for i := range clients {
|
||||
c := clients[i]
|
||||
m.clients[c.ID] = &c
|
||||
m.clientsByKey[c.VerifyKey] = &c
|
||||
}
|
||||
|
||||
m.tunnels = make(map[uint]*store.Tunnel)
|
||||
m.tunnelsByPort = make(map[string]*store.Tunnel)
|
||||
for i := range tunnels {
|
||||
t := tunnels[i]
|
||||
m.tunnels[t.ID] = &t
|
||||
key := tunnelPortKey(t.ListenIP, t.ListenPort)
|
||||
m.tunnelsByPort[key] = &t
|
||||
}
|
||||
|
||||
m.hosts = make(map[uint]*store.Host)
|
||||
m.hostIndex = nil
|
||||
for i := range hosts {
|
||||
h := hosts[i]
|
||||
m.hosts[h.ID] = &h
|
||||
m.hostIndex = append(m.hostIndex, hostEntry{host: &h})
|
||||
}
|
||||
|
||||
m.ipRules = compileRules(rules)
|
||||
|
||||
m.authPolicies = make(map[string]*store.AuthPolicy)
|
||||
for i := range policies {
|
||||
p := policies[i]
|
||||
key := authPolicyKey(p.ResourceType, p.ResourceID)
|
||||
m.authPolicies[key] = &p
|
||||
}
|
||||
|
||||
m.visitorBindings = make(map[string][]uint)
|
||||
for i := range visitors {
|
||||
v := visitors[i]
|
||||
key := authPolicyKey(v.ResourceType, v.ResourceID)
|
||||
m.visitorBindings[key] = append(m.visitorBindings[key], v.UserID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tunnelPortKey(ip string, port int) string {
|
||||
if ip == "" {
|
||||
ip = "0.0.0.0"
|
||||
}
|
||||
return ip + ":" + strconv.Itoa(port)
|
||||
}
|
||||
|
||||
func authPolicyKey(resourceType string, resourceID uint) string {
|
||||
return resourceType + ":" + itoa(resourceID)
|
||||
}
|
||||
|
||||
func itoa(n uint) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [20]byte
|
||||
i := len(b)
|
||||
for n > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
|
||||
func compileRules(rules []store.IPRule) []CompiledRule {
|
||||
out := make([]CompiledRule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
cr := CompiledRule{Rule: r}
|
||||
switch r.PatternKind {
|
||||
case "cidr":
|
||||
_, network, err := net.ParseCIDR(r.Pattern)
|
||||
if err == nil {
|
||||
cr.CIDR = network
|
||||
}
|
||||
case "regex":
|
||||
re, err := regexp.Compile(r.Pattern)
|
||||
if err == nil {
|
||||
cr.Regex = re
|
||||
}
|
||||
default:
|
||||
cr.ExactIP = net.ParseIP(r.Pattern)
|
||||
}
|
||||
out = append(out, cr)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) SetClientOnline(clientID uint, version string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
now := time.Now()
|
||||
m.onlineAgents[clientID] = &OnlineAgent{
|
||||
ClientID: clientID,
|
||||
Version: version,
|
||||
ConnectedAt: now,
|
||||
LastPing: now,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) SetClientOffline(clientID uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.onlineAgents, clientID)
|
||||
}
|
||||
|
||||
func (m *Manager) TouchAgent(clientID uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if a, ok := m.onlineAgents[clientID]; ok {
|
||||
a.LastPing = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) AgentLastPing(clientID uint) (time.Time, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
a, ok := m.onlineAgents[clientID]
|
||||
if !ok {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return a.LastPing, true
|
||||
}
|
||||
|
||||
func (m *Manager) IsOnline(clientID uint) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
_, ok := m.onlineAgents[clientID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *Manager) OnlineCount() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.onlineAgents)
|
||||
}
|
||||
|
||||
func (m *Manager) GetClientByKey(key string) (*store.Client, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.clientsByKey[key]
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func (m *Manager) GetClient(id uint) (*store.Client, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
c, ok := m.clients[id]
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func (m *Manager) GetTunnel(id uint) (*store.Tunnel, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
t, ok := m.tunnels[id]
|
||||
return t, ok
|
||||
}
|
||||
|
||||
func (m *Manager) GetHost(id uint) (*store.Host, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
h, ok := m.hosts[id]
|
||||
return h, ok
|
||||
}
|
||||
|
||||
func (m *Manager) MatchHost(hostHeader, path, scheme string) *store.Host {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
hostHeader = strings.ToLower(strings.Split(hostHeader, ":")[0])
|
||||
var best *store.Host
|
||||
bestLen := -1
|
||||
|
||||
for _, entry := range m.hostIndex {
|
||||
h := entry.host
|
||||
if !h.Status {
|
||||
continue
|
||||
}
|
||||
if h.Scheme != "all" && h.Scheme != scheme {
|
||||
continue
|
||||
}
|
||||
if !matchHostPattern(h.Host, hostHeader) {
|
||||
continue
|
||||
}
|
||||
loc := h.Location
|
||||
if loc == "" {
|
||||
loc = "/"
|
||||
}
|
||||
if !strings.HasPrefix(path, loc) {
|
||||
continue
|
||||
}
|
||||
if len(loc) > bestLen {
|
||||
best = h
|
||||
bestLen = len(loc)
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
func matchHostPattern(pattern, host string) bool {
|
||||
pattern = strings.ToLower(pattern)
|
||||
if pattern == host {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(pattern, "*.") {
|
||||
suffix := pattern[1:]
|
||||
return strings.HasSuffix(host, suffix) || host == pattern[2:]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) ReloadIPRules(st *store.Store) error {
|
||||
rules, err := st.ListIPRules("", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.ipRules = compileRules(rules)
|
||||
m.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) AllowIP(ipStr string, scopes []Scope) bool {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
return false
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var denyMatched, allowMatched bool
|
||||
hasAllowRules := false
|
||||
|
||||
for _, cr := range m.ipRules {
|
||||
if !scopeMatch(cr.Rule, scopes) {
|
||||
continue
|
||||
}
|
||||
if !ruleMatch(cr, ip, ipStr) {
|
||||
continue
|
||||
}
|
||||
if cr.Rule.Type == "deny" {
|
||||
denyMatched = true
|
||||
}
|
||||
if cr.Rule.Type == "allow" {
|
||||
allowMatched = true
|
||||
hasAllowRules = true
|
||||
}
|
||||
}
|
||||
|
||||
if denyMatched {
|
||||
return false
|
||||
}
|
||||
if hasAllowRules {
|
||||
return allowMatched
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type Scope struct {
|
||||
Kind string
|
||||
ResourceID uint
|
||||
}
|
||||
|
||||
func scopeMatch(rule store.IPRule, scopes []Scope) bool {
|
||||
if rule.Scope == "global" {
|
||||
return true
|
||||
}
|
||||
for _, s := range scopes {
|
||||
if rule.Scope == s.Kind && rule.ResourceID == s.ResourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ruleMatch(cr CompiledRule, ip net.IP, ipStr string) bool {
|
||||
switch cr.Rule.PatternKind {
|
||||
case "cidr":
|
||||
return cr.CIDR != nil && cr.CIDR.Contains(ip)
|
||||
case "regex":
|
||||
return cr.Regex != nil && cr.Regex.MatchString(ipStr)
|
||||
default:
|
||||
return cr.ExactIP != nil && cr.ExactIP.Equal(ip)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetAuthPolicy(resourceType string, resourceID uint) (*store.AuthPolicy, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
p, ok := m.authPolicies[authPolicyKey(resourceType, resourceID)]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (m *Manager) GetResourceVisitorIDs(resourceType string, resourceID uint) ([]uint, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
ids, ok := m.visitorBindings[authPolicyKey(resourceType, resourceID)]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out := make([]uint, len(ids))
|
||||
copy(out, ids)
|
||||
return out, true
|
||||
}
|
||||
|
||||
func (m *Manager) SetResourceVisitors(resourceType string, resourceID uint, userIDs []uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
key := authPolicyKey(resourceType, resourceID)
|
||||
if len(userIDs) == 0 {
|
||||
delete(m.visitorBindings, key)
|
||||
return
|
||||
}
|
||||
copied := make([]uint, len(userIDs))
|
||||
copy(copied, userIDs)
|
||||
m.visitorBindings[key] = copied
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteResourceVisitors(resourceType string, resourceID uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.visitorBindings, authPolicyKey(resourceType, resourceID))
|
||||
}
|
||||
|
||||
func (m *Manager) AddFlow(resourceType string, resourceID uint, inlet, export int64) {
|
||||
key := resourceType + ":" + itoa(resourceID)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delta, ok := m.flowCounters[key]
|
||||
if !ok {
|
||||
delta = &FlowDelta{}
|
||||
m.flowCounters[key] = delta
|
||||
}
|
||||
atomic.AddInt64(&delta.Inlet, inlet)
|
||||
atomic.AddInt64(&delta.Export, export)
|
||||
}
|
||||
|
||||
func (m *Manager) DrainFlow() map[string]FlowDelta {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make(map[string]FlowDelta, len(m.flowCounters))
|
||||
for k, v := range m.flowCounters {
|
||||
inlet := atomic.SwapInt64(&v.Inlet, 0)
|
||||
export := atomic.SwapInt64(&v.Export, 0)
|
||||
if inlet > 0 || export > 0 {
|
||||
out[k] = FlowDelta{Inlet: inlet, Export: export}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) UpsertClient(c *store.Client) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.clients[c.ID] = c
|
||||
m.clientsByKey[c.VerifyKey] = c
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteClient(id uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if c, ok := m.clients[id]; ok {
|
||||
delete(m.clientsByKey, c.VerifyKey)
|
||||
}
|
||||
delete(m.clients, id)
|
||||
delete(m.onlineAgents, id)
|
||||
}
|
||||
|
||||
func (m *Manager) UpsertTunnel(t *store.Tunnel) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.tunnels[t.ID] = t
|
||||
m.tunnelsByPort[tunnelPortKey(t.ListenIP, t.ListenPort)] = t
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteTunnel(id uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if t, ok := m.tunnels[id]; ok {
|
||||
delete(m.tunnelsByPort, tunnelPortKey(t.ListenIP, t.ListenPort))
|
||||
}
|
||||
delete(m.tunnels, id)
|
||||
}
|
||||
|
||||
func (m *Manager) UpsertHost(h *store.Host) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.hosts[h.ID] = h
|
||||
m.hostIndex = nil
|
||||
for _, host := range m.hosts {
|
||||
m.hostIndex = append(m.hostIndex, hostEntry{host: host})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) DeleteHost(id uint) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.hosts, id)
|
||||
m.hostIndex = nil
|
||||
for _, host := range m.hosts {
|
||||
m.hostIndex = append(m.hostIndex, hostEntry{host: host})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) ListEnabledTunnels() []*store.Tunnel {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []*store.Tunnel
|
||||
for _, t := range m.tunnels {
|
||||
if t.Status {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) SetTunnelRunStatus(id uint, running bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if t, ok := m.tunnels[id]; ok {
|
||||
t.RunStatus = running
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user