Files
Wormhole/internal/proxy/manager.go
T
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

342 lines
8.2 KiB
Go

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