2026-09-28 13:42:21 +08:00
|
|
|
package frp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-10-07 19:26:02 +08:00
|
|
|
"encoding/json"
|
2026-09-28 13:42:21 +08:00
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const agentRemoteAddr = "127.0.0.1:7700"
|
|
|
|
|
|
2026-09-28 15:19:51 +08:00
|
|
|
const (
|
2026-10-07 19:26:02 +08:00
|
|
|
agentTunnelKeepalive = 30 * time.Second
|
|
|
|
|
agentTunnelKeepaliveTimeout = 5 * time.Second
|
|
|
|
|
agentTunnelBackoffMin = time.Second
|
|
|
|
|
agentTunnelBackoffMax = 60 * time.Second
|
2026-09-28 15:19:51 +08:00
|
|
|
)
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
type agentTunnel struct {
|
2026-10-07 19:26:02 +08:00
|
|
|
serverID int64
|
|
|
|
|
localPort int
|
|
|
|
|
remoteAddr string
|
|
|
|
|
ssh SSHInstaller
|
2026-09-28 13:42:21 +08:00
|
|
|
|
|
|
|
|
ctx context.Context
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
|
2026-10-07 19:26:02 +08:00
|
|
|
connectMu sync.Mutex
|
2026-09-28 13:42:21 +08:00
|
|
|
mu sync.Mutex
|
|
|
|
|
listener net.Listener
|
|
|
|
|
client *ssh.Client
|
|
|
|
|
state string
|
|
|
|
|
lastError string
|
|
|
|
|
wg sync.WaitGroup
|
2026-10-07 19:26:02 +08:00
|
|
|
|
|
|
|
|
connsMu sync.Mutex
|
|
|
|
|
conns map[net.Conn]struct{}
|
2026-09-28 13:42:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newAgentTunnel(parent context.Context, serverID int64, localPort int, installer SSHInstaller) *agentTunnel {
|
2026-09-28 14:13:57 +08:00
|
|
|
return newAgentTunnelWithListener(parent, serverID, localPort, installer, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newAgentTunnelWithListener(parent context.Context, serverID int64, localPort int, installer SSHInstaller, listener net.Listener) *agentTunnel {
|
2026-09-28 13:42:21 +08:00
|
|
|
ctx, cancel := context.WithCancel(parent)
|
|
|
|
|
return &agentTunnel{
|
2026-10-07 19:26:02 +08:00
|
|
|
serverID: serverID,
|
|
|
|
|
localPort: localPort,
|
|
|
|
|
remoteAddr: agentRemoteAddr,
|
|
|
|
|
ssh: installer,
|
|
|
|
|
ctx: ctx,
|
|
|
|
|
cancel: cancel,
|
|
|
|
|
listener: listener,
|
|
|
|
|
state: "connecting",
|
|
|
|
|
conns: map[net.Conn]struct{}{},
|
2026-09-28 13:42:21 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) Start() error {
|
2026-09-28 14:13:57 +08:00
|
|
|
if t.listener == nil {
|
|
|
|
|
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", t.localPort))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.setState("failed", err)
|
|
|
|
|
return fmt.Errorf("监听隧道本地端口失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
t.listener = listener
|
2026-09-28 13:42:21 +08:00
|
|
|
}
|
2026-10-07 19:26:02 +08:00
|
|
|
if t.localPort == 0 {
|
|
|
|
|
if addr, ok := t.listener.Addr().(*net.TCPAddr); ok {
|
|
|
|
|
t.localPort = addr.Port
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-28 13:42:21 +08:00
|
|
|
t.mu.Lock()
|
2026-10-07 19:26:02 +08:00
|
|
|
t.state = "connecting"
|
2026-09-28 13:42:21 +08:00
|
|
|
t.lastError = ""
|
|
|
|
|
t.mu.Unlock()
|
2026-09-28 15:19:51 +08:00
|
|
|
t.wg.Add(2)
|
2026-09-28 13:42:21 +08:00
|
|
|
go t.acceptLoop()
|
2026-09-28 15:19:51 +08:00
|
|
|
go t.monitorLoop()
|
2026-09-28 13:42:21 +08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 15:19:51 +08:00
|
|
|
// monitorLoop 主动保活并在断开后按指数退避重建 SSH 连接,避免只在有流量时才尝试重连。
|
|
|
|
|
func (t *agentTunnel) monitorLoop() {
|
|
|
|
|
defer t.wg.Done()
|
|
|
|
|
backoff := agentTunnelBackoffMin
|
|
|
|
|
for {
|
2026-10-07 19:26:02 +08:00
|
|
|
err := t.check()
|
2026-09-28 15:19:51 +08:00
|
|
|
wait := agentTunnelKeepalive
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.setState("reconnecting", err)
|
|
|
|
|
backoff = nextTunnelBackoff(backoff)
|
|
|
|
|
wait = backoff
|
|
|
|
|
} else {
|
|
|
|
|
t.setState("connected", nil)
|
|
|
|
|
backoff = agentTunnelBackoffMin
|
|
|
|
|
}
|
|
|
|
|
if !t.sleep(wait) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 19:26:02 +08:00
|
|
|
// check 同时验证 SSH 连接和远端 Agent 端口,避免只连上 SSH 就误判隧道可用。
|
|
|
|
|
func (t *agentTunnel) check() error {
|
|
|
|
|
client, err := t.ensureClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := t.keepalive(client); err != nil {
|
|
|
|
|
t.invalidateClient(client)
|
|
|
|
|
return fmt.Errorf("SSH keepalive 失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
remote, err := client.Dial("tcp", t.remoteAddr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("连接远端 Agent 端口失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
_ = remote.Close()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 15:19:51 +08:00
|
|
|
func (t *agentTunnel) sleep(d time.Duration) bool {
|
|
|
|
|
timer := time.NewTimer(d)
|
|
|
|
|
defer timer.Stop()
|
|
|
|
|
select {
|
|
|
|
|
case <-t.ctx.Done():
|
|
|
|
|
return false
|
|
|
|
|
case <-timer.C:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func nextTunnelBackoff(current time.Duration) time.Duration {
|
|
|
|
|
if current < agentTunnelBackoffMin {
|
|
|
|
|
return agentTunnelBackoffMin
|
|
|
|
|
}
|
|
|
|
|
next := current * 2
|
|
|
|
|
if next > agentTunnelBackoffMax {
|
|
|
|
|
return agentTunnelBackoffMax
|
|
|
|
|
}
|
|
|
|
|
return next
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
func (t *agentTunnel) acceptLoop() {
|
|
|
|
|
defer t.wg.Done()
|
|
|
|
|
for {
|
|
|
|
|
conn, err := t.listener.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if t.ctx.Err() != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
t.setState("failed", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
t.wg.Add(1)
|
2026-10-07 19:26:02 +08:00
|
|
|
if !t.trackConn(conn) {
|
|
|
|
|
t.wg.Done()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-28 13:42:21 +08:00
|
|
|
go func() {
|
|
|
|
|
defer t.wg.Done()
|
2026-10-07 19:26:02 +08:00
|
|
|
defer t.untrackConn(conn)
|
2026-09-28 13:42:21 +08:00
|
|
|
t.handle(conn)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) handle(local net.Conn) {
|
|
|
|
|
defer local.Close()
|
|
|
|
|
client, err := t.ensureClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.setState("reconnecting", err)
|
2026-10-07 19:26:02 +08:00
|
|
|
writeTunnelError(local, err)
|
2026-09-28 13:42:21 +08:00
|
|
|
return
|
|
|
|
|
}
|
2026-10-07 19:26:02 +08:00
|
|
|
remote, err := client.Dial("tcp", t.remoteAddr)
|
2026-09-28 13:42:21 +08:00
|
|
|
if err != nil {
|
2026-10-07 19:26:02 +08:00
|
|
|
remote, err = t.retryRemoteDial(client, err)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.setState("reconnecting", err)
|
|
|
|
|
writeTunnelError(local, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-28 13:42:21 +08:00
|
|
|
}
|
|
|
|
|
defer remote.Close()
|
|
|
|
|
t.setState("connected", nil)
|
|
|
|
|
proxyTunnel(local, remote)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 19:26:02 +08:00
|
|
|
// retryRemoteDial 仅在 SSH 连接本身失效时重连,远端 Agent 端口暂时不可用不会关闭共享连接。
|
|
|
|
|
func (t *agentTunnel) retryRemoteDial(client *ssh.Client, firstErr error) (net.Conn, error) {
|
|
|
|
|
if err := t.keepalive(client); err == nil {
|
|
|
|
|
return nil, fmt.Errorf("连接远端 Agent 端口失败: %w", firstErr)
|
|
|
|
|
}
|
|
|
|
|
t.invalidateClient(client)
|
|
|
|
|
reconnected, err := t.ensureClient()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("SSH 连接失效,重连失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
remote, err := reconnected.Dial("tcp", t.remoteAddr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("SSH 已重连,但远端 Agent 端口仍不可用: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return remote, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
func (t *agentTunnel) ensureClient() (*ssh.Client, error) {
|
2026-10-07 19:26:02 +08:00
|
|
|
// 单飞:监控循环和请求处理可能同时触发建连,串行化后只允许一条 SSH 握手。
|
|
|
|
|
// 正常请求只复用连接,不在热路径做 keepalive,避免多个接口互相排队。
|
|
|
|
|
t.connectMu.Lock()
|
|
|
|
|
defer t.connectMu.Unlock()
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
t.mu.Lock()
|
|
|
|
|
if t.client != nil {
|
|
|
|
|
client := t.client
|
|
|
|
|
t.mu.Unlock()
|
2026-10-07 19:26:02 +08:00
|
|
|
return client, nil
|
2026-09-28 13:42:21 +08:00
|
|
|
}
|
2026-10-07 19:26:02 +08:00
|
|
|
t.mu.Unlock()
|
2026-09-28 13:42:21 +08:00
|
|
|
ctx, cancel := context.WithTimeout(t.ctx, 15*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
client, err := t.ssh.client(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
if t.client != nil {
|
|
|
|
|
existing := t.client
|
|
|
|
|
t.mu.Unlock()
|
|
|
|
|
_ = client.Close()
|
|
|
|
|
return existing, nil
|
|
|
|
|
}
|
|
|
|
|
t.client = client
|
|
|
|
|
t.mu.Unlock()
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 19:26:02 +08:00
|
|
|
func (t *agentTunnel) keepalive(client *ssh.Client) error {
|
|
|
|
|
done := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
_, _, err := client.SendRequest("keepalive@openssh.com", true, nil)
|
|
|
|
|
done <- err
|
|
|
|
|
}()
|
|
|
|
|
select {
|
|
|
|
|
case err := <-done:
|
|
|
|
|
return err
|
|
|
|
|
case <-time.After(agentTunnelKeepaliveTimeout):
|
|
|
|
|
return fmt.Errorf("SSH keepalive 超时")
|
|
|
|
|
case <-t.ctx.Done():
|
|
|
|
|
return t.ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// writeTunnelError 让本地 HTTP 客户端立即得到明确错误,而不是一直等到 context deadline。
|
|
|
|
|
func writeTunnelError(conn net.Conn, err error) {
|
|
|
|
|
body, _ := json.Marshal(map[string]string{"error": "SSH 隧道不可用: " + err.Error()})
|
|
|
|
|
_, _ = fmt.Fprintf(conn,
|
|
|
|
|
"HTTP/1.1 502 Bad Gateway\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s",
|
|
|
|
|
len(body), body)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
func (t *agentTunnel) invalidateClient(client *ssh.Client) {
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
if t.client == client {
|
|
|
|
|
t.client = nil
|
|
|
|
|
}
|
|
|
|
|
t.mu.Unlock()
|
|
|
|
|
_ = client.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) State() (string, string) {
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
defer t.mu.Unlock()
|
|
|
|
|
return t.state, t.lastError
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) setState(state string, err error) {
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
defer t.mu.Unlock()
|
|
|
|
|
t.state = state
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.lastError = ""
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
t.lastError = err.Error()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) Stop() {
|
|
|
|
|
t.cancel()
|
|
|
|
|
t.mu.Lock()
|
|
|
|
|
if t.listener != nil {
|
|
|
|
|
_ = t.listener.Close()
|
|
|
|
|
}
|
|
|
|
|
if t.client != nil {
|
|
|
|
|
_ = t.client.Close()
|
|
|
|
|
}
|
|
|
|
|
t.mu.Unlock()
|
2026-10-07 19:26:02 +08:00
|
|
|
t.closeConns()
|
2026-09-28 13:42:21 +08:00
|
|
|
t.wg.Wait()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 19:26:02 +08:00
|
|
|
func (t *agentTunnel) trackConn(conn net.Conn) bool {
|
|
|
|
|
t.connsMu.Lock()
|
|
|
|
|
defer t.connsMu.Unlock()
|
|
|
|
|
if t.ctx.Err() != nil {
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
t.conns[conn] = struct{}{}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) untrackConn(conn net.Conn) {
|
|
|
|
|
t.connsMu.Lock()
|
|
|
|
|
delete(t.conns, conn)
|
|
|
|
|
t.connsMu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (t *agentTunnel) closeConns() {
|
|
|
|
|
t.connsMu.Lock()
|
|
|
|
|
conns := make([]net.Conn, 0, len(t.conns))
|
|
|
|
|
for conn := range t.conns {
|
|
|
|
|
conns = append(conns, conn)
|
|
|
|
|
}
|
|
|
|
|
t.connsMu.Unlock()
|
|
|
|
|
for _, conn := range conns {
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-28 13:42:21 +08:00
|
|
|
func proxyTunnel(a, b net.Conn) {
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
wg.Add(2)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
_, _ = io.Copy(a, b)
|
|
|
|
|
_ = a.Close()
|
|
|
|
|
_ = b.Close()
|
|
|
|
|
}()
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
_, _ = io.Copy(b, a)
|
|
|
|
|
_ = a.Close()
|
|
|
|
|
_ = b.Close()
|
|
|
|
|
}()
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}
|