PortRelay:把内网服务临时、可控地开放给同事

边缘网关 + Agent 的端口中继:Agent 主动连出到网关,把内网目标挂到网关端口上;
通过邀请链接按设备授权,可选审批,首次连接起计时,到期、闲置自动收回。

- portrelay-server:边缘网关(端口池、白名单与授权租约、邀请与授权网页)
- portrelay-agent:Agent(yamux 隧道、本地 API 与网页面板、流量统计)
- mac-native:SwiftUI 菜单栏 App
This commit is contained in:
DeepFal committed 2026-10-13 16:33:17 +08:00
commit 86e159b3f0
67 files changed
+13888

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
package acl
import (
"fmt"
"net"
"strings"
"sync"
"time"
)
// Whitelist checks if an IP is allowed based on rules and dynamic leases
type Whitelist struct {
rules []string
allowAll bool
ips map[string]bool
subnets []*net.IPNet
dynamic map[string]time.Time // dynamic IP -> expiration
mu sync.RWMutex
}
// NewWhitelist parses a list of rules
func NewWhitelist(rules []string) *Whitelist {
w := &Whitelist{
rules: rules,
ips: make(map[string]bool),
subnets: make([]*net.IPNet, 0),
dynamic: make(map[string]time.Time),
}
for _, r := range rules {
r = strings.TrimSpace(r)
if r == "" {
continue
}
if r == "*" || r == "0.0.0.0/0" || r == "::/0" {
w.allowAll = true
continue
}
if strings.Contains(r, "/") {
_, ipNet, err := net.ParseCIDR(r)
if err == nil {
w.subnets = append(w.subnets, ipNet)
}
} else {
ip := net.ParseIP(r)
if ip != nil {
w.ips[ip.String()] = true
}
}
}
return w
}
// ValidateRule reports whether a whitelist rule is "*", a valid IP or a valid CIDR
func ValidateRule(r string) error {
r = strings.TrimSpace(r)
switch {
case r == "":
return fmt.Errorf("白名单规则不能为空")
case r == "*":
return nil
case strings.Contains(r, "/"):
if _, _, err := net.ParseCIDR(r); err != nil {
return fmt.Errorf("无效的网段 %q", r)
}
default:
if net.ParseIP(r) == nil {
return fmt.Errorf("无效的 IP 地址 %q", r)
}
}
return nil
}
// DynamicLeases returns a copy of unexpired dynamic leases
func (w *Whitelist) DynamicLeases() map[string]time.Time {
w.mu.RLock()
defer w.mu.RUnlock()
now := time.Now()
out := make(map[string]time.Time, len(w.dynamic))
for ip, exp := range w.dynamic {
if now.Before(exp) {
out[ip] = exp
}
}
return out
}
// RestoreDynamic merges previously granted leases (e.g. after a config update)
func (w *Whitelist) RestoreDynamic(leases map[string]time.Time) {
w.mu.Lock()
defer w.mu.Unlock()
for ip, exp := range leases {
if cur, ok := w.dynamic[ip]; !ok || exp.After(cur) {
w.dynamic[ip] = exp
}
}
}
// RemoveDynamic revokes the lease for ip (all leases when ip is empty) and returns how many were removed
func (w *Whitelist) RemoveDynamic(ip string) int {
w.mu.Lock()
defer w.mu.Unlock()
if ip == "" {
n := len(w.dynamic)
w.dynamic = make(map[string]time.Time)
return n
}
if parsed := net.ParseIP(strings.TrimSpace(ip)); parsed != nil {
if _, ok := w.dynamic[parsed.String()]; ok {
delete(w.dynamic, parsed.String())
return 1
}
}
return 0
}
// AddDynamic adds an IP with an expiration timestamp
func (w *Whitelist) AddDynamic(ipStr string, expireAt time.Time) {
w.mu.Lock()
defer w.mu.Unlock()
host := ipStr
if h, _, err := net.SplitHostPort(ipStr); err == nil {
host = h
}
parsed := net.ParseIP(host)
if parsed != nil {
w.dynamic[parsed.String()] = expireAt
}
}
// GetDynamic returns active unexpired dynamic IPs
func (w *Whitelist) GetDynamic() []string {
w.mu.Lock()
defer w.mu.Unlock()
now := time.Now()
var list []string
for ip, exp := range w.dynamic {
if now.Before(exp) {
list = append(list, ip)
} else {
delete(w.dynamic, ip)
}
}
return list
}
// IsAllowed checks if remoteAddr (either "IP" or "IP:Port") is permitted
func (w *Whitelist) IsAllowed(remoteAddr string) (bool, string) {
if w.allowAll {
return true, ""
}
host := remoteAddr
if h, _, err := net.SplitHostPort(remoteAddr); err == nil {
host = h
}
parsedIP := net.ParseIP(host)
if parsedIP == nil {
return false, "invalid client IP"
}
ipStr := parsedIP.String()
// Canonical IP string check
if w.ips[ipStr] {
return true, ""
}
// Dynamic leases check
w.mu.RLock()
if exp, ok := w.dynamic[ipStr]; ok {
if time.Now().Before(exp) {
w.mu.RUnlock()
return true, ""
}
}
w.mu.RUnlock()
// CIDR subnets check
for _, subnet := range w.subnets {
if subnet.Contains(parsedIP) {
return true, ""
}
}
return false, "IP not in whitelist"
}
// Rules returns the raw rules
func (w *Whitelist) Rules() []string {
return w.rules
}