chore(release): 发布 v1.9.2
CI / verify (push) Canceled after 0s
Docker Release / build-and-push (amd64, linux/amd64) (push) Canceled after 0s
Docker Release / build-and-push (arm64, linux/arm64) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Docker Release / create-manifest (push) Canceled after 0s
CI / verify (push) Canceled after 0s
Docker Release / build-and-push (amd64, linux/amd64) (push) Canceled after 0s
Docker Release / build-and-push (arm64, linux/arm64) (push) Canceled after 0s
GitHub Release / release (push) Canceled after 0s
Docker Release / create-manifest (push) Canceled after 0s
- 「公网服务端」新增 Nginx 启停控制(Agent 0.15.2,停止带二次确认) - frps 启动失败不再误报成功,并增强启动诊断 - frps vhost 端口禁止 80/443,避免与 VPS 上 Nginx 冲突 - 旧服务端配置的 tcp_mux / login_fail_exit 默认值补齐 - 新增安全审查、页面布局方案与场景操作文档
This commit is contained in:
+138
-25
@@ -2,6 +2,7 @@ package frp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -14,25 +15,31 @@ import (
|
||||
const agentRemoteAddr = "127.0.0.1:7700"
|
||||
|
||||
const (
|
||||
agentTunnelKeepalive = 30 * time.Second
|
||||
agentTunnelBackoffMin = time.Second
|
||||
agentTunnelBackoffMax = 60 * time.Second
|
||||
agentTunnelKeepalive = 30 * time.Second
|
||||
agentTunnelKeepaliveTimeout = 5 * time.Second
|
||||
agentTunnelBackoffMin = time.Second
|
||||
agentTunnelBackoffMax = 60 * time.Second
|
||||
)
|
||||
|
||||
type agentTunnel struct {
|
||||
serverID int64
|
||||
localPort int
|
||||
ssh SSHInstaller
|
||||
serverID int64
|
||||
localPort int
|
||||
remoteAddr string
|
||||
ssh SSHInstaller
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
connectMu sync.Mutex
|
||||
mu sync.Mutex
|
||||
listener net.Listener
|
||||
client *ssh.Client
|
||||
state string
|
||||
lastError string
|
||||
wg sync.WaitGroup
|
||||
|
||||
connsMu sync.Mutex
|
||||
conns map[net.Conn]struct{}
|
||||
}
|
||||
|
||||
func newAgentTunnel(parent context.Context, serverID int64, localPort int, installer SSHInstaller) *agentTunnel {
|
||||
@@ -42,13 +49,15 @@ func newAgentTunnel(parent context.Context, serverID int64, localPort int, insta
|
||||
func newAgentTunnelWithListener(parent context.Context, serverID int64, localPort int, installer SSHInstaller, listener net.Listener) *agentTunnel {
|
||||
ctx, cancel := context.WithCancel(parent)
|
||||
return &agentTunnel{
|
||||
serverID: serverID,
|
||||
localPort: localPort,
|
||||
ssh: installer,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
listener: listener,
|
||||
state: "connecting",
|
||||
serverID: serverID,
|
||||
localPort: localPort,
|
||||
remoteAddr: agentRemoteAddr,
|
||||
ssh: installer,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
listener: listener,
|
||||
state: "connecting",
|
||||
conns: map[net.Conn]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +70,13 @@ func (t *agentTunnel) Start() error {
|
||||
}
|
||||
t.listener = listener
|
||||
}
|
||||
if t.localPort == 0 {
|
||||
if addr, ok := t.listener.Addr().(*net.TCPAddr); ok {
|
||||
t.localPort = addr.Port
|
||||
}
|
||||
}
|
||||
t.mu.Lock()
|
||||
t.state = "connected"
|
||||
t.state = "connecting"
|
||||
t.lastError = ""
|
||||
t.mu.Unlock()
|
||||
t.wg.Add(2)
|
||||
@@ -76,7 +90,7 @@ func (t *agentTunnel) monitorLoop() {
|
||||
defer t.wg.Done()
|
||||
backoff := agentTunnelBackoffMin
|
||||
for {
|
||||
_, err := t.ensureClient()
|
||||
err := t.check()
|
||||
wait := agentTunnelKeepalive
|
||||
if err != nil {
|
||||
t.setState("reconnecting", err)
|
||||
@@ -92,6 +106,24 @@ func (t *agentTunnel) monitorLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (t *agentTunnel) sleep(d time.Duration) bool {
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
@@ -126,8 +158,13 @@ func (t *agentTunnel) acceptLoop() {
|
||||
return
|
||||
}
|
||||
t.wg.Add(1)
|
||||
if !t.trackConn(conn) {
|
||||
t.wg.Done()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
defer t.untrackConn(conn)
|
||||
t.handle(conn)
|
||||
}()
|
||||
}
|
||||
@@ -138,31 +175,53 @@ func (t *agentTunnel) handle(local net.Conn) {
|
||||
client, err := t.ensureClient()
|
||||
if err != nil {
|
||||
t.setState("reconnecting", err)
|
||||
writeTunnelError(local, err)
|
||||
return
|
||||
}
|
||||
remote, err := client.Dial("tcp", agentRemoteAddr)
|
||||
remote, err := client.Dial("tcp", t.remoteAddr)
|
||||
if err != nil {
|
||||
t.invalidateClient(client)
|
||||
t.setState("reconnecting", err)
|
||||
return
|
||||
remote, err = t.retryRemoteDial(client, err)
|
||||
if err != nil {
|
||||
t.setState("reconnecting", err)
|
||||
writeTunnelError(local, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer remote.Close()
|
||||
t.setState("connected", nil)
|
||||
proxyTunnel(local, remote)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (t *agentTunnel) ensureClient() (*ssh.Client, error) {
|
||||
// 单飞:监控循环和请求处理可能同时触发建连,串行化后只允许一条 SSH 握手。
|
||||
// 正常请求只复用连接,不在热路径做 keepalive,避免多个接口互相排队。
|
||||
t.connectMu.Lock()
|
||||
defer t.connectMu.Unlock()
|
||||
|
||||
t.mu.Lock()
|
||||
if t.client != nil {
|
||||
client := t.client
|
||||
t.mu.Unlock()
|
||||
if _, _, err := client.SendRequest("keepalive@openssh.com", true, nil); err == nil {
|
||||
return client, nil
|
||||
}
|
||||
t.invalidateClient(client)
|
||||
} else {
|
||||
t.mu.Unlock()
|
||||
return client, nil
|
||||
}
|
||||
t.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(t.ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
client, err := t.ssh.client(ctx)
|
||||
@@ -181,6 +240,30 @@ func (t *agentTunnel) ensureClient() (*ssh.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (t *agentTunnel) invalidateClient(client *ssh.Client) {
|
||||
t.mu.Lock()
|
||||
if t.client == client {
|
||||
@@ -217,9 +300,39 @@ func (t *agentTunnel) Stop() {
|
||||
_ = t.client.Close()
|
||||
}
|
||||
t.mu.Unlock()
|
||||
t.closeConns()
|
||||
t.wg.Wait()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func proxyTunnel(a, b net.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
Reference in New Issue
Block a user