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>
342 lines
8.2 KiB
Go
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)
|
|
}
|