Prechádzať zdrojové kódy

feat(network): 支持静态模式下不配置IPv4地址

移除静态IP必填校验,允许addresses为空以表示网口无IPv4配置。同步更新服务端生成逻辑、客户端校验及文档说明。
yangkaixiang 1 týždeň pred
rodič
commit
9eb21a24ed

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

@@ -322,9 +322,9 @@ X-Admin-Password: hvAC2026#%
 
 校验规则:
 
-1. `dhcp4=false` 时,`addresses` 至少需要 1 项
-2. `addresses[].ip` 必填
-3. `addresses[].prefix` 必填
+1. `dhcp4=false` 时,`addresses` 可以为空,表示该网口不配置 IPv4 地址
+2. `addresses` 非空时,`addresses[].ip` 必填
+3. `addresses` 非空时,`addresses[].prefix` 必填
 4. `dns` 选填
 5. `interface` 必填,必须是有效的真实接口名
 6. `routes[].to` 必须为 `default` 或 IPv4 CIDR,且 CIDR 地址必须填写目标网段地址(网络号),例如 `192.168.50.0/24` 合法,`192.168.50.0/16` 应写为 `192.168.0.0/16`

+ 2 - 1
docs/06-netplan修改策略.md

@@ -82,7 +82,8 @@
 1. `dhcp4`
    - 设为 `false`
 2. `addresses`
-   - 按用户输入的一个或多个 IP 和前缀长度生成
+   - 有静态 IP 时,按用户输入的一个或多个 IP 和前缀长度生成
+   - 未填写静态 IP 时删除该字段,表示网口不配置 IPv4 地址
 3. `routes`
    - 若用户填写默认网关或静态路由,则写入
    - 默认网关表达为 `to: default`

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

@@ -159,7 +159,7 @@ hvAC2026#%
 4. 自定义路由。
 5. DNS 地址。
 
-静态配置时,可通过“+ 添加 IP”“+ 添加路由”“+ 添加 DNS”增加多行配置,也可以删除已有行。
+静态配置时,可通过“+ 添加 IP”“+ 添加路由”“+ 添加 DNS”增加多行配置,也可以删除已有行。删除全部 IP 行且不启用 DHCP,可将网口设为无 IPv4 地址状态。
 
 使用建议:
 
@@ -178,8 +178,8 @@ hvAC2026#%
 
 常见校验规则:
 
-1. 静态模式下至少需要一个 IP 地址。
-2. IP、网关、DNS 必须是合法 IPv4。
+1. 静态模式可以不填 IP,表示网口不配置 IPv4 地址。
+2. 已填写的 IP、网关、DNS 必须是合法 IPv4。
 3. 子网掩码必须合法。
 4. 静态接口 IP 不能填写所在网段的网段地址或广播地址。
 5. 默认网关必须与对应网口 IP 在同一网段。
@@ -237,7 +237,6 @@ hvAC2026#%
 | 读取全部网口配置失败 | Server 不支持批量读取接口;HTTP 连接失败;读取网口列表失败 | 确认 Server 已更新并运行;检查端口和网络连接;查看 Server 日志 |
 | 读取目标网口配置失败 | 批量读取中该接口读取 netplan 失败;Server 内部错误 | 点击“重新获取”;查看 Server 日志和 `/etc/netplan` 文件 |
 | 网口配置不能为空 | 客户端没有读取到任何可提交的网口配置 | 点击“重新获取”;确认 Server 可读取 Linux 网口列表 |
-| IP 地址不能为空 | 静态模式下没有填写 IP | 填写 IP 地址,或勾选 DHCP |
 | 子网掩码格式不正确 | 子网掩码不是合法掩码 | 使用 `255.255.255.0`、`255.255.0.0` 等合法掩码 |
 | 网关格式不正确 | 默认网关或路由网关不是合法 IPv4 | 修正网关地址,或不需要网关时取消启用 |
 | DNS 格式不正确 | DNS 地址不是合法 IPv4 | 修正 DNS 地址,或删除该 DNS 行 |

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

@@ -6,7 +6,7 @@ import (
 	"net"
 )
 
