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>
230 lines
5.0 KiB
Go
230 lines
5.0 KiB
Go
package agent
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/xtaci/smux"
|
|
"github.com/wormhole/wormhole/internal/bridge"
|
|
"github.com/wormhole/wormhole/internal/protocol"
|
|
"github.com/wormhole/wormhole/internal/version"
|
|
)
|
|
|
|
func Run(args []string) {
|
|
fs := flag.NewFlagSet("agent", flag.ExitOnError)
|
|
serverAddr := fs.String("server", "127.0.0.1:8528", "wormhole server bridge address")
|
|
verifyKey := fs.String("key", "", "client verify key")
|
|
useTLS := fs.Bool("tls", false, "use TLS for bridge connection")
|
|
fs.Parse(args)
|
|
|
|
if *verifyKey == "" {
|
|
log.Fatal("verify key required: -key")
|
|
}
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
backoff := time.Second
|
|
for {
|
|
select {
|
|
case <-sig:
|
|
log.Println("[agent] shutting down")
|
|
return
|
|
default:
|
|
}
|
|
|
|
shutdown, hadSession := runSession(*serverAddr, *verifyKey, *useTLS, sig)
|
|
if shutdown {
|
|
return
|
|
}
|
|
if hadSession {
|
|
backoff = time.Second
|
|
}
|
|
|
|
log.Printf("[agent] reconnecting in %v...", backoff)
|
|
select {
|
|
case <-sig:
|
|
log.Println("[agent] shutting down")
|
|
return
|
|
case <-time.After(backoff):
|
|
}
|
|
if backoff < 30*time.Second {
|
|
backoff *= 2
|
|
}
|
|
}
|
|
}
|
|
|
|
func dialBridge(addr string, useTLS bool) (net.Conn, error) {
|
|
var conn net.Conn
|
|
var err error
|
|
if useTLS {
|
|
conn, err = tls.Dial("tcp", addr, &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
InsecureSkipVerify: true,
|
|
})
|
|
} else {
|
|
conn, err = net.Dial("tcp", addr)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bridge.SetTCPKeepAlive(conn)
|
|
return conn, nil
|
|
}
|
|
|
|
// runSession connects and blocks until disconnect.
|
|
// Returns shutdown=true when SIGINT/SIGTERM received; hadSession=true if registration succeeded.
|
|
func runSession(serverAddr, verifyKey string, useTLS bool, sig chan os.Signal) (shutdown, hadSession bool) {
|
|
conn, err := dialBridge(serverAddr, useTLS)
|
|
if err != nil {
|
|
log.Printf("[agent] dial server: %v", err)
|
|
return false, false
|
|
}
|
|
defer conn.Close()
|
|
|
|
if err := protocol.WriteMessage(conn, protocol.MsgRegister, protocol.RegisterPayload{
|
|
VerifyKey: verifyKey,
|
|
Version: version.Version,
|
|
}); err != nil {
|
|
log.Printf("[agent] register: %v", err)
|
|
return false, false
|
|
}
|
|
msgType, body, err := protocol.ReadMessage(conn)
|
|
if err != nil {
|
|
log.Printf("[agent] register ack: %v", err)
|
|
return false, false
|
|
}
|
|
if msgType != protocol.MsgRegisterAck {
|
|
log.Printf("[agent] unexpected register response")
|
|
return false, false
|
|
}
|
|
var ack protocol.RegisterAckPayload
|
|
if len(body) > 0 {
|
|
if err := json.Unmarshal(body, &ack); err != nil {
|
|
log.Printf("[agent] parse ack: %v", err)
|
|
return false, false
|
|
}
|
|
}
|
|
if !ack.OK {
|
|
log.Printf("[agent] register failed: %s", ack.Message)
|
|
return false, false
|
|
}
|
|
log.Printf("[agent] registered as client %d", ack.ClientID)
|
|
hadSession = true
|
|
|
|
session, err := smux.Server(conn, bridge.SmuxConfig())
|
|
if err != nil {
|
|
log.Printf("[agent] smux: %v", err)
|
|
return false, hadSession
|
|
}
|
|
defer session.Close()
|
|
|
|
sessionDead := make(chan string, 1)
|
|
go func() {
|
|
reason := heartbeat(session)
|
|
sessionDead <- reason
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
stream, err := session.AcceptStream()
|
|
if err != nil {
|
|
select {
|
|
case sessionDead <- "accept stream: " + err.Error():
|
|
default:
|
|
}
|
|
return
|
|
}
|
|
go handleStream(stream)
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-sig:
|
|
return true, hadSession
|
|
case reason := <-sessionDead:
|
|
log.Printf("[agent] session closed (%s)", reason)
|
|
return false, hadSession
|
|
}
|
|
}
|
|
|
|
const heartbeatMaxFailures = 3
|
|
|
|
func heartbeat(session *smux.Session) string {
|
|
ticker := time.NewTicker(bridge.HeartbeatInterval)
|
|
defer ticker.Stop()
|
|
failures := 0
|
|
for range ticker.C {
|
|
if err := pingOnce(session); err != nil {
|
|
failures++
|
|
log.Printf("[agent] heartbeat failed (%d/%d): %v", failures, heartbeatMaxFailures, err)
|
|
if failures >= heartbeatMaxFailures {
|
|
return "heartbeat timeout"
|
|
}
|
|
continue
|
|
}
|
|
failures = 0
|
|
}
|
|
return "heartbeat stopped"
|
|
}
|
|
|
|
func pingOnce(session *smux.Session) error {
|
|
stream, err := session.OpenStream()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stream.Close()
|
|
if err := protocol.WriteMessage(stream, protocol.MsgPing, nil); err != nil {
|
|
return err
|
|
}
|
|
msgType, _, err := protocol.ReadMessage(stream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if msgType != protocol.MsgPong {
|
|
return fmt.Errorf("unexpected response type %d", msgType)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleStream(stream *smux.Stream) {
|
|
defer stream.Close()
|
|
msgType, body, err := protocol.ReadMessage(stream)
|
|
if err != nil {
|
|
return
|
|
}
|
|
switch msgType {
|
|
case protocol.MsgPing:
|
|
_ = protocol.WriteMessage(stream, protocol.MsgPong, nil)
|
|
return
|
|
case protocol.MsgOpenStream:
|
|
handleDataStream(stream, body)
|
|
}
|
|
}
|
|
|
|
func handleDataStream(stream *smux.Stream, body []byte) {
|
|
meta, err := protocol.ParseLinkMeta(body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
target, err := net.Dial("tcp", meta.Target)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer target.Close()
|
|
_ = protocol.WriteMessage(stream, protocol.MsgStreamReady, nil)
|
|
go func() {
|
|
_, _ = io.Copy(target, stream)
|
|
}()
|
|
_, _ = io.Copy(stream, target)
|
|
}
|