feat: persist tunnel probe targets

This commit is contained in:
sagitchu
2026-05-01 22:58:47 +08:00
parent 615d9e67eb
commit 90012725cc
6 changed files with 253 additions and 51 deletions
+37 -11
View File
@@ -607,6 +607,17 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
trafficRatio := asFloat(req["trafficRatio"], 1.0)
inIP := asString(req["inIp"])
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()
inx := h.repo.NextIndex("tunnel")
localDomain := h.federationLocalDomain()
@@ -685,17 +696,19 @@ func (h *Handler) tunnelCreate(w http.ResponseWriter, r *http.Request) {
tunnelProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
}
tunnel := model.Tunnel{
Name: name,
TrafficRatio: trafficRatio,
Type: typeVal,
Protocol: tunnelProtocol,
Flow: flow,
CreatedTime: now,
UpdatedTime: now,
Status: status,
InIP: tunnelInIP,
Inx: inx,
IPPreference: ipPreference,
Name: name,
TrafficRatio: trafficRatio,
Type: typeVal,
Protocol: tunnelProtocol,
Flow: flow,
CreatedTime: now,
UpdatedTime: now,
Status: status,
InIP: tunnelInIP,
Inx: inx,
IPPreference: ipPreference,
ProbeTargetHost: probeTargetHost,
ProbeTargetPort: probeTargetPort,
}
if err := tx.Create(&tunnel).Error; err != nil {
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()
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()
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,
ipPreference,
updateProtocol,
probeTargetHost,
probeTargetPort,
now,
); err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
@@ -142,7 +142,7 @@ func effectiveTunnelProbeTarget(tunnel *model.Tunnel) tunnelProbeTarget {
if tunnel == nil {
return defaultTunnelProbeTarget()
}
return defaultTunnelProbeTarget()
return effectiveTunnelProbeTargetValues(tunnel.ProbeTargetHost, tunnel.ProbeTargetPort)
}
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
}