Kaynağa Gözat

feat(network): 禁止配置网络地址或广播地址

在服务端、Windows客户端及文档中增加校验,防止将静态IP配置为网段的网络地址或广播地址,避免网络通信异常。
yangkaixiang 1 ay önce
ebeveyn
işleme
91ddf4ff39

+ 4 - 3
docs/05-Server模块设计.md

@@ -174,9 +174,10 @@
 1. 校验目标接口是否存在
 2. 校验 IP 格式
 3. 校验前缀长度
-4. 校验网关格式和同网段关系
-5. 校验 DNS 格式
-6. 返回警告信息和错误信息
+4. 校验静态 IP 不能是所在网段的网络地址或广播地址
+5. 校验网关格式和同网段关系
+6. 校验 DNS 格式
+7. 返回警告信息和错误信息
 
 说明:
 

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

@@ -181,9 +181,10 @@ Dieteng2026
 1. 静态模式下至少需要一个 IP 地址。
 2. IP、网关、DNS 必须是合法 IPv4。
 3. 子网掩码必须合法。
-4. 默认网关必须与对应网口 IP 在同一网段。
-5. 单个接口最多只能有一条默认路由。
-6. 所有网口中最多只能配置一个默认网关。
+4. 静态 IP 不能是所在网段的网络地址或广播地址。
+5. 默认网关必须与对应网口 IP 在同一网段。
+6. 单个接口最多只能有一条默认路由。
+7. 所有网口中最多只能配置一个默认网关。
 
 ### 3.8 保存配置
 

+ 21 - 1
server/internal/config/config.go

@@ -6,7 +6,7 @@ import (
 	"net"
 )
 
-const ServerVersion = "2026.05.14.1741"
+const ServerVersion = "2026.05.14.1808"
 
 type Config struct {
 	HTTPHost         string
@@ -45,8 +45,28 @@ func Load(args []string) Config {
 		if parsed == nil || ipv4 == nil || ipv4[0] != 169 || ipv4[1] != 254 {
 			panic(fmt.Sprintf("invalid maintenance ip: %s", cfg.MaintenanceIP))
 		}
+		if isNetworkOrBroadcastAddress(ipv4, 16) {
+			panic(fmt.Sprintf("maintenance ip cannot be network or broadcast address: %s", cfg.MaintenanceIP))
+		}
 		cfg.MaintenanceCIDR = fmt.Sprintf("%s/16", cfg.MaintenanceIP)
 	}
 
 	return cfg
 }
+
+func isNetworkOrBroadcastAddress(ip net.IP, prefix int) bool {
+	if prefix > 30 {
+		return false
+	}
+	ipv4 := ip.To4()
+	if ipv4 == nil {
+		return false
+	}
+	mask := net.CIDRMask(prefix, 32)
+	network := ipv4.Mask(mask)
+	broadcast := make(net.IP, len(network))
+	for i := range network {
+		broadcast[i] = network[i] | ^mask[i]
+	}
+	return ipv4.Equal(network) || ipv4.Equal(broadcast)
+}

+ 21 - 0
server/internal/network/validator/validator.go

@@ -48,6 +48,10 @@ func (s *Service) Validate(input model.InterfaceConfig) model.ValidateResponse {
 			resp.Errors = append(resp.Errors, fmt.Sprintf("前缀长度不正确:%s/%d", address.IP, address.Prefix))
 			continue
 		}
+		if isNetworkOrBroadcastAddress(ip, address.Prefix) {
+			resp.Errors = append(resp.Errors, fmt.Sprintf("IP 地址不能是网络地址或广播地址:%s/%d", address.IP, address.Prefix))
+			continue
+		}
 		key := fmt.Sprintf("%s/%d", ip.String(), address.Prefix)
 		if _, ok := seenAddresses[key]; ok {
 			resp.Errors = append(resp.Errors, fmt.Sprintf("IP 地址重复:%s", key))
@@ -128,3 +132,20 @@ func containsGateway(networks []*net.IPNet, gateway net.IP) bool {
 	}
 	return false
 }
