mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
fix: preserve bind IP when editing forward without explicit inIp change
- Add replaceForwardPortsPreservingInIP to maintain existing InIP values - Track inIpTouched state in frontend to distinguish user changes - Only send inIp in update request when user explicitly changed it - Add unit tests for forward port replacement with preserved InIP
This commit is contained in:
@@ -1285,7 +1285,12 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
port = h.pickTunnelPort(tunnelID)
|
||||
}
|
||||
}
|
||||
inIp := asString(req["inIp"])
|
||||
hasInIP := false
|
||||
inIp := ""
|
||||
if rawInIP, ok := req["inIp"]; ok {
|
||||
hasInIP = true
|
||||
inIp = asString(rawInIP)
|
||||
}
|
||||
fwdEntryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||
for _, nodeID := range fwdEntryNodes {
|
||||
node, nodeErr := h.getNodeRecord(nodeID)
|
||||
@@ -1302,7 +1307,14 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if err := h.replaceForwardPorts(id, tunnelID, port, inIp); err != nil {
|
||||
if hasInIP {
|
||||
err = h.replaceForwardPorts(id, tunnelID, port, inIp)
|
||||
} else if tunnelID != forward.TunnelID {
|
||||
err = h.replaceForwardPorts(id, tunnelID, port, "")
|
||||
} else {
|
||||
err = h.replaceForwardPortsPreservingInIP(id, tunnelID, port, oldPorts)
|
||||
}
|
||||
if err != nil {
|
||||
h.rollbackForwardMutation(forward, oldPorts)
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
@@ -3016,38 +3028,62 @@ func parsePorts(portRange string) ([]int, error) {
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
type forwardPortReplaceEntry = struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
InIP string
|
||||
}
|
||||
|
||||
func buildForwardPortEntriesWithPreservedInIP(entryNodeIDs []int64, oldPorts []forwardPortRecord, port int) []forwardPortReplaceEntry {
|
||||
preservedByNode := make(map[int64]string)
|
||||
for _, fp := range oldPorts {
|
||||
current, exists := preservedByNode[fp.NodeID]
|
||||
if !exists {
|
||||
preservedByNode[fp.NodeID] = fp.InIP
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(current) == "" && strings.TrimSpace(fp.InIP) != "" {
|
||||
preservedByNode[fp.NodeID] = fp.InIP
|
||||
}
|
||||
}
|
||||
|
||||
entries := make([]forwardPortReplaceEntry, 0, len(entryNodeIDs))
|
||||
for _, nid := range entryNodeIDs {
|
||||
entries = append(entries, forwardPortReplaceEntry{
|
||||
NodeID: nid,
|
||||
Port: port,
|
||||
InIP: preservedByNode[nid],
|
||||
})
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int, inIp string) error {
|
||||
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries := make([]struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
InIP string
|
||||
}, len(entryNodes))
|
||||
entries := make([]forwardPortReplaceEntry, len(entryNodes))
|
||||
for i, nid := range entryNodes {
|
||||
entries[i] = struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
InIP string
|
||||
}{NodeID: nid, Port: port, InIP: inIp}
|
||||
entries[i] = forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIp}
|
||||
}
|
||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
||||
}
|
||||
|
||||
func (h *Handler) replaceForwardPortsPreservingInIP(forwardID, tunnelID int64, port int, oldPorts []forwardPortRecord) error {
|
||||
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries := buildForwardPortEntriesWithPreservedInIP(entryNodes, oldPorts, port)
|
||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
||||
}
|
||||
|
||||
func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwardPortRecord) error {
|
||||
entries := make([]struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
InIP string
|
||||
}, len(ports))
|
||||
entries := make([]forwardPortReplaceEntry, len(ports))
|
||||
for i, fp := range ports {
|
||||
entries[i] = struct {
|
||||
NodeID int64
|
||||
Port int
|
||||
InIP string
|
||||
}{NodeID: fp.NodeID, Port: fp.Port, InIP: fp.InIP}
|
||||
entries[i] = forwardPortReplaceEntry{NodeID: fp.NodeID, Port: fp.Port, InIP: fp.InIP}
|
||||
}
|
||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package handler
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBuildForwardPortEntriesWithPreservedInIP(t *testing.T) {
|
||||
entryNodeIDs := []int64{10, 20, 30}
|
||||
oldPorts := []forwardPortRecord{
|
||||
{NodeID: 10, Port: 10001, InIP: ""},
|
||||
{NodeID: 10, Port: 10002, InIP: "10.0.0.10"},
|
||||
{NodeID: 20, Port: 10003, InIP: "10.0.0.20"},
|
||||
}
|
||||
|
||||
entries := buildForwardPortEntriesWithPreservedInIP(entryNodeIDs, oldPorts, 18080)
|
||||
if len(entries) != 3 {
|
||||
t.Fatalf("expected 3 entries, got %d", len(entries))
|
||||
}
|
||||
|
||||
if entries[0].NodeID != 10 || entries[0].Port != 18080 || entries[0].InIP != "10.0.0.10" {
|
||||
t.Fatalf("unexpected first entry: %+v", entries[0])
|
||||
}
|
||||
if entries[1].NodeID != 20 || entries[1].Port != 18080 || entries[1].InIP != "10.0.0.20" {
|
||||
t.Fatalf("unexpected second entry: %+v", entries[1])
|
||||
}
|
||||
if entries[2].NodeID != 30 || entries[2].Port != 18080 || entries[2].InIP != "" {
|
||||
t.Fatalf("unexpected third entry: %+v", entries[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForwardPortEntriesWithPreservedInIP_EmptyOldPorts(t *testing.T) {
|
||||
entryNodeIDs := []int64{99}
|
||||
entries := buildForwardPortEntriesWithPreservedInIP(entryNodeIDs, nil, 17000)
|
||||
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected 1 entry, got %d", len(entries))
|
||||
}
|
||||
if entries[0].NodeID != 99 || entries[0].Port != 17000 || entries[0].InIP != "" {
|
||||
t.Fatalf("unexpected entry: %+v", entries[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# 003 Forward Edit Bind IP Preserve
|
||||
|
||||
## Checklist
|
||||
|
||||
- [x] Confirm forward edit flow and identify why untouched listen IP gets overwritten.
|
||||
- [x] Update frontend forward edit submit logic to only send `inIp` when user explicitly changes listen IP.
|
||||
- [x] On tunnel switch in edit form, reset listen IP to default unless user reselects.
|
||||
- [x] Update backend forward update logic to preserve existing `forward_port.in_ip` when request omits `inIp` and tunnel is unchanged.
|
||||
- [x] Keep backend behavior explicit: if `inIp` is sent (including empty), apply requested value; if tunnel changed with no `inIp`, use default bind.
|
||||
- [x] Add regression tests for preserved bind-IP reconstruction helper behavior.
|
||||
- [x] Run focused frontend/backend checks for touched files.
|
||||
@@ -586,6 +586,7 @@ export default function ForwardPage() {
|
||||
strategy: "fifo",
|
||||
speedId: null,
|
||||
});
|
||||
const [inIpTouched, setInIpTouched] = useState(false);
|
||||
|
||||
// 表单验证错误
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
||||
@@ -1276,6 +1277,7 @@ export default function ForwardPage() {
|
||||
// 新增转发
|
||||
const handleAdd = () => {
|
||||
setIsEdit(false);
|
||||
setInIpTouched(false);
|
||||
setForm({
|
||||
name: "",
|
||||
tunnelId: null,
|
||||
@@ -1293,6 +1295,7 @@ export default function ForwardPage() {
|
||||
// 编辑转发
|
||||
const handleEdit = (forward: Forward) => {
|
||||
setIsEdit(true);
|
||||
setInIpTouched(false);
|
||||
setForm({
|
||||
id: forward.id,
|
||||
userId: forward.userId,
|
||||
@@ -1357,11 +1360,17 @@ export default function ForwardPage() {
|
||||
const nextTunnelId = parseInt(tunnelId);
|
||||
const options = tunnelInIpOptionMap.get(nextTunnelId) || [];
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
tunnelId: nextTunnelId,
|
||||
inIp: options.includes(prev.inIp) ? prev.inIp : "",
|
||||
}));
|
||||
setInIpTouched(false);
|
||||
|
||||
setForm((prev) => {
|
||||
const tunnelChanged = prev.tunnelId !== nextTunnelId;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
tunnelId: nextTunnelId,
|
||||
inIp: tunnelChanged ? "" : options.includes(prev.inIp) ? prev.inIp : "",
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
@@ -1388,7 +1397,7 @@ export default function ForwardPage() {
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
inIp: form.inIp || undefined,
|
||||
...(inIpTouched ? { inIp: form.inIp || "" } : {}),
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||
speedId: normalizeSpeedId(form.speedId),
|
||||
@@ -4288,6 +4297,8 @@ export default function ForwardPage() {
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
|
||||
setInIpTouched(true);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
inIp: selectedKey === "__default__" ? "" : selectedKey,
|
||||
|
||||
@@ -115,7 +115,10 @@ const validateNyItem = (line: string, value: unknown): ParsedNyImportLine => {
|
||||
return { line, error: "listen_port格式错误,应为1-65535之间的数字" };
|
||||
}
|
||||
|
||||
if (normalizedListenPort !== null && !isValidListenPort(normalizedListenPort)) {
|
||||
if (
|
||||
normalizedListenPort !== null &&
|
||||
!isValidListenPort(normalizedListenPort)
|
||||
) {
|
||||
return { line, error: "listen_port必须为1-65535之间的数字" };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user