瀏覽代碼

feat(network): 优化UDP发现响应,强制经入站网卡回包

- 实现基于入站接口的UDP单播回复,确保多网卡环境下地址正确
- 调整维护IP获取逻辑,优先使用广播接收网卡的链路本地地址
- 更新文档以反映新的网络行为及参数说明
yangkaixiang 1 周之前
父節點
當前提交
01ad7b02f3

+ 2 - 2
docs/01-总体方案.md

@@ -88,7 +88,7 @@
 1. `169.254.100.2` 不配置网关
 2. DHCP 地址可带默认网关
 3. DHCP 地址可带网关等网络参数
-4. Server 只通过 `169.254.100.2` 提供服务
+4. Server 监听所有 IPv4 网卡;UDP 发现按广播的入站网卡返回该网卡的 `169.254.x.x` 地址
 
 ### 4.2 LAN1 地址策略
 
@@ -116,7 +116,7 @@
 
 ### 5.1 网络访问限制
 
-1. Server 仅监听 `169.254.100.2`
+1. Server 当前监听所有 IPv4 网卡,HTTP 访问由密码鉴权保护
 2. 当前阶段暂不限制来源 IP,仅通过密码校验控制访问
 3. `LAN2` 接入 4G 路由器后,4G 网络侧理论上可达 Server,因此当前阶段要依赖密码保护
 

+ 3 - 1
docs/03-通信与HTTP_API.md

@@ -9,6 +9,8 @@
 1. Windows 客户端在指定网卡上广播发现设备
 2. Linux Server 响应自身信息
 
+当 Linux 的多张网卡都配置了 `169.254.x.x` 地址时,Server 会识别广播的入站网卡,在响应中返回该网卡的 IP 和 MAC,并强制通过同一张网卡发送回包。
+
 建议端口:`50000`
 
 #### 发现请求
@@ -45,7 +47,7 @@ Base URL:
 
 启动参数:
 
-1. `--ip`:指定维护地址,默认 `169.254.100.2`
+1. `--ip`:指定入站网卡无法识别时使用的备用维护地址
 2. `--port`:指定 HTTP 端口,默认 `48888`
 3. `--password`:指定管理密码,默认 `hvAC2026#%`
 

+ 2 - 1
docs/05-Server模块设计.md

@@ -89,7 +89,8 @@
 1. 监听 UDP 广播请求
 2. 解析发现报文
 3. 优先按收到广播的本地网卡生成发现响应
-4. 返回设备基础信息
+4. 强制使用入站网卡及其 `169.254.x.x` 地址发送回包
+5. 返回设备基础信息
 
 边界:
 

+ 3 - 2
docs/07-Server首阶段实现清单.md

@@ -89,8 +89,9 @@
 
 1. 回传 `request_id`
 2. 返回设备基础信息
-3. 返回维护地址 `169.254.100.2`
-4. 返回 `auth_required=true`
+3. 返回收到广播的网卡上的 `169.254.x.x` 维护地址
+4. 使用同一张入站网卡和维护地址发送回包
+5. 返回 `auth_required=true`
 
 ## 5. 第一阶段建议模块顺序
 

+ 2 - 1
docs/09-使用说明与故障处理.md

@@ -72,11 +72,12 @@ sudo nohup /home/x/nettool-server >/home/x/nettool-server-run.log 2>&1 < /dev/nu
 
 1. 必须使用 `sudo` 启动,否则无法写入 `netplan`、执行 `netplan apply`、重启或关机。
 2. 启动参数都是可选项,不填写时使用默认值。
-3. `--ip` 可选,用于指定维护地址;填写时必须是 `169.254.0.0/16` 范围内的 IPv4 地址。
+3. `--ip` 可选,用于指定无法识别广播入站网卡时的备用维护地址;填写时必须是 `169.254.0.0/16` 范围内的 IPv4 地址。
 4. `--port` 可选,用于指定 HTTP 端口,默认 `48888`。
 5. `--password` 可选,用于指定管理密码,默认 `hvAC2026#%`。
 6. 如果启动时修改了 `--password`,Windows 客户端连接时必须填写相同密码。
 7. 如果启动时指定了 `--ip`,该地址需要已存在于 Linux 网口上,否则客户端无法通过该地址连接 Server。
+8. 多张网卡都有 `169.254.x.x` 地址时,Server 会使用收到广播的网卡及其地址返回发现响应。
 
 ## 3. 正常操作流程
 

+ 2 - 2
server/internal/config/config.go

@@ -6,7 +6,7 @@ import (
 	"net"
 )
 
