Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wormhole/wormhole/internal/auth"
|
||||
"github.com/wormhole/wormhole/internal/bridge"
|
||||
"github.com/wormhole/wormhole/internal/cache"
|
||||
"github.com/wormhole/wormhole/internal/metrics"
|
||||
"github.com/wormhole/wormhole/internal/protocol"
|
||||
"github.com/wormhole/wormhole/internal/security"
|
||||
"github.com/wormhole/wormhole/internal/store"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
cache *cache.Manager
|
||||
store *store.Store
|
||||
bridge *bridge.Bridge
|
||||
security *security.Service
|
||||
resAuth *auth.ResourceAuth
|
||||
metrics *metrics.Collector
|
||||
|
||||
mu sync.Mutex
|
||||
services map[uint]Service
|
||||
httpProxySrv *http.Server
|
||||
httpsProxySrv *http.Server
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
ID() uint
|
||||
}
|
||||
|
||||
func NewManager(c *cache.Manager, st *store.Store, b *bridge.Bridge, sec *security.Service, ra *auth.ResourceAuth, mc *metrics.Collector) *Manager {
|
||||
return &Manager{
|
||||
cache: c,
|
||||
store: st,
|
||||
bridge: b,
|
||||
security: sec,
|
||||
resAuth: ra,
|
||||
metrics: mc,
|
||||
services: make(map[uint]Service),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) StartAll() error {
|
||||
tunnels := m.cache.ListEnabledTunnels()
|
||||
for _, t := range tunnels {
|
||||
if err := m.StartTunnel(t.ID); err != nil {
|
||||
log.Printf("[proxy] start tunnel %d: %v", t.ID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) StartTunnel(id uint) error {
|
||||
t, ok := m.cache.GetTunnel(id)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if old, exists := m.services[id]; exists {
|
||||
_ = old.Stop()
|
||||
delete(m.services, id)
|
||||
}
|
||||
var svc Service
|
||||
switch t.Mode {
|
||||
case "udp":
|
||||
svc = newUDPServer(m, t)
|
||||
default:
|
||||
if t.VisitorAuthEnabled {
|
||||
svc = newHTTPTunnelServer(m, t)
|
||||
} else {
|
||||
svc = newTCPServer(m, t)
|
||||
}
|
||||
}
|
||||
if err := svc.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
m.services[id] = svc
|
||||
t.RunStatus = true
|
||||
_ = m.store.UpdateTunnel(t)
|
||||
m.cache.SetTunnelRunStatus(id, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) StopTunnel(id uint) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if svc, ok := m.services[id]; ok {
|
||||
_ = svc.Stop()
|
||||
delete(m.services, id)
|
||||
}
|
||||
if t, ok := m.cache.GetTunnel(id); ok {
|
||||
t.RunStatus = false
|
||||
_ = m.store.UpdateTunnel(t)
|
||||
m.cache.SetTunnelRunStatus(id, false)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) StartHTTPServer(httpPort, httpsPort int, certFile, keyFile string) error {
|
||||
if httpPort > 0 {
|
||||
srv := &http.Server{
|
||||
Addr: formatPort(httpPort),
|
||||
Handler: m.httpHandler("http"),
|
||||
}
|
||||
m.httpProxySrv = srv
|
||||
go func() {
|
||||
log.Printf("[proxy] http host server on %s", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[proxy] http server error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if httpsPort > 0 {
|
||||
srv := &http.Server{
|
||||
Addr: formatPort(httpsPort),
|
||||
Handler: m.httpHandler("https"),
|
||||
}
|
||||
m.httpsProxySrv = srv
|
||||
go func() {
|
||||
log.Printf("[proxy] https host server on %s", srv.Addr)
|
||||
if certFile != "" && keyFile != "" {
|
||||
if err := srv.ListenAndServeTLS(certFile, keyFile); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("[proxy] https server error: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[proxy] https port enabled but tls cert/key not configured, skipping TLS listener")
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Shutdown(ctx context.Context) error {
|
||||
m.mu.Lock()
|
||||
services := make([]Service, 0, len(m.services))
|
||||
for _, svc := range m.services {
|
||||
services = append(services, svc)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
for _, svc := range services {
|
||||
_ = svc.Stop()
|
||||
}
|
||||
var err error
|
||||
if m.httpProxySrv != nil {
|
||||
if e := m.httpProxySrv.Shutdown(ctx); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
if m.httpsProxySrv != nil {
|
||||
if e := m.httpsProxySrv.Shutdown(ctx); e != nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func formatPort(port int) string {
|
||||
return net.JoinHostPort("", itoa(port))
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [12]byte
|
||||
i := len(b)
|
||||
for n > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
|
||||
func (m *Manager) httpHandler(scheme string) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
directIP := auth.DirectRemoteIP(r)
|
||||
|
||||
host := m.cache.MatchHost(r.Host, r.URL.Path, scheme)
|
||||
if host == nil {
|
||||
http.Error(w, "host not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ip := auth.ExtractRemoteIP(r)
|
||||
m.metrics.RecordRequestIP("host", host.ID, ip, directIP)
|
||||
if !m.security.Allow(directIP, host.ClientID, "host", host.ID) {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !m.resAuth.CheckHTTP(w, r, "host", host.ID) {
|
||||
return
|
||||
}
|
||||
if host.AutoHTTPS && scheme == "http" {
|
||||
target := "https://" + r.Host + r.URL.RequestURI()
|
||||
http.Redirect(w, r, target, http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
m.proxyHTTP(w, r, host, "host")
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) proxyHTTP(w http.ResponseWriter, r *http.Request, host *store.Host, resourceType string) {
|
||||
targetURL, err := url.Parse("http://" + host.TargetAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad target", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
if !m.bridge.IsOnline(host.ClientID) {
|
||||
http.Error(w, "client offline", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
mode, headers := resolveProxyConfig(m.store, host.ClientID, host.IPForwardMode, host.CustomHeaders)
|
||||
forwardIP := forwardIPForHTTP(r, mode)
|
||||
meta := &protocol.LinkMeta{
|
||||
ConnType: protocol.ConnTypeHTTP,
|
||||
Target: host.TargetAddr,
|
||||
RemoteAddr: forwardIP,
|
||||
}
|
||||
stream, err := m.bridge.OpenStream(host.ClientID, meta)
|
||||
if err != nil {
|
||||
http.Error(w, "tunnel error", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return stream, nil
|
||||
},
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
||||
proxy.Transport = transport
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
req.Host = targetURL.Host
|
||||
req.URL.Host = targetURL.Host
|
||||
req.URL.Scheme = targetURL.Scheme
|
||||
applyOutboundHeaders(req, r, mode, headers)
|
||||
}
|
||||
proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, e error) {
|
||||
http.Error(rw, "proxy error", http.StatusBadGateway)
|
||||
}
|
||||
if isWebSocketUpgrade(r) {
|
||||
proxy.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
rec := &responseRecorder{ResponseWriter: w, status: 200}
|
||||
proxy.ServeHTTP(rec, r)
|
||||
inlet := int64(rec.written)
|
||||
export := int64(r.ContentLength)
|
||||
if export < 0 {
|
||||
export = 0
|
||||
}
|
||||
m.metrics.AddFlow(resourceType, host.ID, host.ClientID, inlet, export)
|
||||
}
|
||||
|
||||
func (m *Manager) proxyTunnelHTTP(w http.ResponseWriter, r *http.Request, t *store.Tunnel) {
|
||||
host := &store.Host{
|
||||
ID: t.ID,
|
||||
ClientID: t.ClientID,
|
||||
TargetAddr: t.TargetAddr,
|
||||
IPForwardMode: t.IPForwardMode,
|
||||
CustomHeaders: t.CustomHeaders,
|
||||
}
|
||||
m.proxyHTTP(w, r, host, "tunnel")
|
||||
}
|
||||
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
written int
|
||||
}
|
||||
|
||||
func (r *responseRecorder) Write(b []byte) (int, error) {
|
||||
n, err := r.ResponseWriter.Write(b)
|
||||
r.written += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if h, ok := r.ResponseWriter.(http.Hijacker); ok {
|
||||
return h.Hijack()
|
||||
}
|
||||
return nil, nil, fmt.Errorf("hijack not supported")
|
||||
}
|
||||
|
||||
func (r *responseRecorder) Flush() {
|
||||
if f, ok := r.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func isWebSocketUpgrade(r *http.Request) bool {
|
||||
return strings.EqualFold(r.Header.Get("Upgrade"), "websocket")
|
||||
}
|
||||
|
||||
func (m *Manager) handleTCPConnection(t *store.Tunnel, clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
ip := auth.AddrHost(clientConn.RemoteAddr())
|
||||
m.metrics.RecordRequestIP("tunnel", t.ID, ip, ip)
|
||||
if !m.security.Allow(ip, t.ClientID, "tunnel", t.ID) {
|
||||
return
|
||||
}
|
||||
if !m.resAuth.CheckTCP(clientConn.RemoteAddr(), "tunnel", t.ID) {
|
||||
return
|
||||
}
|
||||
if !m.bridge.IsOnline(t.ClientID) {
|
||||
return
|
||||
}
|
||||
mode, _ := resolveProxyConfig(m.store, t.ClientID, t.IPForwardMode, t.CustomHeaders)
|
||||
meta := &protocol.LinkMeta{
|
||||
ConnType: protocol.ConnTypeTCP,
|
||||
Target: t.TargetAddr,
|
||||
RemoteAddr: forwardIPForTCP(clientConn.RemoteAddr().String(), mode),
|
||||
}
|
||||
stream, err := m.bridge.OpenStream(t.ClientID, meta)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
inlet, export := bridge.CopyBoth(clientConn, stream)
|
||||
m.metrics.AddFlow("tunnel", t.ID, t.ClientID, inlet, export)
|
||||
}
|
||||
Reference in New Issue
Block a user