0d84b07f68
CI / docker (push) Successful in 1m58s
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
526 lines
11 KiB
Go
526 lines
11 KiB
Go
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
|
|
}
|
|
}
|