fix(forward): split proxy protocol directions

Closes #520
This commit is contained in:
sagit
2026-06-21 21:03:47 +08:00
committed by GitHub
parent 3ce320da5a
commit 559a793b09
13 changed files with 505 additions and 259 deletions
@@ -1757,6 +1757,7 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel
services := make([]map[string]interface{}, 0, 2) services := make([]map[string]interface{}, 0, 2)
targets := splitRemoteTargets(forward.RemoteAddr) targets := splitRemoteTargets(forward.RemoteAddr)
strategy := strings.TrimSpace(forward.Strategy) strategy := strings.TrimSpace(forward.Strategy)
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(forward.ProxyProtocol, forward.ProxyProtocolReceive, forward.ProxyProtocolSend)
if strategy == "" { if strategy == "" {
strategy = "fifo" strategy = "fifo"
} }
@@ -1801,12 +1802,16 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel
if runtimeLimiters.TrafficLimiter != "" { if runtimeLimiters.TrafficLimiter != "" {
service["limiter"] = 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{}) handlerConfig := service["handler"].(map[string]interface{})
if handlerConfig["metadata"] == nil { if handlerConfig["metadata"] == nil {
handlerConfig["metadata"] = map[string]interface{}{} handlerConfig["metadata"] = map[string]interface{}{}
} }
handlerConfig["metadata"].(map[string]interface{})["proxyProtocol"] = forward.ProxyProtocol handlerConfig["metadata"].(map[string]interface{})["proxyProtocol"] = proxyProtocolSend
} }
if protocol == "udp" { if protocol == "udp" {
listenerMetadata := map[string]interface{}{ 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) service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", forward.TunnelID)
} }
if tunnel != nil && tunnel.Type == 1 && strings.TrimSpace(node.InterfaceName) != "" { if tunnel != nil && tunnel.Type == 1 && strings.TrimSpace(node.InterfaceName) != "" {
if service["metadata"] == nil { serviceMetadata := ensureServiceMetadata(service)
service["metadata"] = map[string]interface{}{} serviceMetadata["interface"] = node.InterfaceName
}
service["metadata"].(map[string]interface{})["interface"] = node.InterfaceName
} }
services = append(services, service) services = append(services, service)
} }
@@ -1841,6 +1844,25 @@ func buildForwarderNodes(targets []string) []map[string]interface{} {
return nodes 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 { func processServerAddress(serverAddr string) string {
serverAddr = normalizeServerAddressInput(serverAddr) serverAddr = normalizeServerAddressInput(serverAddr)
if serverAddr == "" { if serverAddr == "" {
@@ -9,14 +9,15 @@ import (
"go-backend/internal/store/repo" "go-backend/internal/store/repo"
) )
func TestBuildForwardServiceConfigsSendsProxyProtocolToForwardHandler(t *testing.T) { func TestBuildForwardServiceConfigsAppliesProxyProtocolReceiveAndSendIndependently(t *testing.T) {
forward := &forwardRecord{ forward := &forwardRecord{
ID: 1, ID: 1,
UserID: 2, UserID: 2,
TunnelID: 3, TunnelID: 3,
RemoteAddr: "1.1.1.1:443", RemoteAddr: "1.1.1.1:443",
Strategy: "fifo", Strategy: "fifo",
ProxyProtocol: 2, ProxyProtocolReceive: 1,
ProxyProtocolSend: 2,
} }
tunnel := &tunnelRecord{Type: 1} tunnel := &tunnelRecord{Type: 1}
node := &nodeRecord{ node := &nodeRecord{
@@ -38,8 +39,8 @@ func TestBuildForwardServiceConfigsSendsProxyProtocolToForwardHandler(t *testing
if serviceMetadata["interface"] != "eth0" { if serviceMetadata["interface"] != "eth0" {
t.Fatalf("expected interface metadata eth0, got %v", serviceMetadata["interface"]) t.Fatalf("expected interface metadata eth0, got %v", serviceMetadata["interface"])
} }
if _, ok := serviceMetadata["proxyProtocol"]; ok { if serviceMetadata["proxyProtocol"] != 1 {
t.Fatalf("proxyProtocol should not be listener metadata: %v", serviceMetadata) t.Fatalf("expected service proxyProtocol 1 for receive mode, got %v", serviceMetadata["proxyProtocol"])
} }
handlerConfig, ok := service["handler"].(map[string]interface{}) 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) { func TestRollbackForwardMutationRestoresProxyProtocol(t *testing.T) {
r, err := repo.Open(":memory:") r, err := repo.Open(":memory:")
if err != nil { if err != nil {
@@ -2143,8 +2143,10 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
ipMaxConn = 0 ipMaxConn = 0
} }
proxyProtocol := asInt(req["proxyProtocol"], 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 { if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error())) response.WriteJSON(w, response.Err(-2, err.Error()))
return return
@@ -2333,8 +2335,10 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
ipMaxConn = 0 ipMaxConn = 0
} }
proxyProtocol := asInt(req["proxyProtocol"], forward.ProxyProtocol) 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())) response.WriteJSON(w, response.Err(-2, err.Error()))
return return
} }
@@ -4722,7 +4726,7 @@ func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts []
h.repo.RollbackForwardFields( h.repo.RollbackForwardFields(
oldForward.ID, oldForward.UserID, oldForward.UserName, oldForward.Name, oldForward.ID, oldForward.UserID, oldForward.UserName, oldForward.Name,
oldForward.TunnelID, oldForward.RemoteAddr, oldForward.Strategy, oldForward.Status, 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(), time.Now().UnixMilli(),
) )
@@ -490,7 +490,7 @@ func seedForwardForNftables(t *testing.T, h *Handler, tunnelID, nodeID int64, re
now := time.Now().UnixMilli() now := time.Now().UnixMilli()
forwardID, err := h.repo.CreateForwardTx( forwardID, err := h.repo.CreateForwardTx(
1, "admin", "nft-forward", tunnelID, remoteAddr, "fifo", now, 1, 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 { if err != nil {
t.Fatalf("create forward: %v", err) t.Fatalf("create forward: %v", err)
+55 -49
View File
@@ -31,24 +31,26 @@ func (User) TableName() string { return "user" }
// Forward maps to the "forward" table. // Forward maps to the "forward" table.
type Forward struct { type Forward struct {
ID int64 `gorm:"primaryKey;autoIncrement"` ID int64 `gorm:"primaryKey;autoIncrement"`
UserID int64 `gorm:"column:user_id;not null"` UserID int64 `gorm:"column:user_id;not null"`
UserName string `gorm:"column:user_name;type:varchar(100);not null"` UserName string `gorm:"column:user_name;type:varchar(100);not null"`
Name string `gorm:"type:varchar(100);not null"` Name string `gorm:"type:varchar(100);not null"`
TunnelID int64 `gorm:"column:tunnel_id;not null"` TunnelID int64 `gorm:"column:tunnel_id;not null"`
RemoteAddr string `gorm:"column:remote_addr;type:text;not null"` RemoteAddr string `gorm:"column:remote_addr;type:text;not null"`
Strategy string `gorm:"type:varchar(100);not null;default:'fifo'"` Strategy string `gorm:"type:varchar(100);not null;default:'fifo'"`
InFlow int64 `gorm:"not null;default:0"` InFlow int64 `gorm:"not null;default:0"`
OutFlow int64 `gorm:"column:out_flow;not null;default:0"` OutFlow int64 `gorm:"column:out_flow;not null;default:0"`
CreatedTime int64 `gorm:"column:created_time;not null"` CreatedTime int64 `gorm:"column:created_time;not null"`
UpdatedTime int64 `gorm:"column:updated_time;not null"` UpdatedTime int64 `gorm:"column:updated_time;not null"`
Status int `gorm:"not null"` Status int `gorm:"not null"`
Inx int `gorm:"not null;default:0"` Inx int `gorm:"not null;default:0"`
SpeedID sql.NullInt64 `gorm:"column:speed_id"` SpeedID sql.NullInt64 `gorm:"column:speed_id"`
MaxConn int `gorm:"column:max_conn;not null;default:0"` MaxConn int `gorm:"column:max_conn;not null;default:0"`
IPMaxConn int `gorm:"column:ip_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"` IPSpeedID sql.NullInt64 `gorm:"column:ip_speed_id"`
ProxyProtocol int `gorm:"column:proxy_protocol;not null;default:0"` 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" } func (Forward) TableName() string { return "forward" }
@@ -487,24 +489,26 @@ type ChainTunnelBackup struct {
} }
type ForwardBackup struct { type ForwardBackup struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"userId"` UserID int64 `json:"userId"`
UserName string `json:"userName"` UserName string `json:"userName"`
Name string `json:"name"` Name string `json:"name"`
TunnelID int64 `json:"tunnelId"` TunnelID int64 `json:"tunnelId"`
RemoteAddr string `json:"remoteAddr"` RemoteAddr string `json:"remoteAddr"`
Strategy string `json:"strategy"` Strategy string `json:"strategy"`
InFlow int64 `json:"inFlow"` InFlow int64 `json:"inFlow"`
OutFlow int64 `json:"outFlow"` OutFlow int64 `json:"outFlow"`
CreatedTime int64 `json:"createdTime"` CreatedTime int64 `json:"createdTime"`
UpdatedTime int64 `json:"updatedTime"` UpdatedTime int64 `json:"updatedTime"`
Status int `json:"status"` Status int `json:"status"`
Inx int `json:"inx"` Inx int `json:"inx"`
SpeedID *int64 `json:"speedId,omitempty"` SpeedID *int64 `json:"speedId,omitempty"`
IPMaxConn int `json:"ipMaxConn,omitempty"` IPMaxConn int `json:"ipMaxConn,omitempty"`
IPSpeedID *int64 `json:"ipSpeedId,omitempty"` IPSpeedID *int64 `json:"ipSpeedId,omitempty"`
ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"` ForwardPorts *[]ForwardPortBackup `json:"forwardPorts,omitempty"`
ProxyProtocol int `json:"proxyProtocol"` ProxyProtocol int `json:"proxyProtocol"`
ProxyProtocolReceive int `json:"proxyProtocolReceive,omitempty"`
ProxyProtocolSend int `json:"proxyProtocolSend,omitempty"`
} }
type ForwardPortBackup struct { type ForwardPortBackup struct {
@@ -593,19 +597,21 @@ type ImportResult struct {
// ForwardRecord is a minimal forward view used by control plane and flow policy. // ForwardRecord is a minimal forward view used by control plane and flow policy.
type ForwardRecord struct { type ForwardRecord struct {
ID int64 ID int64
UserID int64 UserID int64
UserName string UserName string
Name string Name string
TunnelID int64 TunnelID int64
RemoteAddr string RemoteAddr string
Strategy string Strategy string
Status int Status int
SpeedID sql.NullInt64 SpeedID sql.NullInt64
MaxConn int MaxConn int
IPMaxConn int IPMaxConn int
IPSpeedID sql.NullInt64 IPSpeedID sql.NullInt64
ProxyProtocol int ProxyProtocol int
ProxyProtocolReceive int
ProxyProtocolSend int
} }
// TunnelRecord is a minimal tunnel view used by control plane. // TunnelRecord is a minimal tunnel view used by control plane.
+54 -45
View File
@@ -424,7 +424,7 @@ func prepareSQLiteLegacyColumns(db *gorm.DB) error {
} }
if m.HasTable(&model.Forward{}) { 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) { if m.HasColumn(&model.Forward{}, field) {
continue continue
} }
@@ -944,31 +944,33 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
} }
type fwdRow struct { type fwdRow struct {
ID int64 ID int64
UserID int64 UserID int64
UserName string UserName string
Name string Name string
TunnelID int64 TunnelID int64
TunnelName string TunnelName string
TrafficRatio float64 TrafficRatio float64
RemoteAddr string RemoteAddr string
Strategy string Strategy string
InFlow int64 InFlow int64
OutFlow int64 OutFlow int64
CreatedTime int64 CreatedTime int64
Status int Status int
Inx int Inx int
SpeedID sql.NullInt64 SpeedID sql.NullInt64
MaxConn int MaxConn int
IPMaxConn int IPMaxConn int
IPSpeedID sql.NullInt64 IPSpeedID sql.NullInt64
IPSpeedLimitName string IPSpeedLimitName string
ProxyProtocol int ProxyProtocol int
ProxyProtocolReceive int
ProxyProtocolSend int
} }
var rows []fwdRow var rows []fwdRow
err := r.db.Model(&model.Forward{}). 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 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"). 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"). 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)) items := make([]map[string]interface{}, 0, len(rows))
for _, row := range rows { for _, row := range rows {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(row.ProxyProtocol, row.ProxyProtocolReceive, row.ProxyProtocolSend)
inIP, inPort, err := resolveForwardIngress(r.db, row.ID, row.TunnelID) inIP, inPort, err := resolveForwardIngress(r.db, row.ID, row.TunnelID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -991,9 +994,11 @@ func (r *Repository) ListForwards() ([]map[string]interface{}, error) {
"remoteAddr": row.RemoteAddr, "strategy": row.Strategy, "remoteAddr": row.RemoteAddr, "strategy": row.Strategy,
"inFlow": row.InFlow, "outFlow": row.OutFlow, "inFlow": row.InFlow, "outFlow": row.OutFlow,
"createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx), "createdTime": row.CreatedTime, "status": row.Status, "inx": int64(row.Inx),
"maxConn": row.MaxConn, "maxConn": row.MaxConn,
"ipMaxConn": row.IPMaxConn, "ipMaxConn": row.IPMaxConn,
"proxyProtocol": row.ProxyProtocol, "proxyProtocol": row.ProxyProtocol,
"proxyProtocolReceive": proxyProtocolReceive,
"proxyProtocolSend": proxyProtocolSend,
} }
if row.SpeedID.Valid { if row.SpeedID.Valid {
item["speedId"] = row.SpeedID.Int64 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, TunnelID: f.TunnelID, RemoteAddr: f.RemoteAddr, Strategy: f.Strategy,
InFlow: f.InFlow, OutFlow: f.OutFlow, CreatedTime: f.CreatedTime, InFlow: f.InFlow, OutFlow: f.OutFlow, CreatedTime: f.CreatedTime,
UpdatedTime: f.UpdatedTime, Status: f.Status, Inx: f.Inx, UpdatedTime: f.UpdatedTime, Status: f.Status, Inx: f.Inx,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: f.ProxyProtocolReceive,
ProxyProtocolSend: f.ProxyProtocolSend,
} }
if f.SpeedID.Valid { if f.SpeedID.Valid {
v := f.SpeedID.Int64 v := f.SpeedID.Int64
@@ -2630,29 +2637,31 @@ func importForwards(tx *gorm.DB, forwards []model.ForwardBackup, now int64) (int
count := 0 count := 0
for _, f := range forwards { for _, f := range forwards {
item := model.Forward{ item := model.Forward{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
InFlow: f.InFlow, InFlow: f.InFlow,
OutFlow: f.OutFlow, OutFlow: f.OutFlow,
CreatedTime: f.CreatedTime, CreatedTime: f.CreatedTime,
UpdatedTime: now, UpdatedTime: now,
Status: f.Status, Status: f.Status,
Inx: f.Inx, Inx: f.Inx,
SpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.SpeedID), Valid: f.SpeedID != nil && *f.SpeedID > 0}, SpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.SpeedID), Valid: f.SpeedID != nil && *f.SpeedID > 0},
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.IPSpeedID), Valid: f.IPSpeedID != nil && *f.IPSpeedID > 0}, IPSpeedID: sql.NullInt64{Int64: nullableBackupInt64(f.IPSpeedID), Valid: f.IPSpeedID != nil && *f.IPSpeedID > 0},
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: f.ProxyProtocolReceive,
ProxyProtocolSend: f.ProxyProtocolSend,
} }
err := tx.Clauses(clause.OnConflict{ err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}}, Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{ DoUpdates: clause.AssignmentColumns([]string{
"user_id", "user_name", "name", "tunnel_id", "remote_addr", "strategy", "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 }).Create(&item).Error
if err != nil { if err != nil {
@@ -44,20 +44,23 @@ func (r *Repository) ListForwardsByTunnelTx(tx *gorm.DB, tunnelID int64) ([]mode
} }
rows := make([]model.ForwardRecord, 0, len(forwards)) rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards { for _, f := range forwards {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
rows = append(rows, model.ForwardRecord{ rows = append(rows, model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
}) })
} }
for i := range rows { for i := range rows {
@@ -120,20 +120,23 @@ func (r *Repository) ListActiveForwardsByUser(userID int64) ([]model.ForwardReco
} }
rows := make([]model.ForwardRecord, 0, len(forwards)) rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards { for _, f := range forwards {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
rows = append(rows, model.ForwardRecord{ rows = append(rows, model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
}) })
} }
for i := range rows { for i := range rows {
@@ -155,20 +158,23 @@ func (r *Repository) ListActiveForwardsByUserTunnel(userID, tunnelID int64) ([]m
} }
rows := make([]model.ForwardRecord, 0, len(forwards)) rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards { for _, f := range forwards {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
rows = append(rows, model.ForwardRecord{ rows = append(rows, model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
}) })
} }
for i := range rows { for i := range rows {
@@ -190,20 +196,23 @@ func (r *Repository) ListForwardsByUserAndTunnel(userID, tunnelID int64) ([]mode
} }
rows := make([]model.ForwardRecord, 0, len(forwards)) rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards { for _, f := range forwards {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
rows = append(rows, model.ForwardRecord{ rows = append(rows, model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
}) })
} }
for i := range rows { for i := range rows {
@@ -226,20 +235,23 @@ func (r *Repository) GetForwardRecord(forwardID int64) (*model.ForwardRecord, er
} }
return nil, err return nil, err
} }
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
fr := model.ForwardRecord{ fr := model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
} }
if strings.TrimSpace(fr.Strategy) == "" { if strings.TrimSpace(fr.Strategy) == "" {
fr.Strategy = "fifo" fr.Strategy = "fifo"
@@ -160,7 +160,7 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) {
defer r.Close() defer r.Close()
now := time.Now().UnixMilli() 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 { if err != nil {
t.Fatalf("CreateForwardTx: %v", err) t.Fatalf("CreateForwardTx: %v", err)
} }
@@ -175,7 +175,7 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) {
t.Fatalf("expected created ipSpeedId 21, got %+v", record.IPSpeedID) 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) t.Fatalf("UpdateForward: %v", err)
} }
record, err = r.GetForwardRecord(forwardID) 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) { func TestRollbackForwardFieldsRestoresPerIPLimits(t *testing.T) {
r, err := Open(":memory:") r, err := Open(":memory:")
if err != nil { if err != nil {
@@ -224,15 +301,15 @@ func TestRollbackForwardFieldsRestoresPerIPLimits(t *testing.T) {
defer r.Close() defer r.Close()
now := time.Now().UnixMilli() 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 { if err != nil {
t.Fatalf("CreateForwardTx: %v", err) 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) 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) record, err := r.GetForwardRecord(forwardID)
if err != nil { if err != nil {
@@ -726,23 +726,26 @@ func (r *Repository) GetMinForwardPort(forwardID int64) sql.NullInt64 {
return p 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 { if r == nil || r.db == nil {
return errors.New("repository not initialized") return errors.New("repository not initialized")
} }
_, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
return r.db.Model(&model.Forward{}). return r.db.Model(&model.Forward{}).
Where("id = ?", id). Where("id = ?", id).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"name": name, "name": name,
"tunnel_id": tunnelID, "tunnel_id": tunnelID,
"remote_addr": remoteAddr, "remote_addr": remoteAddr,
"strategy": strategy, "strategy": strategy,
"speed_id": nullInt64FromInterface(speedID), "speed_id": nullInt64FromInterface(speedID),
"max_conn": maxConn, "max_conn": maxConn,
"ip_max_conn": ipMaxConn, "ip_max_conn": ipMaxConn,
"ip_speed_id": nullInt64FromInterface(ipSpeedID), "ip_speed_id": nullInt64FromInterface(ipSpeedID),
"proxy_protocol": proxyProtocol, "proxy_protocol": proxyProtocol,
"updated_time": now, "proxy_protocol_receive": proxyProtocolReceive,
"proxy_protocol_send": proxyProtocolSend,
"updated_time": now,
}).Error }).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 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 { if r == nil || r.db == nil {
return return
} }
_, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
_ = r.db.Model(&model.Forward{}). _ = r.db.Model(&model.Forward{}).
Where("id = ?", id). Where("id = ?", id).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"user_id": userID, "user_id": userID,
"user_name": userName, "user_name": userName,
"name": name, "name": name,
"tunnel_id": tunnelID, "tunnel_id": tunnelID,
"remote_addr": remoteAddr, "remote_addr": remoteAddr,
"strategy": strategy, "strategy": strategy,
"status": status, "status": status,
"speed_id": nullInt64FromInterface(speedID), "speed_id": nullInt64FromInterface(speedID),
"max_conn": maxConn, "max_conn": maxConn,
"ip_max_conn": ipMaxConn, "ip_max_conn": ipMaxConn,
"ip_speed_id": nullInt64FromInterface(ipSpeedID), "ip_speed_id": nullInt64FromInterface(ipSpeedID),
"proxy_protocol": proxyProtocol, "proxy_protocol": proxyProtocol,
"updated_time": now, "proxy_protocol_receive": proxyProtocolReceive,
"proxy_protocol_send": proxyProtocolSend,
"updated_time": now,
}).Error }).Error
} }
@@ -1298,30 +1304,33 @@ func (r *Repository) EnsureUserTunnelGrant(userID, tunnelID int64) (int64, bool,
return ut.ID, true, nil 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 { if r == nil || r.db == nil {
return 0, errors.New("repository not initialized") return 0, errors.New("repository not initialized")
} }
_, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
var forwardID int64 var forwardID int64
err := r.db.Transaction(func(tx *gorm.DB) error { err := r.db.Transaction(func(tx *gorm.DB) error {
fwd := model.Forward{ fwd := model.Forward{
UserID: userID, UserID: userID,
UserName: userName, UserName: userName,
Name: name, Name: name,
TunnelID: tunnelID, TunnelID: tunnelID,
RemoteAddr: remoteAddr, RemoteAddr: remoteAddr,
Strategy: strategy, Strategy: strategy,
InFlow: 0, InFlow: 0,
OutFlow: 0, OutFlow: 0,
CreatedTime: now, CreatedTime: now,
UpdatedTime: now, UpdatedTime: now,
Status: 1, Status: 1,
Inx: inx, Inx: inx,
MaxConn: maxConn, MaxConn: maxConn,
SpeedID: nullInt64FromInterface(speedID), SpeedID: nullInt64FromInterface(speedID),
IPMaxConn: ipMaxConn, IPMaxConn: ipMaxConn,
IPSpeedID: nullInt64FromInterface(ipSpeedID), IPSpeedID: nullInt64FromInterface(ipSpeedID),
ProxyProtocol: proxyProtocol, ProxyProtocol: proxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
} }
if err := tx.Create(&fwd).Error; err != nil { if err := tx.Create(&fwd).Error; err != nil {
return err return err
@@ -207,20 +207,23 @@ func (r *Repository) ListActiveForwardsByNode(nodeID int64) ([]model.ForwardReco
} }
rows := make([]model.ForwardRecord, 0, len(forwards)) rows := make([]model.ForwardRecord, 0, len(forwards))
for _, f := range forwards { for _, f := range forwards {
proxyProtocolReceive, proxyProtocolSend := normalizeForwardProxyProtocol(f.ProxyProtocol, f.ProxyProtocolReceive, f.ProxyProtocolSend)
rows = append(rows, model.ForwardRecord{ rows = append(rows, model.ForwardRecord{
ID: f.ID, ID: f.ID,
UserID: f.UserID, UserID: f.UserID,
UserName: f.UserName, UserName: f.UserName,
Name: f.Name, Name: f.Name,
TunnelID: f.TunnelID, TunnelID: f.TunnelID,
RemoteAddr: f.RemoteAddr, RemoteAddr: f.RemoteAddr,
Strategy: f.Strategy, Strategy: f.Strategy,
Status: f.Status, Status: f.Status,
SpeedID: f.SpeedID, SpeedID: f.SpeedID,
MaxConn: f.MaxConn, MaxConn: f.MaxConn,
IPMaxConn: f.IPMaxConn, IPMaxConn: f.IPMaxConn,
IPSpeedID: f.IPSpeedID, IPSpeedID: f.IPSpeedID,
ProxyProtocol: f.ProxyProtocol, ProxyProtocol: f.ProxyProtocol,
ProxyProtocolReceive: proxyProtocolReceive,
ProxyProtocolSend: proxyProtocolSend,
}) })
} }
for i := range rows { for i := range rows {
@@ -237,3 +240,10 @@ func defaultString(value, fallback string) string {
} }
return value return value
} }
func normalizeForwardProxyProtocol(legacy, receive, send int) (int, int) {
if send == 0 && legacy > 0 {
send = legacy
}
return receive, send
}
+4
View File
@@ -82,6 +82,8 @@ export interface ForwardApiItem {
ipSpeedLimitName?: string; ipSpeedLimitName?: string;
maxConn?: number; maxConn?: number;
proxyProtocol?: number; proxyProtocol?: number;
proxyProtocolReceive?: number;
proxyProtocolSend?: number;
inx?: number; inx?: number;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -422,6 +424,8 @@ export interface ForwardMutationPayload {
ipSpeedId?: number | null; ipSpeedId?: number | null;
maxConn?: number; maxConn?: number;
proxyProtocol?: number; proxyProtocol?: number;
proxyProtocolReceive?: number;
proxyProtocolSend?: number;
} }
export interface SpeedLimitMutationPayload { export interface SpeedLimitMutationPayload {
+69 -20
View File
@@ -130,6 +130,8 @@ interface Forward {
ipSpeedId?: number | null; ipSpeedId?: number | null;
ipSpeedLimitName?: string; ipSpeedLimitName?: string;
proxyProtocol?: number; proxyProtocol?: number;
proxyProtocolReceive?: number;
proxyProtocolSend?: number;
} }
interface Tunnel { interface Tunnel {
@@ -169,6 +171,8 @@ interface ForwardForm {
ipSpeedId: number | null; ipSpeedId: number | null;
maxConn?: number; maxConn?: number;
proxyProtocol?: number; proxyProtocol?: number;
proxyProtocolReceive?: number;
proxyProtocolSend?: number;
} }
interface ForwardUserGroup { interface ForwardUserGroup {
@@ -600,6 +604,16 @@ const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => {
typeof forward.proxyProtocol === "number" typeof forward.proxyProtocol === "number"
? forward.proxyProtocol ? forward.proxyProtocol
: undefined, : 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, serviceRunning: forward.status === 1,
})); }));
}; };
@@ -1337,6 +1351,8 @@ export default function ForwardPage() {
ipSpeedId: null, ipSpeedId: null,
maxConn: 0, maxConn: 0,
proxyProtocol: 0, proxyProtocol: 0,
proxyProtocolReceive: 0,
proxyProtocolSend: 0,
}); });
const [inIpTouched, setInIpTouched] = useState(false); const [inIpTouched, setInIpTouched] = useState(false);
@@ -2128,6 +2144,8 @@ export default function ForwardPage() {
ipMaxConn: 0, ipMaxConn: 0,
ipSpeedId: null, ipSpeedId: null,
proxyProtocol: 0, proxyProtocol: 0,
proxyProtocolReceive: 0,
proxyProtocolSend: 0,
}); });
setErrors({}); setErrors({});
setModalOpen(true); setModalOpen(true);
@@ -2152,6 +2170,8 @@ export default function ForwardPage() {
ipSpeedId: normalizeSpeedId(forward.ipSpeedId), ipSpeedId: normalizeSpeedId(forward.ipSpeedId),
maxConn: forward.maxConn ?? 0, maxConn: forward.maxConn ?? 0,
proxyProtocol: forward.proxyProtocol ?? 0, proxyProtocol: forward.proxyProtocol ?? 0,
proxyProtocolReceive: forward.proxyProtocolReceive ?? 0,
proxyProtocolSend: forward.proxyProtocolSend ?? forward.proxyProtocol ?? 0,
}); });
setErrors({}); setErrors({});
setModalOpen(true); setModalOpen(true);
@@ -2285,7 +2305,9 @@ export default function ForwardPage() {
ipMaxConn: form.ipMaxConn, ipMaxConn: form.ipMaxConn,
...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}), ...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}),
maxConn: form.maxConn, maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol, proxyProtocol: form.proxyProtocolSend,
proxyProtocolReceive: form.proxyProtocolReceive,
proxyProtocolSend: form.proxyProtocolSend,
}; };
res = await updateForward(updateData); res = await updateForward(updateData);
@@ -2301,7 +2323,9 @@ export default function ForwardPage() {
ipMaxConn: form.ipMaxConn, ipMaxConn: form.ipMaxConn,
...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}), ...(isAdmin ? { ipSpeedId: normalizedIPSpeedId } : {}),
maxConn: form.maxConn, maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol, proxyProtocol: form.proxyProtocolSend,
proxyProtocolReceive: form.proxyProtocolReceive,
proxyProtocolSend: form.proxyProtocolSend,
}; };
res = await createForward(createData); res = await createForward(createData);
@@ -4968,25 +4992,50 @@ export default function ForwardPage() {
setForm((prev) => ({ ...prev, ipMaxConn: value })); setForm((prev) => ({ ...prev, ipMaxConn: value }));
}} }}
/> />
<Select <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
description="启用 PROXY protocol,用于透传客户端真实 IP" <Select
label="Proxy Protocol" description="入口监听接收 PROXY protocol,用于读取上游传入的真实客户端 IP。"
placeholder="禁用" label="Proxy Protocol 接收"
selectedKeys={[String(form.proxyProtocol || 0)]} placeholder="禁用"
variant="bordered" selectedKeys={[
onSelectionChange={(keys) => { String(form.proxyProtocolReceive || 0),
const selectedKey = Array.from(keys)[0] as string; ]}
variant="bordered"
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as string;
setForm((prev) => ({ setForm((prev) => ({
...prev, ...prev,
proxyProtocol: Number(selectedKey), proxyProtocolReceive: Number(selectedKey),
})); }));
}} }}
> >
<SelectItem key="0">禁用</SelectItem> <SelectItem key="0">禁用</SelectItem>
<SelectItem key="1">Version 1</SelectItem> <SelectItem key="1">Version 1</SelectItem>
<SelectItem key="2">Version 2</SelectItem> <SelectItem key="2">Version 2</SelectItem>
</Select> </Select>
<Select
description="连接目标地址时发送 PROXY protocol,用于向下游透传客户端真实 IP。"
label="Proxy Protocol 发送"
placeholder="禁用"
selectedKeys={[String(form.proxyProtocolSend || 0)]}
variant="bordered"
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as string;
const proxyProtocolSend = Number(selectedKey);
setForm((prev) => ({
...prev,
proxyProtocol: proxyProtocolSend,
proxyProtocolSend,
}));
}}
>
<SelectItem key="0">禁用</SelectItem>
<SelectItem key="1">Version 1</SelectItem>
<SelectItem key="2">Version 2</SelectItem>
</Select>
</div>
{isAdmin && ( {isAdmin && (
<Select <Select
label="规则限速" label="规则限速"