Files
rose_cat707 0d84b07f68
CI / docker (push) Successful in 1m58s
feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。
Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 17:52:43 +08:00

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
}
}