-const ServerVersion = "2026.07.27.1520"
+const ServerVersion = "2026.07.27.1532"
 
 type Config struct {
 	HTTPHost         string

+ 5 - 1
server/internal/network/netplan/netplan.go

@@ -107,7 +107,11 @@ func (s *Service) WriteMany(path string, inputs []model.InterfaceConfig, managem
 		for _, address := range addresses {
 			addressItems = append(addressItems, fmt.Sprintf("%s/%d", strings.TrimSpace(address.IP), address.Prefix))
 		}
-		target["addresses"] = addressItems
+		if len(addressItems) > 0 {
+			target["addresses"] = addressItems
+		} else {
+			delete(target, "addresses")
+		}
 		delete(target, "gateway4")
 		if len(routes) > 0 {
 			routeItems := make([]map[string]string, 0, len(routes))

+ 56 - 0
server/internal/network/netplan/netplan_test.go

@@ -0,0 +1,56 @@
+package netplan
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"nettool/internal/model"
+
+	"gopkg.in/yaml.v3"
+)
+
+func TestWriteRemovesExistingAddressForStaticInterfaceWithoutIPv4(t *testing.T) {
+	path := filepath.Join(t.TempDir(), "01-netcfg.yaml")
+	inputYAML := []byte(`network:
+  version: 2
+  ethernets:
+    enp2s0:
+      dhcp4: false
+      addresses:
+        - 192.168.50.10/24
+      optional: true
+`)
+	if err := os.WriteFile(path, inputYAML, 0600); err != nil {
+		t.Fatal(err)
+	}
+
+	err := New().Write(path, "enp2s0", model.InterfaceConfig{
+		Interface: "enp2s0",
+		Dhcp4:     false,
+		Addresses: []model.InterfaceAddressConfig{},
+	}, "", "")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	written, err := os.ReadFile(path)
+	if err != nil {
+		t.Fatal(err)
+	}
+	var cfg fileConfig
+	if err := yaml.Unmarshal(written, &cfg); err != nil {
+		t.Fatal(err)
+	}
+	ethernets := cfg.Network["ethernets"].(map[string]any)
+	target := ethernets["enp2s0"].(map[string]any)
+	if _, ok := target["addresses"]; ok {
+		t.Fatalf("expected addresses to be removed, got netplan:\n%s", written)
+	}
+	if dhcp4, ok := target["dhcp4"].(bool); !ok || dhcp4 {
+		t.Fatalf("expected dhcp4=false, got netplan:\n%s", written)
+	}
+	if optional, ok := target["optional"].(bool); !ok || !optional {
+		t.Fatalf("expected unrelated interface fields to be preserved, got netplan:\n%s", written)
+	}
+}

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

@@ -33,9 +33,6 @@ func (s *Service) Validate(input model.InterfaceConfig) model.ValidateResponse {
 		resp.Valid = len(resp.Errors) == 0
 		return resp
 	}
-	if len(addresses) == 0 {
-		resp.Errors = append(resp.Errors, "至少需要填写一个 IP 地址。")
-	}
 	seenAddresses := make(map[string]struct{})
 	validNetworks := make([]*net.IPNet, 0, len(addresses))
 	for _, address := range addresses {

+ 19 - 0
server/internal/network/validator/validator_test.go

@@ -0,0 +1,19 @@
+package validator
+
+import (
+	"testing"
+
+	"nettool/internal/model"
+)
+
+func TestValidateAllowsStaticInterfaceWithoutIPv4Address(t *testing.T) {
+	result := New().Validate(model.InterfaceConfig{
+		Interface: "enp2s0",
+		Dhcp4:     false,
+		Addresses: []model.InterfaceAddressConfig{},
+	})
+
+	if !result.Valid {
+		t.Fatalf("expected configuration without IPv4 addresses to be valid, got errors: %v", result.Errors)
+	}
+}

+ 0 - 12
windows/NetTool.Client/DeviceDetailsWindow.xaml.cs

@@ -461,12 +461,6 @@ public partial class DeviceDetailsWindow : Window
         var routes = Array.Empty<RemoteInterfaceRouteConfig>();
         if (!dhcp4)
         {
-            if (editor.Addresses.Count == 0)
-            {
-                ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。", StatusMessageType.Error);
-                return null;
-            }
-
             if (!TryBuildAddresses(editor, out addresses, out var addressError))
             {
                 ShowStatusMessage($"{editor.SystemName}:{addressError}", StatusMessageType.Error);
@@ -626,12 +620,6 @@ public partial class DeviceDetailsWindow : Window
         }
 
         addresses = result.ToArray();
-        if (addresses.Length == 0)
-        {
-            error = "IP 地址不能为空,至少需要填写一行地址。";
-            return false;
-        }
-
         error = string.Empty;
         return true;
     }

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

@@ -8,7 +8,7 @@
     <UseWPF>true</UseWPF>
     <AssemblyName>NetTool.Client</AssemblyName>
     <RootNamespace>NetTool.Client</RootNamespace>
-    <InformationalVersion>2026.07.27.1520</InformationalVersion>
+    <InformationalVersion>2026.07.27.1532</InformationalVersion>
   </PropertyGroup>
 
 </Project>