-const ServerVersion = "2026.07.27.1532"
+const ServerVersion = "2026.07.27.1555"
 
 type Config struct {
 	HTTPHost         string
@@ -34,7 +34,7 @@ func Load(args []string) Config {
 	}
 
 	fs := flag.NewFlagSet("nettool-server", flag.ContinueOnError)
-	fs.StringVar(&cfg.MaintenanceIP, "ip", cfg.MaintenanceIP, "maintenance IPv4 address, must be in 169.254.0.0/16 when set")
+	fs.StringVar(&cfg.MaintenanceIP, "ip", cfg.MaintenanceIP, "fallback maintenance IPv4 address, must be in 169.254.0.0/16 when set")
 	fs.IntVar(&cfg.HTTPPort, "port", cfg.HTTPPort, "HTTP listen port")
 	fs.StringVar(&cfg.AdminPassword, "password", cfg.AdminPassword, "admin password")
 	_ = fs.Parse(args)

+ 9 - 7
server/internal/discovery/discovery.go

@@ -79,7 +79,9 @@ func (s *Server) Run(ctx context.Context) error {
 			AuthRequired:    true,
 		}
 		payload, _ := json.Marshal(resp)
-		_, _ = conn.WriteToUDP(payload, remote)
+		if _, err := writeToUDP(conn, payload, remote, packetInfo, net.ParseIP(lan2IP)); err != nil {
+			s.log.Warn("failed to send udp discovery response", "remote", remote.String(), "local_ip", lan2IP, "interface_index", packetInfo.ifIndex, "error", err.Error())
+		}
 	}
 }
 
@@ -89,12 +91,6 @@ type udpPacketInfo struct {
 }
 
 func (s *Server) maintenanceEndpoint(packetInfo udpPacketInfo) (string, string) {
-	if s.cfg.MaintenanceIP != "" {
-		mac := findMACByIP(s.cfg.MaintenanceIP)
-		if mac != "" {
-			return s.cfg.MaintenanceIP, mac
-		}
-	}
 	if packetInfo.ifIndex > 0 {
 		lan2IP, mac := findLinkLocalEndpointByInterfaceIndex(packetInfo.ifIndex)
 		if lan2IP != "" {
@@ -107,6 +103,12 @@ func (s *Server) maintenanceEndpoint(packetInfo udpPacketInfo) (string, string)
 			return lan2IP, mac
 		}
 	}
+	if s.cfg.MaintenanceIP != "" {
+		mac := findMACByIP(s.cfg.MaintenanceIP)
+		if mac != "" {
+			return s.cfg.MaintenanceIP, mac
+		}
+	}
 	return findFirstLinkLocalEndpoint()
 }
 

+ 21 - 0
server/internal/discovery/packetinfo_linux.go

@@ -6,6 +6,7 @@ import (
 	"encoding/binary"
 	"net"
 	"syscall"
+	"unsafe"
 )
 
 func enableLocalAddrControl(conn *net.UDPConn) error {
@@ -33,6 +34,26 @@ func readFromUDP(conn *net.UDPConn, buf []byte) (int, *net.UDPAddr, udpPacketInf
 	return n, remote, packetInfo, nil
 }
 
+func writeToUDP(conn *net.UDPConn, payload []byte, remote *net.UDPAddr, packetInfo udpPacketInfo, localIP net.IP) (int, error) {
+	ipv4 := localIP.To4()
+	if packetInfo.ifIndex <= 0 || ipv4 == nil {
+		return conn.WriteToUDP(payload, remote)
+	}
+
+	oob := make([]byte, syscall.CmsgSpace(syscall.SizeofInet4Pktinfo))
+	header := (*syscall.Cmsghdr)(unsafe.Pointer(&oob[0]))
+	header.Level = syscall.IPPROTO_IP
+	header.Type = syscall.IP_PKTINFO
+	header.SetLen(syscall.CmsgLen(syscall.SizeofInet4Pktinfo))
+
+	info := (*syscall.Inet4Pktinfo)(unsafe.Pointer(&oob[syscall.CmsgLen(0)]))
+	info.Ifindex = int32(packetInfo.ifIndex)
+	copy(info.Spec_dst[:], ipv4)
+
+	n, _, err := conn.WriteMsgUDP(payload, oob, remote)
+	return n, err
+}
+
 func parsePacketInfo(oob []byte) udpPacketInfo {
 	messages, err := syscall.ParseSocketControlMessage(oob)
 	if err != nil {

+ 4 - 0
server/internal/discovery/packetinfo_other.go

@@ -12,3 +12,7 @@ func readFromUDP(conn *net.UDPConn, buf []byte) (int, *net.UDPAddr, udpPacketInf
 	n, remote, err := conn.ReadFromUDP(buf)
 	return n, remote, udpPacketInfo{}, err
 }
+
+func writeToUDP(conn *net.UDPConn, payload []byte, remote *net.UDPAddr, _ udpPacketInfo, _ net.IP) (int, error) {
+	return conn.WriteToUDP(payload, remote)
+}