Files
曾志威 d3edfea03f feat: add independent TCP forwarding and gateway operations improvements
Support target-free SOCKS5 port mappings with reference protection and UI workflows.

Include connection-stage diagnostics, SSH fingerprint confirmation, asset impact checks, listener traffic visibility, SQLite snapshots, account mapping improvements, and MySQL discovery fixes.

Update feature rationale and validation docs; Go checks, frontend build and 121 browser tests passed.
2026-09-22 15:42:58 +08:00

120 lines
2.9 KiB
Go

package gateway
import (
"context"
"crypto/x509"
"fmt"
"net"
"sort"
"time"
)
type ConnectionInfo struct {
RXBytes int64 `json:"rxBytes"`
TXBytes int64 `json:"txBytes"`
ID string `json:"id"`
GatewayID string `json:"gatewayId"`
Protocol string `json:"protocol"`
Username string `json:"username"`
Source string `json:"source"`
StartedAt time.Time `json:"startedAt"`
DurationSeconds float64 `json:"durationSeconds"`
Authenticated bool `json:"authenticated"`
TargetAccountID string `json:"targetAccountId"`
TargetID string `json:"targetId"`
TargetHost string `json:"targetHost"`
TargetPort int `json:"targetPort"`
Disconnecting bool `json:"disconnecting"`
}
// Count at the client socket boundary: RX is client -> gateway; TX is gateway
// -> client. Do not expose ReaderFrom/WriterTo, which could bypass accounting.
type trafficConn struct {
net.Conn
record func(rx, tx int)
}
func (c *trafficConn) Read(p []byte) (int, error) {
n, err := c.Conn.Read(p)
if n > 0 {
c.record(n, 0)
}
return n, err
}
func (c *trafficConn) Write(p []byte) (int, error) {
n, err := c.Conn.Write(p)
if n > 0 {
c.record(0, n)
}
return n, err
}
func (c *trafficConn) CloseWrite() error {
if half, ok := c.Conn.(interface{ CloseWrite() error }); ok {
return half.CloseWrite()
}
return c.Conn.Close()
}
type connectionState struct {
ConnectionInfo
conn net.Conn
cancel context.CancelFunc
}
func (m *Manager) Connections() []ConnectionInfo {
m.mu.Lock()
defer m.mu.Unlock()
result := make([]ConnectionInfo, 0, len(m.connections))
for _, state := range m.connections {
info := state.ConnectionInfo
info.DurationSeconds = time.Since(info.StartedAt).Seconds()
result = append(result, info)
}
sort.Slice(result, func(i, j int) bool { return result[i].StartedAt.Before(result[j].StartedAt) })
return result
}
// Disconnect closes only the selected connection, cancelling its upstream work.
func (m *Manager) Disconnect(id string) (ConnectionInfo, bool) {
m.mu.Lock()
state, ok := m.connections[id]
if !ok {
m.mu.Unlock()
return ConnectionInfo{}, false
}
state.Disconnecting = true
info := state.ConnectionInfo
m.mu.Unlock()
state.cancel()
state.conn.Close()
return info, true
}
func (m *Manager) OnShutdown(hook func()) {
m.mu.Lock()
defer m.mu.Unlock()
m.shutdownHooks = append(m.shutdownHooks, hook)
}
// CertificateExpirations covers certificates configured on the gateway listener.
func (m *Manager) CertificateExpirations() map[string]time.Time {
m.mu.Lock()
defer m.mu.Unlock()
result := map[string]time.Time{}
if m.tls == nil {
return result
}
for i, cert := range m.tls.Certificates {
leaf := cert.Leaf
if leaf == nil && len(cert.Certificate) > 0 {
leaf, _ = x509.ParseCertificate(cert.Certificate[0])
}
if leaf != nil {
result[fmt.Sprintf("gateway-tls-%d", i)] = leaf.NotAfter
}
}
return result
}