mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 10:16:38 +08:00
feat: persist tunnel probe targets
This commit is contained in:
@@ -607,6 +607,17 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
trafficRatio := asFloat(req["trafficRatio"], 1.0)
|
trafficRatio := asFloat(req["trafficRatio"], 1.0)
|
||||||
inIP := asString(req["inIp"])
|
inIP := asString(req["inIp"])
|
||||||
ipPreference := asString(req["ipPreference"])
|
ipPreference := asString(req["ipPreference"])
|
||||||
|
probeTarget, probeTargetConfigured, err := parseTunnelProbeTargetFromRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
probeTargetHost := ""
|
||||||
|
probeTargetPort := 0
|
||||||
|
if probeTargetConfigured {
|
||||||
|
probeTargetHost = probeTarget.Host
|
||||||
|
probeTargetPort = probeTarget.Port
|
||||||
|
}
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
inx := h.repo.NextIndex("tunnel")
|
inx := h.repo.NextIndex("tunnel")
|
||||||
localDomain := h.federationLocalDomain()
|
localDomain := h.federationLocalDomain()
|
||||||
@@ -685,17 +696,19 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
tunnelProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
|
tunnelProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
|
||||||
}
|
}
|
||||||
tunnel := model.Tunnel{
|
tunnel := model.Tunnel{
|
||||||
Name: name,
|
Name: name,
|
||||||
TrafficRatio: trafficRatio,
|
TrafficRatio: trafficRatio,
|
||||||
Type: typeVal,
|
Type: typeVal,
|
||||||
Protocol: tunnelProtocol,
|
Protocol: tunnelProtocol,
|
||||||
Flow: flow,
|
Flow: flow,
|
||||||
CreatedTime: now,
|
CreatedTime: now,
|
||||||
UpdatedTime: now,
|
UpdatedTime: now,
|
||||||
Status: status,
|
Status: status,
|
||||||
InIP: tunnelInIP,
|
InIP: tunnelInIP,
|
||||||
Inx: inx,
|
Inx: inx,
|
||||||
IPPreference: ipPreference,
|
IPPreference: ipPreference,
|
||||||
|
ProbeTargetHost: probeTargetHost,
|
||||||
|
ProbeTargetPort: probeTargetPort,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&tunnel).Error; err != nil {
|
if err := tx.Create(&tunnel).Error; err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
@@ -882,6 +895,17 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
ipPreference := asString(req["ipPreference"])
|
ipPreference := asString(req["ipPreference"])
|
||||||
|
probeTarget, probeTargetConfigured, err := parseTunnelProbeTargetFromRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
response.WriteJSON(w, response.ErrDefault(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
probeTargetHost := ""
|
||||||
|
probeTargetPort := 0
|
||||||
|
if probeTargetConfigured {
|
||||||
|
probeTargetHost = probeTarget.Host
|
||||||
|
probeTargetPort = probeTarget.Port
|
||||||
|
}
|
||||||
localDomain := h.federationLocalDomain()
|
localDomain := h.federationLocalDomain()
|
||||||
|
|
||||||
runtimeState, err := h.prepareTunnelCreateState(h.repo.DB(), req, typeVal, id)
|
runtimeState, err := h.prepareTunnelCreateState(h.repo.DB(), req, typeVal, id)
|
||||||
@@ -928,6 +952,8 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
inIp,
|
inIp,
|
||||||
ipPreference,
|
ipPreference,
|
||||||
updateProtocol,
|
updateProtocol,
|
||||||
|
probeTargetHost,
|
||||||
|
probeTargetPort,
|
||||||
now,
|
now,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func effectiveTunnelProbeTarget(tunnel *model.Tunnel) tunnelProbeTarget {
|
|||||||
if tunnel == nil {
|
if tunnel == nil {
|
||||||
return defaultTunnelProbeTarget()
|
return defaultTunnelProbeTarget()
|
||||||
}
|
}
|
||||||
return defaultTunnelProbeTarget()
|
return effectiveTunnelProbeTargetValues(tunnel.ProbeTargetHost, tunnel.ProbeTargetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
func effectiveTunnelProbeTargetValues(host string, port int) tunnelProbeTarget {
|
func effectiveTunnelProbeTargetValues(host string, port int) tunnelProbeTarget {
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend/internal/store/repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTunnelCreatePersistsProbeTargetAndListReturnsConfiguredValue(t *testing.T) {
|
||||||
|
h := setupProbeTargetTunnelHandler(t)
|
||||||
|
body := bytes.NewReader([]byte(`{
|
||||||
|
"name":"custom-target",
|
||||||
|
"type":1,
|
||||||
|
"flow":1,
|
||||||
|
"trafficRatio":1,
|
||||||
|
"status":1,
|
||||||
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
||||||
|
"probeTargetHost":"speed.example.com",
|
||||||
|
"probeTargetPort":8443
|
||||||
|
}`))
|
||||||
|
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
h.tunnelCreate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", body))
|
||||||
|
assertProbeTargetSuccess(t, res)
|
||||||
|
|
||||||
|
listRes := httptest.NewRecorder()
|
||||||
|
h.tunnelList(listRes, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/list", nil))
|
||||||
|
var payload struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Data []map[string]any `json:"data"`
|
||||||
|
}
|
||||||
|
decodeProbeTargetResponse(t, listRes, &payload)
|
||||||
|
if payload.Code != 0 {
|
||||||
|
t.Fatalf("expected success, got code %d", payload.Code)
|
||||||
|
}
|
||||||
|
item := payload.Data[0]
|
||||||
|
if item["probeTargetHost"] != "speed.example.com" || item["probeTargetPort"] != float64(8443) {
|
||||||
|
t.Fatalf("unexpected probe target in list response: %+v", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelUpdatePersistsDefaultProbeTargetAsEmpty(t *testing.T) {
|
||||||
|
h := setupProbeTargetTunnelHandler(t)
|
||||||
|
seedProbeTargetTunnel(t, h, 77, "existing", "old.example.com", 9443)
|
||||||
|
body := bytes.NewReader([]byte(`{
|
||||||
|
"id":77,
|
||||||
|
"name":"existing",
|
||||||
|
"type":1,
|
||||||
|
"flow":1,
|
||||||
|
"trafficRatio":1,
|
||||||
|
"status":1,
|
||||||
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
||||||
|
"probeTargetHost":"",
|
||||||
|
"probeTargetPort":0
|
||||||
|
}`))
|
||||||
|
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
h.tunnelUpdate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", body))
|
||||||
|
assertProbeTargetSuccess(t, res)
|
||||||
|
|
||||||
|
items, err := h.repo.ListTunnels()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tunnels: %v", err)
|
||||||
|
}
|
||||||
|
item := findProbeTargetTunnelItem(t, items, 77)
|
||||||
|
if item["probeTargetHost"] != "" || item["probeTargetPort"] != 0 {
|
||||||
|
t.Fatalf("expected default target to round-trip as empty/0, got %+v", item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunnelCreateRejectsInvalidProbeTarget(t *testing.T) {
|
||||||
|
h := setupProbeTargetTunnelHandler(t)
|
||||||
|
body := bytes.NewReader([]byte(`{
|
||||||
|
"name":"bad-target",
|
||||||
|
"type":1,
|
||||||
|
"flow":1,
|
||||||
|
"trafficRatio":1,
|
||||||
|
"status":1,
|
||||||
|
"inNodeId":[{"nodeId":10,"protocol":"tls"}],
|
||||||
|
"probeTargetHost":"https://example.com",
|
||||||
|
"probeTargetPort":443
|
||||||
|
}`))
|
||||||
|
|
||||||
|
res := httptest.NewRecorder()
|
||||||
|
h.tunnelCreate(res, httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", body))
|
||||||
|
var payload struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
decodeProbeTargetResponse(t, res, &payload)
|
||||||
|
if payload.Code == 0 || payload.Msg == "" {
|
||||||
|
t.Fatalf("expected validation failure, got %+v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupProbeTargetTunnelHandler(t *testing.T) *Handler {
|
||||||
|
t.Helper()
|
||||||
|
r, err := repo.Open(filepath.Join(t.TempDir(), "panel.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
h := New(r, "secret")
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if err := r.DB().Exec(`
|
||||||
|
INSERT INTO node(id, name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
|
VALUES(10, 'entry-a', 'entry-secret', '10.0.0.1', '10.0.0.1', '', '30000-30010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0)
|
||||||
|
`, now, now).Error; err != nil {
|
||||||
|
t.Fatalf("insert node: %v", err)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func seedProbeTargetTunnel(t *testing.T, h *Handler, id int64, name string, host string, port int) {
|
||||||
|
t.Helper()
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
if err := h.repo.DB().Exec(`
|
||||||
|
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, inx, ip_preference, probe_target_host, probe_target_port)
|
||||||
|
VALUES(?, ?, 1, 1, 'tls', 1, ?, ?, 1, ?, '', ?, ?)
|
||||||
|
`, id, name, now, now, id, host, port).Error; err != nil {
|
||||||
|
t.Fatalf("insert tunnel: %v", err)
|
||||||
|
}
|
||||||
|
if err := h.repo.DB().Exec(`
|
||||||
|
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||||
|
VALUES(?, '1', 10, 30001, 'round', 1, 'tls')
|
||||||
|
`, id).Error; err != nil {
|
||||||
|
t.Fatalf("insert chain: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertProbeTargetSuccess(t *testing.T, res *httptest.ResponseRecorder) {
|
||||||
|
t.Helper()
|
||||||
|
var payload struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
decodeProbeTargetResponse(t, res, &payload)
|
||||||
|
if payload.Code != 0 {
|
||||||
|
t.Fatalf("expected success, got %+v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeProbeTargetResponse(t *testing.T, res *httptest.ResponseRecorder, v any) {
|
||||||
|
t.Helper()
|
||||||
|
if res.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected HTTP %d, got %d", http.StatusOK, res.Code)
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(res.Body).Decode(v); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findProbeTargetTunnelItem(t *testing.T, items []map[string]interface{}, id int64) map[string]interface{} {
|
||||||
|
t.Helper()
|
||||||
|
for _, item := range items {
|
||||||
|
if asInt64(item["id"], 0) == id {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("tunnel %d not found: %+v", id, items)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -119,18 +119,20 @@ type StatisticsFlow struct {
|
|||||||
func (StatisticsFlow) TableName() string { return "statistics_flow" }
|
func (StatisticsFlow) TableName() string { return "statistics_flow" }
|
||||||
|
|
||||||
type Tunnel struct {
|
type Tunnel struct {
|
||||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
Name string `gorm:"type:varchar(100);not null"`
|
Name string `gorm:"type:varchar(100);not null"`
|
||||||
TrafficRatio float64 `gorm:"column:traffic_ratio;not null;default:1.0"`
|
TrafficRatio float64 `gorm:"column:traffic_ratio;not null;default:1.0"`
|
||||||
Type int `gorm:"not null"`
|
Type int `gorm:"not null"`
|
||||||
Protocol string `gorm:"type:varchar(10);not null;default:'tls'"`
|
Protocol string `gorm:"type:varchar(10);not null;default:'tls'"`
|
||||||
Flow int64 `gorm:"not null"`
|
Flow int64 `gorm:"not null"`
|
||||||
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"`
|
||||||
InIP sql.NullString `gorm:"column:in_ip;type:text"`
|
InIP sql.NullString `gorm:"column:in_ip;type:text"`
|
||||||
Inx int `gorm:"not null;default:0"`
|
Inx int `gorm:"not null;default:0"`
|
||||||
IPPreference string `gorm:"column:ip_preference;type:varchar(10);not null;default:''"`
|
IPPreference string `gorm:"column:ip_preference;type:varchar(10);not null;default:''"`
|
||||||
|
ProbeTargetHost string `gorm:"column:probe_target_host;type:text;not null;default:''"`
|
||||||
|
ProbeTargetPort int `gorm:"column:probe_target_port;not null;default:0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Tunnel) TableName() string { return "tunnel" }
|
func (Tunnel) TableName() string { return "tunnel" }
|
||||||
|
|||||||
@@ -1141,11 +1141,13 @@ func (r *Repository) ListTunnels() ([]map[string]interface{}, error) {
|
|||||||
"id": t.ID, "inx": t.Inx, "name": t.Name,
|
"id": t.ID, "inx": t.Inx, "name": t.Name,
|
||||||
"type": t.Type, "flow": t.Flow, "trafficRatio": t.TrafficRatio,
|
"type": t.Type, "flow": t.Flow, "trafficRatio": t.TrafficRatio,
|
||||||
"status": t.Status, "createdTime": t.CreatedTime,
|
"status": t.Status, "createdTime": t.CreatedTime,
|
||||||
"inIp": nullableString(t.InIP),
|
"inIp": nullableString(t.InIP),
|
||||||
"ipPreference": t.IPPreference,
|
"ipPreference": t.IPPreference,
|
||||||
"inNodeId": make([]map[string]interface{}, 0),
|
"probeTargetHost": t.ProbeTargetHost,
|
||||||
"outNodeId": make([]map[string]interface{}, 0),
|
"probeTargetPort": t.ProbeTargetPort,
|
||||||
"chainNodes": make([][]map[string]interface{}, 0),
|
"inNodeId": make([]map[string]interface{}, 0),
|
||||||
|
"outNodeId": make([]map[string]interface{}, 0),
|
||||||
|
"chainNodes": make([][]map[string]interface{}, 0),
|
||||||
}
|
}
|
||||||
orderedIDs = append(orderedIDs, t.ID)
|
orderedIDs = append(orderedIDs, t.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -397,22 +397,24 @@ func (r *Repository) UpdateTunnelOrder(tunnelID int64, inx int, now int64) {
|
|||||||
Updates(map[string]interface{}{"inx": inx, "updated_time": now}).Error
|
Updates(map[string]interface{}{"inx": inx, "updated_time": now}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateTunnelTx(tx *gorm.DB, tunnelID int64, name string, typeVal int, flow int64, trafficRatio float64, status int, inIP, ipPreference string, protocol string, now int64) error {
|
func (r *Repository) UpdateTunnelTx(tx *gorm.DB, tunnelID int64, name string, typeVal int, flow int64, trafficRatio float64, status int, inIP, ipPreference string, protocol string, probeTargetHost string, probeTargetPort int, now int64) error {
|
||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("database unavailable")
|
return errors.New("database unavailable")
|
||||||
}
|
}
|
||||||
return tx.Model(&model.Tunnel{}).
|
return tx.Model(&model.Tunnel{}).
|
||||||
Where("id = ?", tunnelID).
|
Where("id = ?", tunnelID).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": typeVal,
|
"type": typeVal,
|
||||||
"flow": flow,
|
"flow": flow,
|
||||||
"traffic_ratio": trafficRatio,
|
"traffic_ratio": trafficRatio,
|
||||||
"status": status,
|
"status": status,
|
||||||
"in_ip": nullStringFromInterface(inIP),
|
"in_ip": nullStringFromInterface(inIP),
|
||||||
"ip_preference": ipPreference,
|
"ip_preference": ipPreference,
|
||||||
"protocol": protocol,
|
"protocol": protocol,
|
||||||
"updated_time": now,
|
"probe_target_host": probeTargetHost,
|
||||||
|
"probe_target_port": probeTargetPort,
|
||||||
|
"updated_time": now,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,20 +1328,22 @@ func (r *Repository) BatchUpdateForwardStatus(ids []int64, status int) (int, int
|
|||||||
return s, f
|
return s, f
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateTunnelTx(tx *gorm.DB, name string, trafficRatio float64, typeVal int, flow int64, now int64, status int, inIP interface{}, inx int, ipPreference string) (int64, error) {
|
func (r *Repository) CreateTunnelTx(tx *gorm.DB, name string, trafficRatio float64, typeVal int, flow int64, now int64, status int, inIP interface{}, inx int, ipPreference string, probeTargetHost string, probeTargetPort int) (int64, error) {
|
||||||
inIPVal := nullStringFromInterface(inIP)
|
inIPVal := nullStringFromInterface(inIP)
|
||||||
tunnel := model.Tunnel{
|
tunnel := model.Tunnel{
|
||||||
Name: name,
|
Name: name,
|
||||||
TrafficRatio: trafficRatio,
|
TrafficRatio: trafficRatio,
|
||||||
Type: typeVal,
|
Type: typeVal,
|
||||||
Protocol: "tls",
|
Protocol: "tls",
|
||||||
Flow: flow,
|
Flow: flow,
|
||||||
CreatedTime: now,
|
CreatedTime: now,
|
||||||
UpdatedTime: now,
|
UpdatedTime: now,
|
||||||
Status: status,
|
Status: status,
|
||||||
InIP: inIPVal,
|
InIP: inIPVal,
|
||||||
Inx: inx,
|
Inx: inx,
|
||||||
IPPreference: ipPreference,
|
IPPreference: ipPreference,
|
||||||
|
ProbeTargetHost: probeTargetHost,
|
||||||
|
ProbeTargetPort: probeTargetPort,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&tunnel).Error; err != nil {
|
if err := tx.Create(&tunnel).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
Reference in New Issue
Block a user