diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 66c39e9..4025b91 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -1757,6 +1757,7 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel services := make([]map[string]interface{}, 0, 2) targets := splitRemoteTargets(forward.RemoteAddr) strategy := strings.TrimSpace(forward.Strategy) + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(forward.ProxyProtocol, forward.ProxyProtocolReceive, forward.ProxyProtocolSend) if strategy == "" { strategy = "fifo" } @@ -1801,12 +1802,16 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel if runtimeLimiters.TrafficLimiter != "" { service["limiter"] = runtimeLimiters.TrafficLimiter } - if forward.ProxyProtocol > 0 { + if proxyProtocolReceive > 0 { + serviceMetadata := ensureServiceMetadata(service) + serviceMetadata["proxyProtocol"] = proxyProtocolReceive + } + if proxyProtocolSend > 0 { handlerConfig := service["handler"].(map[string]interface{}) if handlerConfig["metadata"] == nil { handlerConfig["metadata"] = map[string]interface{}{} } - handlerConfig["metadata"].(map[string]interface{})["proxyProtocol"] = forward.ProxyProtocol + handlerConfig["metadata"].(map[string]interface{})["proxyProtocol"] = proxyProtocolSend } if protocol == "udp" { listenerMetadata := map[string]interface{}{ @@ -1819,10 +1824,8 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", forward.TunnelID) } if tunnel != nil && tunnel.Type == 1 && strings.TrimSpace(node.InterfaceName) != "" { - if service["metadata"] == nil { - service["metadata"] = map[string]interface{}{} - } - service["metadata"].(map[string]interface{})["interface"] = node.InterfaceName + serviceMetadata := ensureServiceMetadata(service) + serviceMetadata["interface"] = node.InterfaceName } services = append(services, service) } @@ -1841,6 +1844,25 @@ func buildForwarderNodes(targets []string) []map[string]interface{} { return nodes } +func ensureServiceMetadata(service map[string]interface{}) map[string]interface{} { + if service["metadata"] == nil { + service["metadata"] = map[string]interface{}{} + } + metadata, ok := service["metadata"].(map[string]interface{}) + if !ok { + metadata = map[string]interface{}{} + service["metadata"] = metadata + } + return metadata +} + +func normalizeForwardProxyProtocol(legacy, receive, send int) (int, int) { + if send == 0 && legacy > 0 { + send = legacy + } + return receive, send +} + func processServerAddress(serverAddr string) string { serverAddr = normalizeServerAddressInput(serverAddr) if serverAddr == "" { diff --git a/go-backend/internal/http/handler/forward_proxy_protocol_test.go b/go-backend/internal/http/handler/forward_proxy_protocol_test.go index b54df23..e11f5a9 100644 --- a/go-backend/internal/http/handler/forward_proxy_protocol_test.go +++ b/go-backend/internal/http/handler/forward_proxy_protocol_test.go @@ -9,14 +9,15 @@ import ( "go-backend/internal/store/repo" ) -func TestBuildForwardServiceConfigsSendsProxyProtocolToForwardHandler(t *testing.T) { +func TestBuildForwardServiceConfigsAppliesProxyProtocolReceiveAndSendIndependently(t *testing.T) { forward := &forwardRecord{ - ID: 1, - UserID: 2, - TunnelID: 3, - RemoteAddr: "1.1.1.1:443", - Strategy: "fifo", - ProxyProtocol: 2, + ID: 1, + UserID: 2, + TunnelID: 3, + RemoteAddr: "1.1.1.1:443", + Strategy: "fifo", + ProxyProtocolReceive: 1, + ProxyProtocolSend: 2, } tunnel := &tunnelRecord{Type: 1} node := &nodeRecord{ @@ -38,8 +39,8 @@ func TestBuildForwardServiceConfigsSendsProxyProtocolToForwardHandler(t *testing if serviceMetadata["interface"] != "eth0" { t.Fatalf("expected interface metadata eth0, got %v", serviceMetadata["interface"]) } - if _, ok := serviceMetadata["proxyProtocol"]; ok { - t.Fatalf("proxyProtocol should not be listener metadata: %v", serviceMetadata) + if serviceMetadata["proxyProtocol"] != 1 { + t.Fatalf("expected service proxyProtocol 1 for receive mode, got %v", serviceMetadata["proxyProtocol"]) } handlerConfig, ok := service["handler"].(map[string]interface{}) @@ -56,6 +57,46 @@ func TestBuildForwardServiceConfigsSendsProxyProtocolToForwardHandler(t *testing } } +func TestBuildForwardServiceConfigsKeepsLegacyProxyProtocolAsSend(t *testing.T) { + forward := &forwardRecord{ + ID: 1, + UserID: 2, + TunnelID: 3, + RemoteAddr: "1.1.1.1:443", + Strategy: "fifo", + ProxyProtocol: 2, + } + tunnel := &tunnelRecord{Type: 1} + node := &nodeRecord{ + TCPListenAddr: "0.0.0.0", + UDPListenAddr: "0.0.0.0", + } + + services := buildForwardServiceConfigs("1_2_3", forward, tunnel, node, 4001, "", forwardRuntimeLimiters{}) + if len(services) != 2 { + t.Fatalf("expected 2 services, got %d", len(services)) + } + + for _, service := range services { + serviceMetadata, _ := service["metadata"].(map[string]interface{}) + if _, ok := serviceMetadata["proxyProtocol"]; ok { + t.Fatalf("legacy proxyProtocol should not enable receive mode: %v", serviceMetadata) + } + + handlerConfig, ok := service["handler"].(map[string]interface{}) + if !ok { + t.Fatalf("expected handler config map, got %T", service["handler"]) + } + handlerMetadata, ok := handlerConfig["metadata"].(map[string]interface{}) + if !ok { + t.Fatalf("expected handler metadata map, got %T", handlerConfig["metadata"]) + } + if handlerMetadata["proxyProtocol"] != 2 { + t.Fatalf("expected legacy proxyProtocol to send version 2, got %v", handlerMetadata["proxyProtocol"]) + } + } +} + func TestRollbackForwardMutationRestoresProxyProtocol(t *testing.T) { r, err := repo.Open(":memory:") if err != nil { diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 3fbc5d3..3fb5f32 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -2143,8 +2143,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) { ipMaxConn = 0 } proxyProtocol := asInt(req["proxyProtocol"], 0) + proxyProtocolReceive := asInt(req["proxyProtocolReceive"], 0) + proxyProtocolSend := asInt(req["proxyProtocolSend"], proxyProtocol) - forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, ipMaxConn, nullableInt(ipSpeedID), proxyProtocol) + forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, ipMaxConn, nullableInt(ipSpeedID), proxyProtocol, proxyProtocolReceive, proxyProtocolSend) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return @@ -2333,8 +2335,10 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) { ipMaxConn = 0 } proxyProtocol := asInt(req["proxyProtocol"], forward.ProxyProtocol) + proxyProtocolReceive := asInt(req["proxyProtocolReceive"], forward.ProxyProtocolReceive) + proxyProtocolSend := asInt(req["proxyProtocolSend"], forward.ProxyProtocolSend) - if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn, ipMaxConn, newIPSpeedID, proxyProtocol); err != nil { + if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn, ipMaxConn, newIPSpeedID, proxyProtocol, proxyProtocolReceive, proxyProtocolSend); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } @@ -4722,7 +4726,7 @@ func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts [] h.repo.RollbackForwardFields( oldForward.ID, oldForward.UserID, oldForward.UserName, oldForward.Name, oldForward.TunnelID, oldForward.RemoteAddr, oldForward.Strategy, oldForward.Status, - oldForward.SpeedID, oldForward.MaxConn, oldForward.IPMaxConn, oldForward.IPSpeedID, oldForward.ProxyProtocol, + oldForward.SpeedID, oldForward.MaxConn, oldForward.IPMaxConn, oldForward.IPSpeedID, oldForward.ProxyProtocol, oldForward.ProxyProtocolReceive, oldForward.ProxyProtocolSend, time.Now().UnixMilli(), ) diff --git a/go-backend/internal/http/handler/nftables_runtime_test.go b/go-backend/internal/http/handler/nftables_runtime_test.go index 6cfacef..e2bd313 100644 --- a/go-backend/internal/http/handler/nftables_runtime_test.go +++ b/go-backend/internal/http/handler/nftables_runtime_test.go @@ -490,7 +490,7 @@ func seedForwardForNftables(t *testing.T, h *Handler, tunnelID, nodeID int64, re now := time.Now().UnixMilli() forwardID, err := h.repo.CreateForwardTx( 1, "admin", "nft-forward", tunnelID, remoteAddr, "fifo", now, 1, - []int64{nodeID}, 20000, "", nil, 0, 0, nil, 0, + []int64{nodeID}, 20000, "", nil, 0, 0, nil, 0, 0, 0, ) if err != nil { t.Fatalf("create forward: %v", err) diff --git a/go-backend/internal/store/model/model.go b/go-backend/internal/store/model/model.go index 6ac19ad..d35cc8a 100644 --- a/go-backend/internal/store/model/model.go +++ b/go-backend/internal/store/model/model.go @@ -31,24 +31,26 @@ func (User) TableName() string { return "user" } // Forward maps to the "forward" table. type Forward struct { - ID int64 `gorm:"primaryKey;autoIncrement"` - UserID int64 `gorm:"column:user_id;not null"` - UserName string `gorm:"column:user_name;type:varchar(100);not null"` - Name string `gorm:"type:varchar(100);not null"` - TunnelID int64 `gorm:"column:tunnel_id;not null"` - RemoteAddr string `gorm:"column:remote_addr;type:text;not null"` - Strategy string `gorm:"type:varchar(100);not null;default:'fifo'"` - InFlow int64 `gorm:"not null;default:0"` - OutFlow int64 `gorm:"column:out_flow;not null;default:0"` - CreatedTime int64 `gorm:"column:created_time;not null"` - UpdatedTime int64 `gorm:"column:updated_time;not null"` - Status int `gorm:"not null"` - Inx int `gorm:"not null;default:0"` - SpeedID sql.NullInt64 `gorm:"column:speed_id"` - MaxConn int `gorm:"column:max_conn;not null;default:0"` - IPMaxConn int `gorm:"column:ip_max_conn;not null;default:0"` - IPSpeedID sql.NullInt64 `gorm:"column:ip_speed_id"` - ProxyProtocol int `gorm:"column:proxy_protocol;not null;default:0"` + ID int64 `gorm:"primaryKey;autoIncrement"` + UserID int64 `gorm:"column:user_id;not null"` + UserName string `gorm:"column:user_name;type:varchar(100);not null"` + Name string `gorm:"type:varchar(100);not null"` + TunnelID int64 `gorm:"column:tunnel_id;not null"` + RemoteAddr string `gorm:"column:remote_addr;type:text;not null"` + Strategy string `gorm:"type:varchar(100);not null;default:'fifo'"` + InFlow int64 `gorm:"not null;default:0"` + OutFlow int64 `gorm:"column:out_flow;not null;default:0"` + CreatedTime int64 `gorm:"column:created_time;not null"` + UpdatedTime int64 `gorm:"column:updated_time;not null"` + Status int `gorm:"not null"` + Inx int `gorm:"not null;default:0"` + SpeedID sql.NullInt64 `gorm:"column:speed_id"` + MaxConn int `gorm:"column:max_conn;not null;default:0"` + IPMaxConn int `gorm:"column:ip_max_conn;not null;default:0"` + IPSpeedID sql.NullInt64 `gorm:"column:ip_speed_id"` + ProxyProtocol int `gorm:"column:proxy_protocol;not null;default:0"` + ProxyProtocolReceive int `gorm:"column:proxy_protocol_receive;not null;default:0"` + ProxyProtocolSend int `gorm:"column:proxy_protocol_send;not null;default:0"` } func (Forward) TableName() string { return "forward" } @@ -487,24 +489,26 @@ type ChainTunnelBackup struct { } type ForwardBackup struct { - ID int64 `json:"id"` - UserID int64 `json:"userId"` - UserName string `json:"userName"` - Name string `json:"name"` - TunnelID int64 `json:"tunnelId"` - RemoteAddr string `json:"remoteAddr"` - Strategy string `json:"strategy"` - InFlow int64 `json:"inFlow"` - OutFlow int64 `json:"outFlow"` - CreatedTime int64 `json:"createdTime"` - UpdatedTime int64 `json:"updatedTime"` - Status int `json:"status"` - Inx int `json:"inx"` - SpeedID *int64 `json:"speedId,omitempty"` - IPMaxConn int `json:"ipMaxConn,omitempty"` - IPSpeedID *int64 `json:"ipSpeedId,omitempty"` - ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"` - ProxyProtocol int `json:"proxyProtocol"` + ID int64 `json:"id"` + UserID int64 `json:"userId"` + UserName string `json:"userName"` + Name string `json:"name"` + TunnelID int64 `json:"tunnelId"` + RemoteAddr string `json:"remoteAddr"` + Strategy string `json:"strategy"` + InFlow int64 `json:"inFlow"` + OutFlow int64 `json:"outFlow"` + CreatedTime int64 `json:"createdTime"` + UpdatedTime int64 `json:"updatedTime"` + Status int `json:"status"` + Inx int `json:"inx"` + SpeedID *int64 `json:"speedId,omitempty"` + IPMaxConn int `json:"ipMaxConn,omitempty"` + IPSpeedID *int64 `json:"ipSpeedId,omitempty"` + ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"` + ProxyProtocol int `json:"proxyProtocol"` + ProxyProtocolReceive int `json:"proxyProtocolReceive,omitempty"` + ProxyProtocolSend int `json:"proxyProtocolSend,omitempty"` } type ForwardPortBackup struct { @@ -593,19 +597,21 @@ type ImportResult struct { // ForwardRecord is a minimal forward view used by control plane and flow policy. type ForwardRecord struct { - ID int64 - UserID int64 - UserName string - Name string - TunnelID int64 - RemoteAddr string - Strategy string - Status int - SpeedID sql.NullInt64 - MaxConn int - IPMaxConn int - IPSpeedID sql.NullInt64 - ProxyProtocol int + ID int64 + UserID int64 + UserName string + Name string + TunnelID int64 + RemoteAddr string + Strategy string + Status int + SpeedID sql.NullInt64 + MaxConn int + IPMaxConn int + IPSpeedID sql.NullInt64 + ProxyProtocol int + ProxyProtocolReceive int + ProxyProtocolSend int } // TunnelRecord is a minimal tunnel view used by control plane. diff --git a/go-backend/internal/store/repo/repository.go b/go-backend/internal/store/repo/repository.go index 2629dac..b2fccb2 100644 --- a/go-backend/internal/store/repo/repository.go +++ b/go-backend/internal/store/repo/repository.go @@ -424,7 +424,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error { } if m.HasTable(&model.Forward{}) { - for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol"} { + for _, field := range []string{"MaxConn", "IPMaxConn", "IPSpeedID", "ProxyProtocol", "ProxyProtocolReceive", "ProxyProtocolSend"} { if m.HasColumn(&model.Forward{}, field) { continue } @@ -944,31 +944,33 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) { } type fwdRow struct { - ID int64 - UserID int64 - UserName string - Name string - TunnelID int64 - TunnelName string - TrafficRatio float64 - RemoteAddr string - Strategy string - InFlow int64 - OutFlow int64 - CreatedTime int64 - Status int - Inx int - SpeedID sql.NullInt64 - MaxConn int - IPMaxConn int - IPSpeedID sql.NullInt64 - IPSpeedLimitName string - ProxyProtocol int + ID int64 + UserID int64 + UserName string + Name string + TunnelID int64 + TunnelName string + TrafficRatio float64 + RemoteAddr string + Strategy string + InFlow int64 + OutFlow int64 + CreatedTime int64 + Status int + Inx int + SpeedID sql.NullInt64 + MaxConn int + IPMaxConn int + IPSpeedID sql.NullInt64 + IPSpeedLimitName string + ProxyProtocol int + ProxyProtocolReceive int + ProxyProtocolSend int } var rows []fwdRow err := r.db.Model(&model.Forward{}). - Select("forward.id, forward.user_id, forward.user_name, forward.name, forward.tunnel_id, COALESCE(tunnel.name, '') AS tunnel_name, COALESCE(tunnel.traffic_ratio, 1.0) AS traffic_ratio, forward.remote_addr, COALESCE(forward.strategy, 'fifo') AS strategy, forward.in_flow, forward.out_flow, forward.created_time, forward.status, forward.inx, forward.speed_id, forward.max_conn, forward.ip_max_conn, forward.ip_speed_id, COALESCE(ip_speed_limit.name, '') AS ip_speed_limit_name, forward.proxy_protocol"). + Select("forward.id, forward.user_id, forward.user_name, forward.name, forward.tunnel_id, COALESCE(tunnel.name, '') AS tunnel_name, COALESCE(tunnel.traffic_ratio, 1.0) AS traffic_ratio, forward.remote_addr, COALESCE(forward.strategy, 'fifo') AS strategy, forward.in_flow, forward.out_flow, forward.created_time, forward.status, forward.inx, forward.speed_id, forward.max_conn, forward.ip_max_conn, forward.ip_speed_id, COALESCE(ip_speed_limit.name, '') AS ip_speed_limit_name, forward.proxy_protocol, forward.proxy_protocol_receive, forward.proxy_protocol_send"). Joins("LEFT JOIN tunnel ON tunnel.id = forward.tunnel_id"). Joins("LEFT JOIN speed_limit AS ip_speed_limit ON ip_speed_limit.id = forward.ip_speed_id"). Order("forward.inx ASC, forward.id ASC"). @@ -979,6 +981,7 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) { items := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(row.ProxyProtocol, row.ProxyProtocolReceive, row.ProxyProtocolSend) inIP, inPort, err := resolveForwardIngress(r.db, row.ID, row.TunnelID) if err != nil { return nil, err @@ -991,9 +994,11 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) { "remoteAddr": row.RemoteAddr, "strategy": row.Strategy, "inFlow": row.InFlow, "outFlow": row.OutFlow, "createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx), - "maxConn": row.MaxConn, - "ipMaxConn": row.IPMaxConn, - "proxyProtocol": row.ProxyProtocol, + "maxConn": row.MaxConn, + "ipMaxConn": row.IPMaxConn, + "proxyProtocol": row.ProxyProtocol, + "proxyProtocolReceive": proxyProtocolReceive, + "proxyProtocolSend": proxyProtocolSend, } if row.SpeedID.Valid { item["speedId"] = row.SpeedID.Int64 @@ -2195,8 +2200,10 @@ func (r *Repository) exportForwards() ([]model.ForwardBackup, error) { TunnelID: f.TunnelID, RemoteAddr: f.RemoteAddr, Strategy: f.Strategy, InFlow: f.InFlow, OutFlow: f.OutFlow, CreatedTime: f.CreatedTime, UpdatedTime: f.UpdatedTime, Status: f.Status, Inx: f.Inx, - IPMaxConn: f.IPMaxConn, - ProxyProtocol: f.ProxyProtocol, + IPMaxConn: f.IPMaxConn, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: f.ProxyProtocolReceive, + ProxyProtocolSend: f.ProxyProtocolSend, } if f.SpeedID.Valid { v := f.SpeedID.Int64 @@ -2630,29 +2637,31 @@ func importForwards(tx *gorm.DB, forwards []model.ForwardBackup, now int64) (int count := 0 for _, f := range forwards { item := model.Forward{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - InFlow: f.InFlow, - OutFlow: f.OutFlow, - CreatedTime: f.CreatedTime, - UpdatedTime: now, - Status: f.Status, - Inx: f.Inx, - SpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.SpeedID), Valid: f.SpeedID != nil && *f.SpeedID > 0}, - IPMaxConn: f.IPMaxConn, - IPSpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.IPSpeedID), Valid: f.IPSpeedID != nil && *f.IPSpeedID > 0}, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + InFlow: f.InFlow, + OutFlow: f.OutFlow, + CreatedTime: f.CreatedTime, + UpdatedTime: now, + Status: f.Status, + Inx: f.Inx, + SpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.SpeedID), Valid: f.SpeedID != nil && *f.SpeedID > 0}, + IPMaxConn: f.IPMaxConn, + IPSpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.IPSpeedID), Valid: f.IPSpeedID != nil && *f.IPSpeedID > 0}, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: f.ProxyProtocolReceive, + ProxyProtocolSend: f.ProxyProtocolSend, } err := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "id"}}, DoUpdates: clause.AssignmentColumns([]string{ "user_id", "user_name", "name", "tunnel_id", "remote_addr", "strategy", - "in_flow", "out_flow", "updated_time", "status", "inx", "speed_id", "ip_max_conn", "ip_speed_id", "proxy_protocol", + "in_flow", "out_flow", "updated_time", "status", "inx", "speed_id", "ip_max_conn", "ip_speed_id", "proxy_protocol", "proxy_protocol_receive", "proxy_protocol_send", }), }).Create(&item).Error if err != nil { diff --git a/go-backend/internal/store/repo/repository_control.go b/go-backend/internal/store/repo/repository_control.go index 3357db2..307b298 100644 --- a/go-backend/internal/store/repo/repository_control.go +++ b/go-backend/internal/store/repo/repository_control.go @@ -44,20 +44,23 @@ func (r *Repository) ListForwardsByTunnelTx(tx *gorm.DB, tunnelID int64) ([]mode } rows := make([]model.ForwardRecord, 0, len(forwards)) for _, f := range forwards { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) rows = append(rows, model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, }) } for i := range rows { diff --git a/go-backend/internal/store/repo/repository_flow.go b/go-backend/internal/store/repo/repository_flow.go index d468841..c084080 100644 --- a/go-backend/internal/store/repo/repository_flow.go +++ b/go-backend/internal/store/repo/repository_flow.go @@ -120,20 +120,23 @@ func (r *Repository) ListActiveForwardsByUser(userID int64) ([]model.ForwardReco } rows := make([]model.ForwardRecord, 0, len(forwards)) for _, f := range forwards { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) rows = append(rows, model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, }) } for i := range rows { @@ -155,20 +158,23 @@ func (r *Repository) ListActiveForwardsByUserTunnel(userID, tunnelID int64) ([]m } rows := make([]model.ForwardRecord, 0, len(forwards)) for _, f := range forwards { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) rows = append(rows, model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, }) } for i := range rows { @@ -190,20 +196,23 @@ func (r *Repository) ListForwardsByUserAndTunnel(userID, tunnelID int64) ([]mode } rows := make([]model.ForwardRecord, 0, len(forwards)) for _, f := range forwards { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) rows = append(rows, model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, }) } for i := range rows { @@ -226,20 +235,23 @@ func (r *Repository) GetForwardRecord(forwardID int64) (*model.ForwardRecord, er } return nil, err } + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) fr := model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, } if strings.TrimSpace(fr.Strategy) == "" { fr.Strategy = "fifo" diff --git a/go-backend/internal/store/repo/repository_forward_proxy_protocol_test.go b/go-backend/internal/store/repo/repository_forward_proxy_protocol_test.go index 19e208a..76715b7 100644 --- a/go-backend/internal/store/repo/repository_forward_proxy_protocol_test.go +++ b/go-backend/internal/store/repo/repository_forward_proxy_protocol_test.go @@ -160,7 +160,7 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) { defer r.Close() now := time.Now().UnixMilli() - forwardID, err := r.CreateForwardTx(1, "admin", "per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 5, int64(21), 0) + forwardID, err := r.CreateForwardTx(1, "admin", "per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 5, int64(21), 0, 0, 0) if err != nil { t.Fatalf("CreateForwardTx: %v", err) } @@ -175,7 +175,7 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) { t.Fatalf("expected created ipSpeedId 21, got %+v", record.IPSpeedID) } - if err := r.UpdateForward(forwardID, "per-ip-forward", 2, "2.2.2.2:443", "fifo", now+1, nil, 0, 9, int64(22), 0); err != nil { + if err := r.UpdateForward(forwardID, "per-ip-forward", 2, "2.2.2.2:443", "fifo", now+1, nil, 0, 9, int64(22), 0, 0, 0); err != nil { t.Fatalf("UpdateForward: %v", err) } record, err = r.GetForwardRecord(forwardID) @@ -216,6 +216,83 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) { } } +func TestForwardRepositoryPersistsProxyProtocolReceiveAndSend(t *testing.T) { + r, err := Open(":memory:") + if err != nil { + t.Fatalf("open repo: %v", err) + } + defer r.Close() + + now := time.Now().UnixMilli() + forwardID, err := r.CreateForwardTx(1, "admin", "proxy-protocol-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 0, nil, 0, 1, 2) + if err != nil { + t.Fatalf("CreateForwardTx: %v", err) + } + + record, err := r.GetForwardRecord(forwardID) + if err != nil { + t.Fatalf("GetForwardRecord after create: %v", err) + } + if record.ProxyProtocolReceive != 1 || record.ProxyProtocolSend != 2 { + t.Fatalf("expected proxyProtocol receive/send 1/2 after create, got %d/%d", record.ProxyProtocolReceive, record.ProxyProtocolSend) + } + + if err := r.UpdateForward(forwardID, "proxy-protocol-forward", 2, "2.2.2.2:443", "fifo", now+1, nil, 0, 0, nil, 0, 2, 1); err != nil { + t.Fatalf("UpdateForward: %v", err) + } + record, err = r.GetForwardRecord(forwardID) + if err != nil { + t.Fatalf("GetForwardRecord after update: %v", err) + } + if record.ProxyProtocolReceive != 2 || record.ProxyProtocolSend != 1 { + t.Fatalf("expected proxyProtocol receive/send 2/1 after update, got %d/%d", record.ProxyProtocolReceive, record.ProxyProtocolSend) + } + + records, err := r.ListForwardsByTunnel(2) + if err != nil { + t.Fatalf("ListForwardsByTunnel: %v", err) + } + if len(records) != 1 { + t.Fatalf("expected 1 listed record, got %d", len(records)) + } + if records[0].ProxyProtocolReceive != 2 || records[0].ProxyProtocolSend != 1 { + t.Fatalf("expected listed proxyProtocol receive/send 2/1, got %d/%d", records[0].ProxyProtocolReceive, records[0].ProxyProtocolSend) + } +} + +func TestForwardRepositoryMapsLegacyProxyProtocolToSend(t *testing.T) { + r, err := Open(":memory:") + if err != nil { + t.Fatalf("open repo: %v", err) + } + defer r.Close() + + now := time.Now().UnixMilli() + if err := r.DB().Create(&model.Forward{ + UserID: 2, + UserName: "user", + Name: "legacy-proxy-protocol-forward", + TunnelID: 9, + RemoteAddr: "1.1.1.1:443", + Strategy: "fifo", + CreatedTime: now, + UpdatedTime: now, + Status: 1, + ProxyProtocol: 2, + }).Error; err != nil { + t.Fatalf("create legacy forward: %v", err) + } + forwardID := mustRepoLastInsertID(t, r) + + record, err := r.GetForwardRecord(forwardID) + if err != nil { + t.Fatalf("GetForwardRecord: %v", err) + } + if record.ProxyProtocolReceive != 0 || record.ProxyProtocolSend != 2 { + t.Fatalf("expected legacy proxyProtocol to map to receive/send 0/2, got %d/%d", record.ProxyProtocolReceive, record.ProxyProtocolSend) + } +} + func TestRollbackForwardFieldsRestoresPerIPLimits(t *testing.T) { r, err := Open(":memory:") if err != nil { @@ -224,15 +301,15 @@ func TestRollbackForwardFieldsRestoresPerIPLimits(t *testing.T) { defer r.Close() now := time.Now().UnixMilli() - forwardID, err := r.CreateForwardTx(1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, nil, 0, "", nil, 7, 5, int64(21), 2) + forwardID, err := r.CreateForwardTx(1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, nil, 0, "", nil, 7, 5, int64(21), 2, 0, 2) if err != nil { t.Fatalf("CreateForwardTx: %v", err) } - if err := r.UpdateForward(forwardID, "rollback-per-ip-forward", 2, "2.2.2.2:443", "fifo", now+1, nil, 0, 0, nil, 0); err != nil { + if err := r.UpdateForward(forwardID, "rollback-per-ip-forward", 2, "2.2.2.2:443", "fifo", now+1, nil, 0, 0, nil, 0, 0, 0); err != nil { t.Fatalf("UpdateForward: %v", err) } - r.RollbackForwardFields(forwardID, 1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", 1, nil, 7, 5, int64(21), 2, now+2) + r.RollbackForwardFields(forwardID, 1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", 1, nil, 7, 5, int64(21), 2, 0, 2, now+2) record, err := r.GetForwardRecord(forwardID) if err != nil { diff --git a/go-backend/internal/store/repo/repository_mutations.go b/go-backend/internal/store/repo/repository_mutations.go index 609ac5a..20bc83d 100644 --- a/go-backend/internal/store/repo/repository_mutations.go +++ b/go-backend/internal/store/repo/repository_mutations.go @@ -726,23 +726,26 @@ func (r *Repository) GetMinForwardPort(forwardID int64) sql.NullInt64 { return p } -func (r *Repository) UpdateForward(id int64, name string, tunnelID int64, remoteAddr, strategy string, now int64, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int) error { +func (r *Repository) UpdateForward(id int64, name string, tunnelID int64, remoteAddr, strategy string, now int64, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, proxyProtocolReceive int, proxyProtocolSend int) error { if r == nil || r.db == nil { return errors.New("repository not initialized") } + _, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend) return r.db.Model(&model.Forward{}). Where("id = ?", id). Updates(map[string]interface{}{ - "name": name, - "tunnel_id": tunnelID, - "remote_addr": remoteAddr, - "strategy": strategy, - "speed_id": nullInt64FromInterface(speedID), - "max_conn": maxConn, - "ip_max_conn": ipMaxConn, - "ip_speed_id": nullInt64FromInterface(ipSpeedID), - "proxy_protocol": proxyProtocol, - "updated_time": now, + "name": name, + "tunnel_id": tunnelID, + "remote_addr": remoteAddr, + "strategy": strategy, + "speed_id": nullInt64FromInterface(speedID), + "max_conn": maxConn, + "ip_max_conn": ipMaxConn, + "ip_speed_id": nullInt64FromInterface(ipSpeedID), + "proxy_protocol": proxyProtocol, + "proxy_protocol_receive": proxyProtocolReceive, + "proxy_protocol_send": proxyProtocolSend, + "updated_time": now, }).Error } @@ -819,26 +822,29 @@ func (r *Repository) UpdateForwardPortBindIP(forwardID, nodeID int64, port int, Update("in_ip", sql.NullString{String: inIP, Valid: strings.TrimSpace(inIP) != ""}).Error } -func (r *Repository) RollbackForwardFields(id, userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, status int, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, now int64) { +func (r *Repository) RollbackForwardFields(id, userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, status int, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, proxyProtocolReceive int, proxyProtocolSend int, now int64) { if r == nil || r.db == nil { return } + _, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend) _ = r.db.Model(&model.Forward{}). Where("id = ?", id). Updates(map[string]interface{}{ - "user_id": userID, - "user_name": userName, - "name": name, - "tunnel_id": tunnelID, - "remote_addr": remoteAddr, - "strategy": strategy, - "status": status, - "speed_id": nullInt64FromInterface(speedID), - "max_conn": maxConn, - "ip_max_conn": ipMaxConn, - "ip_speed_id": nullInt64FromInterface(ipSpeedID), - "proxy_protocol": proxyProtocol, - "updated_time": now, + "user_id": userID, + "user_name": userName, + "name": name, + "tunnel_id": tunnelID, + "remote_addr": remoteAddr, + "strategy": strategy, + "status": status, + "speed_id": nullInt64FromInterface(speedID), + "max_conn": maxConn, + "ip_max_conn": ipMaxConn, + "ip_speed_id": nullInt64FromInterface(ipSpeedID), + "proxy_protocol": proxyProtocol, + "proxy_protocol_receive": proxyProtocolReceive, + "proxy_protocol_send": proxyProtocolSend, + "updated_time": now, }).Error } @@ -1298,30 +1304,33 @@ func (r *Repository) EnsureUserTunnelGrant(userID, tunnelID int64) (int64, bool, return ut.ID, true, nil } -func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, inIp string, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int) (int64, error) { +func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, inIp string, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, proxyProtocolReceive int, proxyProtocolSend int) (int64, error) { if r == nil || r.db == nil { return 0, errors.New("repository not initialized") } + _, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend) var forwardID int64 err := r.db.Transaction(func(tx *gorm.DB) error { fwd := model.Forward{ - UserID: userID, - UserName: userName, - Name: name, - TunnelID: tunnelID, - RemoteAddr: remoteAddr, - Strategy: strategy, - InFlow: 0, - OutFlow: 0, - CreatedTime: now, - UpdatedTime: now, - Status: 1, - Inx: inx, - MaxConn: maxConn, - SpeedID: nullInt64FromInterface(speedID), - IPMaxConn: ipMaxConn, - IPSpeedID: nullInt64FromInterface(ipSpeedID), - ProxyProtocol: proxyProtocol, + UserID: userID, + UserName: userName, + Name: name, + TunnelID: tunnelID, + RemoteAddr: remoteAddr, + Strategy: strategy, + InFlow: 0, + OutFlow: 0, + CreatedTime: now, + UpdatedTime: now, + Status: 1, + Inx: inx, + MaxConn: maxConn, + SpeedID: nullInt64FromInterface(speedID), + IPMaxConn: ipMaxConn, + IPSpeedID: nullInt64FromInterface(ipSpeedID), + ProxyProtocol: proxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, } if err := tx.Create(&fwd).Error; err != nil { return err diff --git a/go-backend/internal/store/repo/repository_nftables.go b/go-backend/internal/store/repo/repository_nftables.go index 27d6767..26a4b9d 100644 --- a/go-backend/internal/store/repo/repository_nftables.go +++ b/go-backend/internal/store/repo/repository_nftables.go @@ -207,20 +207,23 @@ func (r *Repository) ListActiveForwardsByNode(nodeID int64) ([]model.ForwardReco } rows := make([]model.ForwardRecord, 0, len(forwards)) for _, f := range forwards { + proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend) rows = append(rows, model.ForwardRecord{ - ID: f.ID, - UserID: f.UserID, - UserName: f.UserName, - Name: f.Name, - TunnelID: f.TunnelID, - RemoteAddr: f.RemoteAddr, - Strategy: f.Strategy, - Status: f.Status, - SpeedID: f.SpeedID, - MaxConn: f.MaxConn, - IPMaxConn: f.IPMaxConn, - IPSpeedID: f.IPSpeedID, - ProxyProtocol: f.ProxyProtocol, + ID: f.ID, + UserID: f.UserID, + UserName: f.UserName, + Name: f.Name, + TunnelID: f.TunnelID, + RemoteAddr: f.RemoteAddr, + Strategy: f.Strategy, + Status: f.Status, + SpeedID: f.SpeedID, + MaxConn: f.MaxConn, + IPMaxConn: f.IPMaxConn, + IPSpeedID: f.IPSpeedID, + ProxyProtocol: f.ProxyProtocol, + ProxyProtocolReceive: proxyProtocolReceive, + ProxyProtocolSend: proxyProtocolSend, }) } for i := range rows { @@ -237,3 +240,10 @@ func defaultString(value, fallback string) string { } return value } + +func normalizeForwardProxyProtocol(legacy, receive, send int) (int, int) { + if send == 0 && legacy > 0 { + send = legacy + } + return receive, send +} diff --git a/vite-frontend/src/api/types.ts b/vite-frontend/src/api/types.ts index b112a52..37bd990 100644 --- a/vite-frontend/src/api/types.ts +++ b/vite-frontend/src/api/types.ts @@ -82,6 +82,8 @@ export interface ForwardApiItem { ipSpeedLimitName?: string; maxConn?: number; proxyProtocol?: number; + proxyProtocolReceive?: number; + proxyProtocolSend?: number; inx?: number; [key: string]: unknown; } @@ -422,6 +424,8 @@ export interface ForwardMutationPayload { ipSpeedId?: number | null; maxConn?: number; proxyProtocol?: number; + proxyProtocolReceive?: number; + proxyProtocolSend?: number; } export interface SpeedLimitMutationPayload { diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 37bf603..10d2a6c 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -130,6 +130,8 @@ interface Forward { ipSpeedId?: number | null; ipSpeedLimitName?: string; proxyProtocol?: number; + proxyProtocolReceive?: number; + proxyProtocolSend?: number; } interface Tunnel { @@ -169,6 +171,8 @@ interface ForwardForm { ipSpeedId: number | null; maxConn?: number; proxyProtocol?: number; + proxyProtocolReceive?: number; + proxyProtocolSend?: number; } interface ForwardUserGroup { @@ -600,6 +604,16 @@ const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => { typeof forward.proxyProtocol === "number" ? forward.proxyProtocol : undefined, + proxyProtocolReceive: + typeof forward.proxyProtocolReceive === "number" + ? forward.proxyProtocolReceive + : 0, + proxyProtocolSend: + typeof forward.proxyProtocolSend === "number" + ? forward.proxyProtocolSend + : typeof forward.proxyProtocol === "number" + ? forward.proxyProtocol + : 0, serviceRunning: forward.status === 1, })); }; @@ -1337,6 +1351,8 @@ export default function ForwardPage() { ipSpeedId: null, maxConn: 0, proxyProtocol: 0, + proxyProtocolReceive: 0, + proxyProtocolSend: 0, }); const [inIpTouched, setInIpTouched] = useState(false); @@ -2128,6 +2144,8 @@ export default function ForwardPage() { ipMaxConn: 0, ipSpeedId: null, proxyProtocol: 0, + proxyProtocolReceive: 0, + proxyProtocolSend: 0, }); setErrors({}); setModalOpen(true); @@ -2152,6 +2170,8 @@ export default function ForwardPage() { ipSpeedId: normalizeSpeedId(forward.ipSpeedId), maxConn: forward.maxConn ?? 0, proxyProtocol: forward.proxyProtocol ?? 0, + proxyProtocolReceive: forward.proxyProtocolReceive ?? 0, + proxyProtocolSend: forward.proxyProtocolSend ?? forward.proxyProtocol ?? 0, }); setErrors({}); setModalOpen(true); @@ -2285,7 +2305,9 @@ export default function ForwardPage() { ipMaxConn: form.ipMaxConn, ...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}), maxConn: form.maxConn, - proxyProtocol: form.proxyProtocol, + proxyProtocol: form.proxyProtocolSend, + proxyProtocolReceive: form.proxyProtocolReceive, + proxyProtocolSend: form.proxyProtocolSend, }; res = await updateForward(updateData); @@ -2301,7 +2323,9 @@ export default function ForwardPage() { ipMaxConn: form.ipMaxConn, ...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}), maxConn: form.maxConn, - proxyProtocol: form.proxyProtocol, + proxyProtocol: form.proxyProtocolSend, + proxyProtocolReceive: form.proxyProtocolReceive, + proxyProtocolSend: form.proxyProtocolSend, }; res = await createForward(createData); @@ -4968,25 +4992,50 @@ export default function ForwardPage() { setForm((prev) => ({ ...prev, ipMaxConn: value })); }} /> - { + const selectedKey = Array.from(keys)[0] as string; - setForm((prev) => ({ - ...prev, - proxyProtocol: Number(selectedKey), - })); - }} - > - 禁用 - Version 1 - Version 2 - + setForm((prev) => ({ + ...prev, + proxyProtocolReceive: Number(selectedKey), + })); + }} + > + 禁用 + Version 1 + Version 2 + + + {isAdmin && (