+
+func isNetworkOrBroadcastAddress(ip net.IP, prefix int) bool {
+	if prefix > 30 {
+		return false
+	}
+	ipv4 := ip.To4()
+	if ipv4 == nil {
+		return false
+	}
+	mask := net.CIDRMask(prefix, 32)
+	network := ipv4.Mask(mask)
+	broadcast := make(net.IP, len(network))
+	for i := range network {
+		broadcast[i] = network[i] | ^mask[i]
+	}
+	return ipv4.Equal(network) || ipv4.Equal(broadcast)
+}

+ 45 - 0
windows/NetworkTool.Client/DeviceDetailsWindow.xaml.cs

@@ -1,4 +1,5 @@
 using System.Globalization;
+using System.Net;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Runtime.CompilerServices;
@@ -543,6 +544,12 @@ public partial class DeviceDetailsWindow : Window
                     error = $"地址格式不正确:{ip}";
                     return false;
                 }
+                if (IsNetworkOrBroadcastAddress(parts[0], cidrPrefix))
+                {
+                    addresses = [];
+                    error = $"IP 地址不能是网络地址或广播地址:{parts[0]}/{cidrPrefix}";
+                    return false;
+                }
                 row.IP = parts[0];
                 row.Mask = PrefixToMask(cidrPrefix);
                 result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = cidrPrefix });
@@ -560,6 +567,12 @@ public partial class DeviceDetailsWindow : Window
                 error = $"子网掩码格式不正确:{ip} {maskText}";
                 return false;
             }
+            if (IsNetworkOrBroadcastAddress(ip, prefix))
+            {
+                addresses = [];
+                error = $"IP 地址不能是网络地址或广播地址:{ip}/{prefix}";
+                return false;
+            }
             row.Mask = PrefixToMask(prefix);
             result.Add(new RemoteInterfaceAddressConfig { IP = ip, Prefix = prefix });
         }
@@ -675,6 +688,12 @@ public partial class DeviceDetailsWindow : Window
                     error = $"地址格式不正确:{line}";
                     return false;
                 }
+                if (IsNetworkOrBroadcastAddress(cidrParts[0], prefix))
+                {
+                    addresses = [];
+                    error = $"IP 地址不能是网络地址或广播地址:{cidrParts[0]}/{prefix}";
+                    return false;
+                }
                 result.Add(new RemoteInterfaceAddressConfig { IP = cidrParts[0], Prefix = prefix });
                 continue;
             }
@@ -691,6 +710,12 @@ public partial class DeviceDetailsWindow : Window
                 error = $"子网掩码或前缀格式不正确:{line}";
                 return false;
             }
+            if (IsNetworkOrBroadcastAddress(parts[0], parsedPrefix))
+            {
+                addresses = [];
+                error = $"IP 地址不能是网络地址或广播地址:{parts[0]}/{parsedPrefix}";
+                return false;
+            }
             result.Add(new RemoteInterfaceAddressConfig { IP = parts[0], Prefix = parsedPrefix });
         }
 
@@ -731,6 +756,26 @@ public partial class DeviceDetailsWindow : Window
         return text.Split(['\r', '\n', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
     }
 
+    private static bool IsNetworkOrBroadcastAddress(string ipText, int prefix)
+    {
+        if (prefix > 30 || !IPAddress.TryParse(ipText, out var parsed))
+        {
+            return false;
+        }
+
+        var bytes = parsed.GetAddressBytes();
+        if (bytes.Length != 4)
+        {
+            return false;
+        }
+
+        var ip = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16) | ((uint)bytes[2] << 8) | bytes[3];
+        var mask = prefix == 0 ? 0u : uint.MaxValue << (32 - prefix);
+        var network = ip & mask;
+        var broadcast = network | ~mask;
+        return ip == network || ip == broadcast;
+    }
+
     private static bool TryMaskOrPrefixToPrefix(string text, out int prefix)
     {
         if (int.TryParse(text, out prefix) && prefix >= 0 && prefix <= 32)

+ 1 - 1
windows/NetworkTool.Client/NetworkTool.Client.csproj

@@ -6,7 +6,7 @@
     <Nullable>enable</Nullable>
     <ImplicitUsings>enable</ImplicitUsings>
     <UseWPF>true</UseWPF>
-    <InformationalVersion>2026.05.13.1845</InformationalVersion>
+    <InformationalVersion>2026.05.14.1808</InformationalVersion>
   </PropertyGroup>
 
 </Project>