Browse Source

fix(wireguard): fix duplicate port bug in peer endpoint parsing and rendering

Proxmox Router 1 tuần trước cách đây
mục cha
commit
9474499a6a
3 tập tin đã thay đổi với 107 bổ sung và 15 xóa
  1. BIN
      bin/proxmox-router
  2. 62 10
      internal/vpn/wireguard.go
  3. 45 5
      internal/web/static/app.js

BIN
bin/proxmox-router


+ 62 - 10
internal/vpn/wireguard.go

@@ -3,6 +3,7 @@ package vpn
 import (
 	"encoding/json"
 	"fmt"
+	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -34,6 +35,39 @@ func NewWireGuardManager() *WireGuardManager {
 	return m
 }
 
+// CleanEndpoint normalizes an endpoint address and port.
+// If addr contains a port (e.g. "1.2.3.4:51820", "[2001:db8::1]:51820"), it strips the port
+// from addr and returns the clean host and the parsed port.
+func CleanEndpoint(addr string, port int) (string, int) {
+	addr = strings.TrimSpace(addr)
+	if addr == "" || addr == "(none)" {
+		return "", 0
+	}
+
+	// Remove multiple colons/ports if corrupted (e.g. "10.2.145.253:37616:37616")
+	for {
+		if h, p, err := net.SplitHostPort(addr); err == nil {
+			if parsedPort, err := strconv.Atoi(p); err == nil && parsedPort > 0 {
+				port = parsedPort
+			}
+			addr = h
+		} else if strings.Contains(addr, ":") && !strings.HasPrefix(addr, "[") {
+			lastColon := strings.LastIndex(addr, ":")
+			maybePort := addr[lastColon+1:]
+			if parsedPort, err := strconv.Atoi(maybePort); err == nil && parsedPort > 0 {
+				port = parsedPort
+				addr = addr[:lastColon]
+			} else {
+				break
+			}
+		} else {
+			break
+		}
+	}
+
+	return addr, port
+}
+
 func (m *WireGuardManager) load() {
 	if data, err := os.ReadFile(WGInterfacesStorePath); err == nil {
 		_ = json.Unmarshal(data, &m.interfaces)
@@ -47,6 +81,10 @@ func (m *WireGuardManager) load() {
 	if m.peers == nil {
 		m.peers = make([]WireGuardPeer, 0)
 	}
+	// Sanitize endpoints for all stored peers
+	for i := range m.peers {
+		m.peers[i].EndpointAddress, m.peers[i].EndpointPort = CleanEndpoint(m.peers[i].EndpointAddress, m.peers[i].EndpointPort)
+	}
 }
 
 func (m *WireGuardManager) save() error {
@@ -298,8 +336,10 @@ func (m *WireGuardManager) ListPeers(ifaceFilter string) []WireGuardPeer {
 							handshakeStr = time.Since(time.Unix(handshakeUnix, 0)).Round(time.Second).String() + " ago"
 						}
 
+						epHost, epPort := CleanEndpoint(endpoint, 0)
 						dumpMap[iface.Name][pubKey] = WireGuardPeer{
-							EndpointAddress: endpoint,
+							EndpointAddress: epHost,
+							EndpointPort:    epPort,
 							LatestHandshake: handshakeStr,
 							TransferRxBytes: rx,
 							TransferTxBytes: tx,
@@ -321,7 +361,12 @@ func (m *WireGuardManager) ListPeers(ifaceFilter string) []WireGuardPeer {
 		// Overlay runtime stats if available
 		if ifaceDumps, ok := dumpMap[p.Interface]; ok {
 			if stats, has := ifaceDumps[p.PublicKey]; has {
-				item.EndpointAddress = stats.EndpointAddress
+				if stats.EndpointAddress != "" {
+					item.EndpointAddress = stats.EndpointAddress
+					if stats.EndpointPort > 0 {
+						item.EndpointPort = stats.EndpointPort
+					}
+				}
 				item.LatestHandshake = stats.LatestHandshake
 				item.TransferRxBytes = stats.TransferRxBytes
 				item.TransferTxBytes = stats.TransferTxBytes
@@ -329,6 +374,7 @@ func (m *WireGuardManager) ListPeers(ifaceFilter string) []WireGuardPeer {
 				item.TransferTxHuman = stats.TransferTxHuman
 			}
 		}
+		item.EndpointAddress, item.EndpointPort = CleanEndpoint(item.EndpointAddress, item.EndpointPort)
 		result = append(result, item)
 	}
 	return result
@@ -378,6 +424,10 @@ func (m *WireGuardManager) SavePeer(req SaveWireGuardPeerRequest) error {
 		}
 	}
 
+	req.EndpointAddress, req.EndpointPort = CleanEndpoint(req.EndpointAddress, req.EndpointPort)
+	req.WireGuardPeer.EndpointAddress = req.EndpointAddress
+	req.WireGuardPeer.EndpointPort = req.EndpointPort
+
 	found := false
 	for i, existing := range m.peers {
 		if existing.PublicKey == req.PublicKey && existing.Interface == req.Interface {
@@ -457,10 +507,11 @@ func (m *WireGuardManager) SetPeerEnabled(ifaceName, pubKey string, enabled bool
 					if len(p.AllowedIPs) > 0 {
 						args = append(args, "allowed-ips", strings.Join(p.AllowedIPs, ","))
 					}
-					if p.EndpointAddress != "" {
-						ep := p.EndpointAddress
-						if p.EndpointPort > 0 {
-							ep = fmt.Sprintf("%s:%d", p.EndpointAddress, p.EndpointPort)
+					epHost, epPort := CleanEndpoint(p.EndpointAddress, p.EndpointPort)
+					if epHost != "" {
+						ep := epHost
+						if epPort > 0 {
+							ep = fmt.Sprintf("%s:%d", epHost, epPort)
 						}
 						args = append(args, "endpoint", ep)
 					}
@@ -547,10 +598,11 @@ func (m *WireGuardManager) generateWGConfigFile(ifaceName string) error {
 			if len(p.AllowedIPs) > 0 {
 				sb.WriteString(fmt.Sprintf("AllowedIPs = %s\n", strings.Join(p.AllowedIPs, ", ")))
 			}
-			if p.EndpointAddress != "" {
-				ep := p.EndpointAddress
-				if p.EndpointPort > 0 {
-					ep = fmt.Sprintf("%s:%d", p.EndpointAddress, p.EndpointPort)
+			epHost, epPort := CleanEndpoint(p.EndpointAddress, p.EndpointPort)
+			if epHost != "" {
+				ep := epHost
+				if epPort > 0 {
+					ep = fmt.Sprintf("%s:%d", epHost, epPort)
 				}
 				sb.WriteString(fmt.Sprintf("Endpoint = %s\n", ep))
 			}

+ 45 - 5
internal/web/static/app.js

@@ -1409,6 +1409,23 @@ async function loadWireGuard(populateInputs = true) {
   }
 }
 
+function formatWGPeerEndpoint(address, port) {
+  if (!address || address === '(none)') return '--';
+  let addr = address.trim();
+  let p = port || 0;
+  while (addr.includes(':') && !addr.startsWith('[')) {
+    const lastColon = addr.lastIndexOf(':');
+    const maybePort = addr.substring(lastColon + 1);
+    if (!isNaN(parseInt(maybePort, 10)) && parseInt(maybePort, 10) > 0) {
+      if (!p) p = parseInt(maybePort, 10);
+      addr = addr.substring(0, lastColon);
+    } else {
+      break;
+    }
+  }
+  return p ? `${addr}:${p}` : addr;
+}
+
 function renderWGPeers() {
   const filterSel = document.getElementById('wg-peers-filter-iface');
   const selectedIface = filterSel ? filterSel.value : '';
@@ -1438,7 +1455,7 @@ function renderWGPeers() {
           </span>
         </td>
         <td class="py-2.5 px-3 text-emerald-400 font-mono font-semibold">${(p.allowed_ips || []).join(', ')}</td>
-        <td class="py-2.5 px-3 text-slate-400 font-mono">${p.endpoint_address ? (p.endpoint_address + (p.endpoint_port ? ':' + p.endpoint_port : '')) : '--'}</td>
+        <td class="py-2.5 px-3 text-slate-400 font-mono">${formatWGPeerEndpoint(p.endpoint_address, p.endpoint_port)}</td>
         <td class="py-2.5 px-3 text-slate-300 font-sans">${p.latest_handshake || 'Never'}</td>
         <td class="py-2.5 px-3 font-mono text-[11px]">
           <span class="text-emerald-400">↓ ${p.transfer_rx_human || '0 B'}</span>
@@ -1706,8 +1723,21 @@ function openEditWGPeerModal(ifaceName, pubKey) {
   document.getElementById('wg-peer-modal-name').value = peer.name || '';
   document.getElementById('wg-peer-modal-pubkey').value = peer.public_key || '';
   document.getElementById('wg-peer-modal-allowedips').value = (peer.allowed_ips || []).join(', ');
-  document.getElementById('wg-peer-modal-endpoint').value = peer.endpoint_address || '';
-  document.getElementById('wg-peer-modal-port').value = peer.endpoint_port || '';
+  let epHost = (peer.endpoint_address || '').trim();
+  let epPort = peer.endpoint_port || '';
+  while (epHost.includes(':') && !epHost.startsWith('[')) {
+    const lastColon = epHost.lastIndexOf(':');
+    const maybePort = epHost.substring(lastColon + 1);
+    if (!isNaN(parseInt(maybePort, 10)) && parseInt(maybePort, 10) > 0) {
+      if (!epPort) epPort = parseInt(maybePort, 10);
+      epHost = epHost.substring(0, lastColon);
+    } else {
+      break;
+    }
+  }
+
+  document.getElementById('wg-peer-modal-endpoint').value = epHost;
+  document.getElementById('wg-peer-modal-port').value = epPort;
   document.getElementById('wg-peer-modal-keepalive').value = peer.persistent_keepalive ?? 25;
   document.getElementById('wg-peer-modal-preshared').value = peer.preshared_key || '';
   document.getElementById('wg-peer-modal-disabled').checked = !!peer.disabled;
@@ -1782,8 +1812,18 @@ async function submitSaveWGPeer(e) {
     .split(',')
     .map(s => s.trim())
     .filter(s => s.length > 0);
-  const epHost = document.getElementById('wg-peer-modal-endpoint').value.trim();
-  const epPort = parseInt(document.getElementById('wg-peer-modal-port').value, 10) || 0;
+  let epHost = document.getElementById('wg-peer-modal-endpoint').value.trim();
+  let epPort = parseInt(document.getElementById('wg-peer-modal-port').value, 10) || 0;
+  while (epHost.includes(':') && !epHost.startsWith('[')) {
+    const lastColon = epHost.lastIndexOf(':');
+    const maybePort = epHost.substring(lastColon + 1);
+    if (!isNaN(parseInt(maybePort, 10)) && parseInt(maybePort, 10) > 0) {
+      if (!epPort) epPort = parseInt(maybePort, 10);
+      epHost = epHost.substring(0, lastColon);
+    } else {
+      break;
+    }
+  }
   const keepalive = parseInt(document.getElementById('wg-peer-modal-keepalive').value, 10) || 0;
   const psk = document.getElementById('wg-peer-modal-preshared').value.trim();
   const disabled = document.getElementById('wg-peer-modal-disabled').checked;