mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
fix(tunnel): resolve UDP stream interruption and add KCP protocol support
- Increase UDP listener default TTL from 5s to 30s to prevent idle disconnect - Add mux keepalive config (15s interval, 45s timeout) to tunnel relay handler - Add KCP as tunnel chain transport protocol with keepalive and UDP mode default - Add KCP protocol option to tunnel UI (frontend) - Remove generic 'tcp' fallback key from KCP metadata to prevent false TCP mode - Simplify forward service config by removing unused tunnelTLSProtocol parameter
This commit is contained in:
@@ -185,14 +185,6 @@ func (h *Handler) listForwardPorts(forwardID int64) ([]forwardPortRecord, error)
|
|||||||
return h.repo.ListForwardPorts(forwardID)
|
return h.repo.ListForwardPorts(forwardID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) isTunnelSelectedTLSProtocol(tunnelID int64) (bool, error) {
|
|
||||||
protocol, err := h.repo.GetTunnelOutProtocol(tunnelID)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return isTLSTunnelProtocol(protocol), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) getNodeRecord(nodeID int64) (*nodeRecord, error) {
|
func (h *Handler) getNodeRecord(nodeID int64) (*nodeRecord, error) {
|
||||||
n, err := h.repo.GetNodeRecord(nodeID)
|
n, err := h.repo.GetNodeRecord(nodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -272,10 +264,6 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceBase := buildForwardServiceBaseWithResolvedUserTunnel(forward.ID, forward.UserID, userTunnelID)
|
serviceBase := buildForwardServiceBaseWithResolvedUserTunnel(forward.ID, forward.UserID, userTunnelID)
|
||||||
tunnelTLSProtocol, err := h.isTunnelSelectedTLSProtocol(forward.TunnelID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fp := range ports {
|
for _, fp := range ports {
|
||||||
if limiterID != nil && speed != nil {
|
if limiterID != nil && speed != nil {
|
||||||
@@ -298,7 +286,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, strings.TrimSpace(fp.InIP), limiterID, tunnelTLSProtocol)
|
services := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, strings.TrimSpace(fp.InIP), limiterID)
|
||||||
_, err = h.sendNodeCommand(node.ID, method, services, true, false)
|
_, err = h.sendNodeCommand(node.ID, method, services, true, false)
|
||||||
if err != nil && allowFallbackAdd && method == "UpdateService" {
|
if err != nil && allowFallbackAdd && method == "UpdateService" {
|
||||||
if isNotFoundError(err) {
|
if isNotFoundError(err) {
|
||||||
@@ -313,7 +301,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
|
|||||||
}
|
}
|
||||||
if err != nil && strings.EqualFold(strings.TrimSpace(method), "UpdateService") && isCannotAssignRequestedAddressError(err) {
|
if err != nil && strings.EqualFold(strings.TrimSpace(method), "UpdateService") && isCannotAssignRequestedAddressError(err) {
|
||||||
var warning string
|
var warning string
|
||||||
warning, err = h.fallbackForwardPortToDefaultBind(forward, tunnel, node, fp, serviceBase, limiterID, tunnelTLSProtocol)
|
warning, err = h.fallbackForwardPortToDefaultBind(forward, tunnel, node, fp, serviceBase, limiterID)
|
||||||
if err == nil && warning != "" {
|
if err == nil && warning != "" {
|
||||||
warnings = append(warnings, warning)
|
warnings = append(warnings, warning)
|
||||||
}
|
}
|
||||||
@@ -339,7 +327,7 @@ func (h *Handler) syncForwardServicesWithWarnings(forward *forwardRecord, method
|
|||||||
return warnings, nil
|
return warnings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, fp forwardPortRecord, serviceBase string, limiterID *int64, tunnelTLSProtocol bool) (string, error) {
|
func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, fp forwardPortRecord, serviceBase string, limiterID *int64) (string, error) {
|
||||||
if h == nil || forward == nil || tunnel == nil || node == nil {
|
if h == nil || forward == nil || tunnel == nil || node == nil {
|
||||||
return "", errors.New("invalid bind fallback context")
|
return "", errors.New("invalid bind fallback context")
|
||||||
}
|
}
|
||||||
@@ -356,7 +344,7 @@ func (h *Handler) fallbackForwardPortToDefaultBind(forward *forwardRecord, tunne
|
|||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(150 * time.Millisecond)
|
time.Sleep(150 * time.Millisecond)
|
||||||
defaultServices := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, "", limiterID, tunnelTLSProtocol)
|
defaultServices := buildForwardServiceConfigs(serviceBase, forward, tunnel, node, fp.Port, "", limiterID)
|
||||||
if _, err := h.sendNodeCommand(node.ID, "AddService", defaultServices, true, false); err != nil {
|
if _, err := h.sendNodeCommand(node.ID, "AddService", defaultServices, true, false); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1561,7 +1549,7 @@ func compactErrorMessage(msg string) string {
|
|||||||
return strings.Join(strings.Fields(strings.ToLower(msg)), "")
|
return strings.Join(strings.Fields(strings.ToLower(msg)), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, bindIP string, limiterID *int64, tunnelTLSProtocol bool) []map[string]interface{} {
|
func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel *tunnelRecord, node *nodeRecord, port int, bindIP string, limiterID *int64) []map[string]interface{} {
|
||||||
protocols := []string{"tcp", "udp"}
|
protocols := []string{"tcp", "udp"}
|
||||||
services := make([]map[string]interface{}, 0, 2)
|
services := make([]map[string]interface{}, 0, 2)
|
||||||
targets := splitRemoteTargets(forward.RemoteAddr)
|
targets := splitRemoteTargets(forward.RemoteAddr)
|
||||||
@@ -1605,9 +1593,9 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
if protocol == "udp" {
|
if protocol == "udp" {
|
||||||
listenerMetadata := map[string]interface{}{"keepAlive": true}
|
listenerMetadata := map[string]interface{}{
|
||||||
if tunnelTLSProtocol {
|
"keepAlive": true,
|
||||||
listenerMetadata["ttl"] = "10s"
|
"ttl": "30s",
|
||||||
}
|
}
|
||||||
service["listener"].(map[string]interface{})["metadata"] = listenerMetadata
|
service["listener"].(map[string]interface{})["metadata"] = listenerMetadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ func TestRetryTunnelServiceAddWithCleanupReturnsCleanupError(t *testing.T) {
|
|||||||
func TestBuildForwardServiceConfigs_UsesBindIPForListen(t *testing.T) {
|
func TestBuildForwardServiceConfigs_UsesBindIPForListen(t *testing.T) {
|
||||||
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
||||||
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
||||||
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22000, "10.9.8.7", nil, false)
|
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22000, "10.9.8.7", nil)
|
||||||
if len(services) != 2 {
|
if len(services) != 2 {
|
||||||
t.Fatalf("expected 2 services, got %d", len(services))
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
}
|
}
|
||||||
@@ -393,7 +393,7 @@ func TestBuildForwardServiceConfigs_UsesBindIPForListen(t *testing.T) {
|
|||||||
func TestBuildForwardServiceConfigs_DefaultListenAddrWhenBindIPEmpty(t *testing.T) {
|
func TestBuildForwardServiceConfigs_DefaultListenAddrWhenBindIPEmpty(t *testing.T) {
|
||||||
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
||||||
node := &nodeRecord{TCPListenAddr: "0.0.0.0", UDPListenAddr: "[::]"}
|
node := &nodeRecord{TCPListenAddr: "0.0.0.0", UDPListenAddr: "[::]"}
|
||||||
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22001, "", nil, false)
|
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 22001, "", nil)
|
||||||
if len(services) != 2 {
|
if len(services) != 2 {
|
||||||
t.Fatalf("expected 2 services, got %d", len(services))
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
}
|
}
|
||||||
@@ -409,7 +409,7 @@ func TestBuildForwardServiceConfigs_DefaultListenAddrWhenBindIPEmpty(t *testing.
|
|||||||
func TestBuildForwardServiceConfigs_BindIPAlreadyContainsPort(t *testing.T) {
|
func TestBuildForwardServiceConfigs_BindIPAlreadyContainsPort(t *testing.T) {
|
||||||
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
||||||
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
||||||
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 55555, "3.3.3.3:12345", nil, false)
|
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, 55555, "3.3.3.3:12345", nil)
|
||||||
if len(services) != 2 {
|
if len(services) != 2 {
|
||||||
t.Fatalf("expected 2 services, got %d", len(services))
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
}
|
}
|
||||||
@@ -464,7 +464,7 @@ func TestBuildForwardServiceConfigs_IPv6BindIP(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
forward := &forwardRecord{RemoteAddr: "1.2.3.4:80", Strategy: "fifo", TunnelID: 7}
|
||||||
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
node := &nodeRecord{TCPListenAddr: "[::]", UDPListenAddr: "[::]"}
|
||||||
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, tt.port, tt.bindIP, nil, false)
|
services := buildForwardServiceConfigs("1_2_0", forward, nil, node, tt.port, tt.bindIP, nil)
|
||||||
if len(services) != 2 {
|
if len(services) != 2 {
|
||||||
t.Fatalf("expected 2 services, got %d", len(services))
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,12 +149,19 @@ func buildFederationServiceConfig(serviceName, addr, protocol, role, chainName s
|
|||||||
"handler": map[string]interface{}{
|
"handler": map[string]interface{}{
|
||||||
"type": "relay",
|
"type": "relay",
|
||||||
},
|
},
|
||||||
"listener": map[string]interface{}{
|
"listener": buildTunnelListenerConfig(protocol),
|
||||||
"type": protocol,
|
}
|
||||||
},
|
if isTCPTunnelProtocol(protocol) {
|
||||||
|
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{
|
||||||
|
"nodelay": true,
|
||||||
|
"mux.keepaliveInterval": "15s",
|
||||||
|
"mux.keepaliveTimeout": "45s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(protocol) {
|
||||||
|
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{
|
||||||
|
"connectTimeout": "30s",
|
||||||
}
|
}
|
||||||
if isTLSTunnelProtocol(protocol) {
|
|
||||||
service["handler"].(map[string]interface{})["metadata"] = map[string]interface{}{"nodelay": true}
|
|
||||||
}
|
}
|
||||||
if role == "middle" {
|
if role == "middle" {
|
||||||
service["handler"].(map[string]interface{})["chain"] = chainName
|
service["handler"].(map[string]interface{})["chain"] = chainName
|
||||||
@@ -1095,16 +1102,23 @@ func (h *Handler) federationRuntimeApplyRole(w http.ResponseWriter, r *http.Requ
|
|||||||
connector := map[string]interface{}{
|
connector := map[string]interface{}{
|
||||||
"type": "relay",
|
"type": "relay",
|
||||||
}
|
}
|
||||||
if isTLSTunnelProtocol(targetProtocol) {
|
if isTCPTunnelProtocol(targetProtocol) {
|
||||||
connector["metadata"] = map[string]interface{}{"nodelay": true}
|
connector["metadata"] = map[string]interface{}{
|
||||||
|
"nodelay": true,
|
||||||
|
"mux.keepaliveInterval": "15s",
|
||||||
|
"mux.keepaliveTimeout": "45s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(targetProtocol) {
|
||||||
|
connector["metadata"] = map[string]interface{}{
|
||||||
|
"connectTimeout": "30s",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
nodeItems = append(nodeItems, map[string]interface{}{
|
nodeItems = append(nodeItems, map[string]interface{}{
|
||||||
"name": fmt.Sprintf("node_%d", i+1),
|
"name": fmt.Sprintf("node_%d", i+1),
|
||||||
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, target.Port)),
|
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, target.Port)),
|
||||||
"connector": connector,
|
"connector": connector,
|
||||||
"dialer": map[string]interface{}{
|
"dialer": buildTunnelDialerConfig(targetProtocol),
|
||||||
"type": targetProtocol,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3452,16 +3452,23 @@ func buildTunnelChainConfig(tunnelID int64, fromNodeID int64, targets []tunnelRu
|
|||||||
connector := map[string]interface{}{
|
connector := map[string]interface{}{
|
||||||
"type": "relay",
|
"type": "relay",
|
||||||
}
|
}
|
||||||
if isTLSTunnelProtocol(protocol) {
|
connectorMetadata := map[string]interface{}{}
|
||||||
connector["metadata"] = map[string]interface{}{"nodelay": true}
|
if isTCPTunnelProtocol(protocol) {
|
||||||
|
connectorMetadata["nodelay"] = true
|
||||||
|
connectorMetadata["mux.keepaliveInterval"] = "15s"
|
||||||
|
connectorMetadata["mux.keepaliveTimeout"] = "45s"
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(protocol) {
|
||||||
|
connectorMetadata["connectTimeout"] = "30s"
|
||||||
|
}
|
||||||
|
if len(connectorMetadata) > 0 {
|
||||||
|
connector["metadata"] = connectorMetadata
|
||||||
}
|
}
|
||||||
nodeItems = append(nodeItems, map[string]interface{}{
|
nodeItems = append(nodeItems, map[string]interface{}{
|
||||||
"name": fmt.Sprintf("node_%d", idx+1),
|
"name": fmt.Sprintf("node_%d", idx+1),
|
||||||
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, port)),
|
"addr": processServerAddress(fmt.Sprintf("%s:%d", host, port)),
|
||||||
"connector": connector,
|
"connector": connector,
|
||||||
"dialer": map[string]interface{}{
|
"dialer": buildTunnelDialerConfig(protocol),
|
||||||
"type": protocol,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3493,19 +3500,26 @@ func buildTunnelChainServiceConfig(tunnelID int64, chainNode tunnelRuntimeNode,
|
|||||||
handlerCfg := map[string]interface{}{
|
handlerCfg := map[string]interface{}{
|
||||||
"type": "relay",
|
"type": "relay",
|
||||||
}
|
}
|
||||||
if isTLSTunnelProtocol(protocol) {
|
handlerMetadata := map[string]interface{}{}
|
||||||
handlerCfg["metadata"] = map[string]interface{}{"nodelay": true}
|
if isTCPTunnelProtocol(protocol) {
|
||||||
|
handlerMetadata["nodelay"] = true
|
||||||
|
handlerMetadata["mux.keepaliveInterval"] = "15s"
|
||||||
|
handlerMetadata["mux.keepaliveTimeout"] = "45s"
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(protocol) {
|
||||||
|
handlerMetadata["connectTimeout"] = "30s"
|
||||||
|
}
|
||||||
|
if len(handlerMetadata) > 0 {
|
||||||
|
handlerCfg["metadata"] = handlerMetadata
|
||||||
}
|
}
|
||||||
if nextHopCandidateCount > 1 {
|
if nextHopCandidateCount > 1 {
|
||||||
handlerCfg["retries"] = nextHopCandidateCount - 1
|
handlerCfg["retries"] = nextHopCandidateCount - 1
|
||||||
}
|
}
|
||||||
service := map[string]interface{}{
|
service := map[string]interface{}{
|
||||||
"name": fmt.Sprintf("%d_tls", tunnelID),
|
"name": fmt.Sprintf("%d_%s", tunnelID, protocol),
|
||||||
"addr": processServerAddress(fmt.Sprintf("%s:%d", defaultString(strings.TrimSpace(chainNode.ConnectIP), node.TCPListenAddr), chainNode.Port)),
|
"addr": processServerAddress(fmt.Sprintf("%s:%d", defaultString(strings.TrimSpace(chainNode.ConnectIP), node.TCPListenAddr), chainNode.Port)),
|
||||||
"handler": handlerCfg,
|
"handler": handlerCfg,
|
||||||
"listener": map[string]interface{}{
|
"listener": buildTunnelListenerConfig(protocol),
|
||||||
"type": protocol,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
if chainNode.ChainType == 2 {
|
if chainNode.ChainType == 2 {
|
||||||
service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", tunnelID)
|
service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", tunnelID)
|
||||||
@@ -3588,10 +3602,45 @@ func nodeDisplayName(node *nodeRecord) string {
|
|||||||
return fmt.Sprintf("node_%d", node.ID)
|
return fmt.Sprintf("node_%d", node.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTCPTunnelProtocol(protocol string) bool {
|
||||||
|
p := strings.ToLower(strings.TrimSpace(defaultString(protocol, "tls")))
|
||||||
|
return p == "tls" || p == "mtls" || p == "mtcp"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isKCPTunnelProtocol(protocol string) bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(protocol), "kcp")
|
||||||
|
}
|
||||||
|
|
||||||
func isTLSTunnelProtocol(protocol string) bool {
|
func isTLSTunnelProtocol(protocol string) bool {
|
||||||
return strings.EqualFold(strings.TrimSpace(defaultString(protocol, "tls")), "tls")
|
return strings.EqualFold(strings.TrimSpace(defaultString(protocol, "tls")), "tls")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildTunnelDialerConfig(protocol string) map[string]interface{} {
|
||||||
|
dialer := map[string]interface{}{
|
||||||
|
"type": protocol,
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(protocol) {
|
||||||
|
dialer["metadata"] = map[string]interface{}{
|
||||||
|
"kcp.keepalive": 10,
|
||||||
|
"kcp.tcp": false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTunnelListenerConfig(protocol string) map[string]interface{} {
|
||||||
|
listener := map[string]interface{}{
|
||||||
|
"type": protocol,
|
||||||
|
}
|
||||||
|
if isKCPTunnelProtocol(protocol) {
|
||||||
|
listener["metadata"] = map[string]interface{}{
|
||||||
|
"kcp.keepalive": 10,
|
||||||
|
"kcp.tcp": false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
func nodeSupportsV4(node *nodeRecord) bool {
|
func nodeSupportsV4(node *nodeRecord) bool {
|
||||||
if node == nil {
|
if node == nil {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (d *kcpDialer) parseMetadata(md mdata.Metadata) (err error) {
|
|||||||
if d.md.config == nil {
|
if d.md.config == nil {
|
||||||
d.md.config = kcp_util.DefaultConfig
|
d.md.config = kcp_util.DefaultConfig
|
||||||
}
|
}
|
||||||
d.md.config.TCP = mdutil.GetBool(md, "kcp.tcp", "tcp")
|
d.md.config.TCP = mdutil.GetBool(md, "kcp.tcp")
|
||||||
d.md.config.Key = mdutil.GetString(md, "kcp.key")
|
d.md.config.Key = mdutil.GetString(md, "kcp.key")
|
||||||
d.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
|
d.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
|
||||||
d.md.config.Mode = mdutil.GetString(md, "kcp.mode")
|
d.md.config.Mode = mdutil.GetString(md, "kcp.mode")
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ var (
|
|||||||
SnmpLog: "",
|
SnmpLog: "",
|
||||||
SnmpPeriod: 60,
|
SnmpPeriod: 60,
|
||||||
Signal: false,
|
Signal: false,
|
||||||
TCP: false,
|
TCP: false, // default UDP mode (use tcpraw when true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (l *kcpListener) parseMetadata(md mdata.Metadata) (err error) {
|
|||||||
if l.md.config == nil {
|
if l.md.config == nil {
|
||||||
l.md.config = kcp_util.DefaultConfig
|
l.md.config = kcp_util.DefaultConfig
|
||||||
}
|
}
|
||||||
l.md.config.TCP = mdutil.GetBool(md, "kcp.tcp", "tcp")
|
l.md.config.TCP = mdutil.GetBool(md, "kcp.tcp")
|
||||||
l.md.config.Key = mdutil.GetString(md, "kcp.key")
|
l.md.config.Key = mdutil.GetString(md, "kcp.key")
|
||||||
l.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
|
l.md.config.Crypt = mdutil.GetString(md, "kcp.crypt")
|
||||||
l.md.config.Mode = mdutil.GetString(md, "kcp.mode")
|
l.md.config.Mode = mdutil.GetString(md, "kcp.mode")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultTTL = 5 * time.Second
|
defaultTTL = 30 * time.Second
|
||||||
defaultReadBufferSize = 8192
|
defaultReadBufferSize = 8192
|
||||||
defaultReadQueueSize = 1024
|
defaultReadQueueSize = 1024
|
||||||
defaultBacklog = 128
|
defaultBacklog = 128
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultTTL = 5 * time.Second
|
defaultTTL = 30 * time.Second
|
||||||
defaultReadBufferSize = 8192
|
defaultReadBufferSize = 8192
|
||||||
defaultReadQueueSize = 128
|
defaultReadQueueSize = 128
|
||||||
defaultBacklog = 128
|
defaultBacklog = 128
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# 069: UDP 隧道转发断流修复 + KCP 协议支持
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
GOST 隧道转发 UDP 断流的根因涉及三个超时层:
|
||||||
|
|
||||||
|
### 1. UDP 虚拟连接 TTL 过短(5秒)— 主因
|
||||||
|
- `go-gost/x/listener/udp/metadata.go:11`: `defaultTTL = 5s`
|
||||||
|
- `go-gost/x/internal/net/udp/pool.go:79-115`: `idleCheck()` 每 TTL 周期标记空闲连接,下一周期关闭
|
||||||
|
- **影响**: UDP 流无数据包超过 ~10 秒即被静默关闭
|
||||||
|
- **后端配置**: `control_plane.go:1607-1613` 设置了 `keepAlive: true`,但仅在 `tunnelTLSProtocol=true` 时设 `ttl: "10s"`,常规隧道转发不设 TTL 覆盖
|
||||||
|
|
||||||
|
### 2. Smux Keepalive 未显式配置
|
||||||
|
- smux `DefaultConfig()`: `KeepAliveInterval=10s`, `KeepAliveTimeout=30s`
|
||||||
|
- 后端 `buildTunnelChainServiceConfig` 生成 relay handler 时不设置 mux 参数
|
||||||
|
- 依赖 smux 默认值,但中间 NAT/防火墙可能先于 30s 断开 TCP 连接
|
||||||
|
|
||||||
|
### 3. 隧道仅依赖 TCP 传输 — 无 UDP 备选
|
||||||
|
- 隧道链节点间仅支持 TLS/TCP 传输
|
||||||
|
- TCP 队头阻塞 + 单连接承载所有流 + 无 UDP 级别的容错
|
||||||
|
|
||||||
|
## 修复方案
|
||||||
|
|
||||||
|
### Part A: 修复现有问题(低风险,即时生效)
|
||||||
|
|
||||||
|
#### A1. 增大 UDP listener 默认 TTL
|
||||||
|
- **文件**: `go-gost/x/listener/udp/metadata.go`
|
||||||
|
- **改动**: `defaultTTL = 30s`(从 5s)
|
||||||
|
- **原因**: 5s 对实际场景太激进,DNS 等长间隔 UDP 应用会被误杀
|
||||||
|
|
||||||
|
#### A2. 后端生成 UDP forward 配置时强制设置 TTL
|
||||||
|
- **文件**: `go-backend/internal/http/handler/control_plane.go`
|
||||||
|
- **改动**: `buildForwardServiceConfigs` 中始终设置 `listener.metadata.ttl = "30s"`
|
||||||
|
- **原因**: 确保配置显式覆盖,不依赖 go-gost 默认值
|
||||||
|
|
||||||
|
#### A3. 后端隧道链服务配置加入 mux keepalive
|
||||||
|
- **文件**: `go-backend/internal/http/handler/mutations.go`
|
||||||
|
- **改动**: `buildTunnelChainServiceConfig` 中的 relay handler 加入 mux metadata:
|
||||||
|
```go
|
||||||
|
"mux.keepaliveInterval": "15s",
|
||||||
|
"mux.keepaliveTimeout": "45s",
|
||||||
|
```
|
||||||
|
|
||||||
|
### Part B: 新增 KCP 协议支持(新功能)
|
||||||
|
|
||||||
|
#### B1. 后端支持 `kcp` 作为隧道链传输协议
|
||||||
|
- **文件**: `go-backend/internal/http/handler/mutations.go`
|
||||||
|
- **改动**:
|
||||||
|
- `buildTunnelChainConfig`: 识别 `kcp` 协议,生成对应的 connector/dialer
|
||||||
|
- `buildTunnelChainServiceConfig`: 识别 `kcp` 协议,生成 KCP listener
|
||||||
|
- `isTLSTunnelProtocol` → `isTCPTunnelProtocol`(包含 TLS)
|
||||||
|
- KCP dialer 设置 `kcp.keepalive` 参数
|
||||||
|
- KCP 配置无需 `nodelay`(那是 TLS 特有)
|
||||||
|
|
||||||
|
#### B2. 前端支持选择 `kcp` 协议
|
||||||
|
- **文件**: `vite-frontend/src/pages/tunnel/` 相关表单
|
||||||
|
- **改动**: 协议选择器中加入 KCP 选项
|
||||||
|
|
||||||
|
#### B3. 确认 go-gost KCP 组件正常
|
||||||
|
- KCP dialer: `go-gost/x/dialer/kcp/` — 已存在
|
||||||
|
- KCP listener: `go-gost/x/listener/kcp/` — 已存在
|
||||||
|
- KCP connector: 使用 `relay` connector 通过 KCP dialer 连接
|
||||||
|
- 验证 KCP 配置的 metadata 参数传递正确
|
||||||
|
|
||||||
|
## 任务清单
|
||||||
|
|
||||||
|
- [x] A1: 增大 UDP listener 默认 TTL (5s→30s)
|
||||||
|
- [x] A2: 后端 forward 配置强制设置 UDP TTL=30s
|
||||||
|
- [x] A3: 后端隧道链 relay handler 加入 mux keepalive
|
||||||
|
- [x] B1: 后端支持 kcp 作为隧道链协议
|
||||||
|
- [x] B2: 前端协议选择器加入 KCP
|
||||||
|
- [x] B3: 验证 go-gost KCP 组件配置正确
|
||||||
|
- [x] C1: go-backend 合约测试 (187 passed)
|
||||||
|
- [x] C2: go-gost 编译验证 (passed)
|
||||||
|
- [x] C3: vite-frontend 编译验证 (passed)
|
||||||
|
|
||||||
|
## 文件变更清单
|
||||||
|
|
||||||
|
| 文件 | 变更类型 |
|
||||||
|
|------|----------|
|
||||||
|
| `go-gost/x/listener/udp/metadata.go` | 修改 defaultTTL: 5s→30s |
|
||||||
|
| `go-gost/x/listener/rudp/metadata.go` | 修改 defaultTTL: 5s→30s |
|
||||||
|
| `go-backend/internal/http/handler/control_plane.go` | buildForwardServiceConfigs: 强制设置 UDP TTL=30s, 移除 tunnelTLSProtocol 参数 |
|
||||||
|
| `go-backend/internal/http/handler/control_plane_test.go` | 更新测试调用签名 |
|
||||||
|
| `go-backend/internal/http/handler/mutations.go` | 新增 isTCPTunnelProtocol/isKCPTunnelProtocol/buildTunnelDialerConfig/buildTunnelListenerConfig; 隧道链配置加入 mux keepalive + KCP 支持 |
|
||||||
|
| `go-backend/internal/http/handler/federation.go` | federation 配置同步加入 mux keepalive + KCP 支持 |
|
||||||
|
| `vite-frontend/src/pages/tunnel.tsx` | 协议选择器新增 KCP 选项 (3 处)
|
||||||
@@ -90,7 +90,7 @@ import {
|
|||||||
|
|
||||||
interface ChainTunnel {
|
interface ChainTunnel {
|
||||||
nodeId: number;
|
nodeId: number;
|
||||||
protocol?: string; // 'tls' | 'wss' | 'tcp' | 'mtls' | 'mwss' | 'mtcp' - 转发链协议
|
protocol?: string; // 'tls' | 'wss' | 'tcp' | 'mtls' | 'mwss' | 'mtcp' | 'kcp' - 转发链协议
|
||||||
strategy?: string; // 'fifo' | 'round' | 'rand' - 仅转发链需要
|
strategy?: string; // 'fifo' | 'round' | 'rand' - 仅转发链需要
|
||||||
chainType?: number; // 1: 入口, 2: 转发链, 3: 出口
|
chainType?: number; // 1: 入口, 2: 转发链, 3: 出口
|
||||||
inx?: number; // 转发链序号
|
inx?: number; // 转发链序号
|
||||||
@@ -2456,6 +2456,7 @@ export default function TunnelPage() {
|
|||||||
<SelectItem key="mtls">MTLS</SelectItem>
|
<SelectItem key="mtls">MTLS</SelectItem>
|
||||||
<SelectItem key="mwss">MWSS</SelectItem>
|
<SelectItem key="mwss">MWSS</SelectItem>
|
||||||
<SelectItem key="mtcp">MTCP</SelectItem>
|
<SelectItem key="mtcp">MTCP</SelectItem>
|
||||||
|
<SelectItem key="kcp">KCP</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* 负载策略 - 25% */}
|
{/* 负载策略 - 25% */}
|
||||||
@@ -2756,6 +2757,7 @@ export default function TunnelPage() {
|
|||||||
<SelectItem key="mtls">MTLS</SelectItem>
|
<SelectItem key="mtls">MTLS</SelectItem>
|
||||||
<SelectItem key="mwss">MWSS</SelectItem>
|
<SelectItem key="mwss">MWSS</SelectItem>
|
||||||
<SelectItem key="mtcp">MTCP</SelectItem>
|
<SelectItem key="mtcp">MTCP</SelectItem>
|
||||||
|
<SelectItem key="kcp">KCP</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
{/* 负载策略 - 25% */}
|
{/* 负载策略 - 25% */}
|
||||||
|
|||||||
Reference in New Issue
Block a user