From 47537e282bb22b017986a7ee759b4d3ba3162811 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:33:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(dns):=20=E4=BC=98=E5=8C=96=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=B7=A5=E5=85=B7=E7=AE=B1DNS=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=BB=A5=E9=80=82=E9=85=8D=E7=8E=B0=E4=BB=A3Linux=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=20(#1202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat(dns): 优化系统工具箱DNS设置以适配现代Linux系统 - 创建 pkg/dns 包实现多种DNS管理方式 - 支持 NetworkManager (RHEL 9.x/10.x) - 支持 netplan (Debian 12+/Ubuntu 22+) - 回退到直接修改 /etc/resolv.conf - 更新前端显示当前DNS管理方式 - 添加单元测试 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 修复代码审查发现的问题 - 提取 shell 参数转义逻辑到独立函数 - 修正 netplan 配置文件选择的注释说明 - 使用常量替代硬编码的缩进值 - 添加前端空数组安全检查 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 优化 * fix: 优化 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 --- internal/service/toolbox_system.go | 20 +- pkg/dns/dns.go | 484 +++++++++++++++++++++++++++ pkg/dns/dns_test.go | 325 ++++++++++++++++++ web/src/locales/en.po | 4 + web/src/locales/frontend.pot | 4 + web/src/locales/zh_CN.po | 4 + web/src/locales/zh_TW.po | 4 + web/src/views/toolbox/SystemView.vue | 16 +- 8 files changed, 845 insertions(+), 16 deletions(-) create mode 100644 pkg/dns/dns.go create mode 100644 pkg/dns/dns_test.go diff --git a/internal/service/toolbox_system.go b/internal/service/toolbox_system.go index db96e5e6..ce4c5028 100644 --- a/internal/service/toolbox_system.go +++ b/internal/service/toolbox_system.go @@ -13,6 +13,7 @@ import ( "github.com/acepanel/panel/internal/app" "github.com/acepanel/panel/internal/http/request" + "github.com/acepanel/panel/pkg/dns" "github.com/acepanel/panel/pkg/io" "github.com/acepanel/panel/pkg/ntp" "github.com/acepanel/panel/pkg/shell" @@ -32,19 +33,16 @@ func NewToolboxSystemService(t *gotext.Locale) *ToolboxSystemService { // GetDNS 获取 DNS 信息 func (s *ToolboxSystemService) GetDNS(w http.ResponseWriter, r *http.Request) { - raw, err := io.Read("/etc/resolv.conf") + dnsServers, manager, err := dns.GetDNS() if err != nil { Error(w, http.StatusInternalServerError, "%v", err) return } - match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1) - dns := make([]string, 0) - for _, m := range match { - dns = append(dns, m[1]) - } - - Success(w, dns) + Success(w, chix.M{ + "dns": dnsServers, + "manager": manager.String(), + }) } // UpdateDNS 设置 DNS 信息 @@ -55,11 +53,7 @@ func (s *ToolboxSystemService) UpdateDNS(w http.ResponseWriter, r *http.Request) return } - var dns string - dns += "nameserver " + req.DNS1 + "\n" - dns += "nameserver " + req.DNS2 + "\n" - - if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil { + if err := dns.SetDNS(req.DNS1, req.DNS2); err != nil { Error(w, http.StatusInternalServerError, s.t.Get("failed to update DNS: %v", err)) return } diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go new file mode 100644 index 00000000..105b348e --- /dev/null +++ b/pkg/dns/dns.go @@ -0,0 +1,484 @@ +// Package dns 提供 DNS 配置管理功能 +package dns + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "go.yaml.in/yaml/v4" + + "github.com/acepanel/panel/pkg/io" + "github.com/acepanel/panel/pkg/shell" + "github.com/acepanel/panel/pkg/systemctl" +) + +// Manager 定义了 DNS 管理的类型 +type Manager int + +const ( + ManagerUnknown Manager = iota + ManagerNetworkManager + ManagerNetplan + ManagerResolvConf +) + +// String 返回 Manager 的字符串表示 +func (m Manager) String() string { + switch m { + case ManagerNetworkManager: + return "NetworkManager" + case ManagerNetplan: + return "netplan" + case ManagerResolvConf: + return "resolv.conf" + default: + return "unknown" + } +} + +// DetectManager 检测当前系统使用的 DNS 管理方式 +func DetectManager() Manager { + if isNetworkManagerActive() { + return ManagerNetworkManager + } + if isNetplanAvailable() { + return ManagerNetplan + } + return ManagerResolvConf +} + +// GetDNS 获取当前 DNS 配置 +func GetDNS() ([]string, Manager, error) { + manager := DetectManager() + dns, err := getDNSFromResolvConf() + return dns, manager, err +} + +// SetDNS 设置 DNS 服务器 +func SetDNS(dns1, dns2 string) error { + manager := DetectManager() + + switch manager { + case ManagerNetworkManager: + return setDNSWithNetworkManager(dns1, dns2) + case ManagerNetplan: + return setDNSWithNetplan(dns1, dns2) + default: + return setDNSWithResolvConf(dns1, dns2) + } +} + +// isNetworkManagerActive 检查 NetworkManager 是否正在运行 +func isNetworkManagerActive() bool { + active, _ := systemctl.Status("NetworkManager") + return active +} + +// isNetplanAvailable 检查 netplan 是否可用 +func isNetplanAvailable() bool { + if _, err := shell.Execf("command -v netplan"); err != nil { + return false + } + + configFiles := []string{ + "/etc/netplan/*.yaml", + "/etc/netplan/*.yml", + } + for _, pattern := range configFiles { + files, _ := filepath.Glob(pattern) + if len(files) > 0 { + return true + } + } + + return false +} + +// getDNSFromResolvConf 从 /etc/resolv.conf 获取 DNS +func getDNSFromResolvConf() ([]string, error) { + raw, err := io.Read("/etc/resolv.conf") + if err != nil { + return nil, err + } + + match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1) + dns := make([]string, 0) + for _, m := range match { + dns = append(dns, m[1]) + } + + return dns, nil +} + +// setDNSWithNetworkManager 使用 NetworkManager 设置 DNS +func setDNSWithNetworkManager(dns1, dns2 string) error { + // 获取所有活动的连接 + connections, err := getActiveNMConnections() + if err != nil || len(connections) == 0 { + // 回退到直接修改 resolv.conf + return setDNSWithResolvConf(dns1, dns2) + } + + // 构建 DNS 服务器列表 + dnsServers := dns1 + if dns2 != "" { + dnsServers = dns1 + "," + dns2 + } + + var lastErr error + successCount := 0 + + // 为所有活动的连接设置 DNS + for _, conn := range connections { + connName := conn.name + // 使用 nmcli 设置 DNS + if _, err = shell.Execf("nmcli connection modify %s ipv4.dns %s", connName, dnsServers); err != nil { + lastErr = fmt.Errorf("failed to set DNS for connection %s: %w", connName, err) + continue + } + // 设置 DNS 优先级,确保自定义 DNS 优先 + _, _ = shell.Execf("nmcli connection modify %s ipv4.dns-priority -1", connName) + // 忽略 DHCP 提供的 DNS + _, _ = shell.Execf("nmcli connection modify %s ipv4.ignore-auto-dns yes", connName) + // 重新激活连接以应用更改 + if _, err = shell.Execf("nmcli connection up %s", connName); err != nil { + lastErr = fmt.Errorf("failed to reactivate connection %s: %w", connName, err) + continue + } + successCount++ + } + + // 只要有一个连接成功设置就算成功 + if successCount == 0 && lastErr != nil { + return lastErr + } + + return nil +} + +// nmConnection NetworkManager 连接信息 +type nmConnection struct { + name string // 连接名称(带引号处理空格) + device string // 设备名 +} + +// getActiveNMConnections 获取所有活动的 NetworkManager 连接 +func getActiveNMConnections() ([]nmConnection, error) { + output, err := shell.Execf("nmcli -t -f NAME,DEVICE connection show --active") + if err != nil { + return nil, err + } + + var connections []nmConnection + lines := strings.Split(output, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // 格式: NAME:DEVICE + parts := strings.SplitN(line, ":", 2) + if len(parts) >= 2 && parts[1] != "" && isValidNetworkInterface(parts[1]) { + // 返回带引号的连接名称,以处理包含空格的名称 + quotedName := "'" + strings.ReplaceAll(parts[0], "'", "'\"'\"'") + "'" + connections = append(connections, nmConnection{ + name: quotedName, + device: parts[1], + }) + } + } + + if len(connections) == 0 { + return nil, fmt.Errorf("no active NetworkManager connections found") + } + + return connections, nil +} + +// setDNSWithNetplan 使用 netplan 设置 DNS +func setDNSWithNetplan(dns1, dns2 string) error { + // 查找 netplan 配置文件 + configPath, err := findNetplanConfig() + if err != nil { + // 回退到直接修改 resolv.conf + return setDNSWithResolvConf(dns1, dns2) + } + + // 读取现有配置 + content, err := io.Read(configPath) + if err != nil { + return setDNSWithResolvConf(dns1, dns2) + } + // 更新 DNS 配置 + newContent, err := updateNetplanDNS(content, dns1, dns2) + if err != nil { + return setDNSWithResolvConf(dns1, dns2) + } + // 写入配置文件 + if err = io.Write(configPath, newContent, 0600); err != nil { + return fmt.Errorf("failed to write netplan config: %w", err) + } + // 应用 netplan 配置 + if _, err = shell.Execf("netplan apply"); err != nil { + return fmt.Errorf("failed to apply netplan config: %w", err) + } + + return nil +} + +// findNetplanConfig 查找 netplan 配置文件 +func findNetplanConfig() (string, error) { + patterns := []string{ + "/etc/netplan/*.yaml", + "/etc/netplan/*.yml", + } + + for _, pattern := range patterns { + files, err := filepath.Glob(pattern) + if err != nil { + continue + } + if len(files) > 0 { + // netplan 按文件名字母顺序处理配置文件 + // 返回最后一个文件,因为它的配置会覆盖之前的配置 + return files[len(files)-1], nil + } + } + + return "", fmt.Errorf("failed to find netplan config file") +} + +// netplanConfig netplan 配置结构 +type netplanConfig struct { + Network netplanNetwork `yaml:"network"` +} + +// netplanNetwork 网络配置 +type netplanNetwork struct { + Version int `yaml:"version,omitempty"` + Renderer string `yaml:"renderer,omitempty"` + Ethernets map[string]*netplanInterface `yaml:"ethernets,omitempty"` + Wifis map[string]*netplanInterface `yaml:"wifis,omitempty"` + Bonds map[string]*netplanInterface `yaml:"bonds,omitempty"` + Bridges map[string]*netplanInterface `yaml:"bridges,omitempty"` + Vlans map[string]*netplanInterface `yaml:"vlans,omitempty"` +} + +// netplanInterface 网络接口配置 +type netplanInterface struct { + DHCP4 any `yaml:"dhcp4,omitempty"` + DHCP6 any `yaml:"dhcp6,omitempty"` + Addresses []string `yaml:"addresses,omitempty"` + Gateway4 string `yaml:"gateway4,omitempty"` + Gateway6 string `yaml:"gateway6,omitempty"` + Routes []netplanRoute `yaml:"routes,omitempty"` + Nameservers *netplanNameservers `yaml:"nameservers,omitempty"` + MTU int `yaml:"mtu,omitempty"` + MACAddress string `yaml:"macaddress,omitempty"` + Optional any `yaml:"optional,omitempty"` + Match *netplanMatch `yaml:"match,omitempty"` + SetName string `yaml:"set-name,omitempty"` + Interfaces []string `yaml:"interfaces,omitempty"` + Parameters map[string]any `yaml:"parameters,omitempty"` + ID int `yaml:"id,omitempty"` + Link string `yaml:"link,omitempty"` + Extra map[string]yaml.Node `yaml:",inline"` // 保留未知字段 +} + +// netplanRoute 路由配置 +type netplanRoute struct { + To string `yaml:"to,omitempty"` + Via string `yaml:"via,omitempty"` + Metric int `yaml:"metric,omitempty"` +} + +// netplanNameservers DNS 配置 +type netplanNameservers struct { + Addresses []string `yaml:"addresses,omitempty"` + Search []string `yaml:"search,omitempty"` +} + +// netplanMatch 网卡匹配规则 +type netplanMatch struct { + MACAddress string `yaml:"macaddress,omitempty"` + Driver string `yaml:"driver,omitempty"` +} + +// updateNetplanDNS 更新 netplan 配置中的 DNS +func updateNetplanDNS(content, dns1, dns2 string) (string, error) { + var config netplanConfig + if err := yaml.Unmarshal([]byte(content), &config); err != nil { + return "", fmt.Errorf("failed to parse netplan config: %w", err) + } + + // 构建新的 DNS 地址列表 + dnsAddresses := []string{dns1} + if dns2 != "" { + dnsAddresses = append(dnsAddresses, dns2) + } + + // 更新所有网络接口的 DNS 配置 + updated := false + + // 更新 ethernets + for _, iface := range config.Network.Ethernets { + if iface != nil { + if iface.Nameservers == nil { + iface.Nameservers = &netplanNameservers{} + } + iface.Nameservers.Addresses = dnsAddresses + updated = true + } + } + + // 更新 wifis + for _, iface := range config.Network.Wifis { + if iface != nil { + if iface.Nameservers == nil { + iface.Nameservers = &netplanNameservers{} + } + iface.Nameservers.Addresses = dnsAddresses + updated = true + } + } + + // 更新 bonds + for _, iface := range config.Network.Bonds { + if iface != nil { + if iface.Nameservers == nil { + iface.Nameservers = &netplanNameservers{} + } + iface.Nameservers.Addresses = dnsAddresses + updated = true + } + } + + // 更新 bridges + for _, iface := range config.Network.Bridges { + if iface != nil { + if iface.Nameservers == nil { + iface.Nameservers = &netplanNameservers{} + } + iface.Nameservers.Addresses = dnsAddresses + updated = true + } + } + + // 更新 vlans + for _, iface := range config.Network.Vlans { + if iface != nil { + if iface.Nameservers == nil { + iface.Nameservers = &netplanNameservers{} + } + iface.Nameservers.Addresses = dnsAddresses + updated = true + } + } + + // 如果配置中没有任何接口,尝试检测当前活动的网络接口并添加配置 + if !updated { + activeIface := detectActiveInterface() + if activeIface == "" { + return "", fmt.Errorf("no network interface found in config and failed to detect active interface") + } + + // 创建 ethernets 配置 + if config.Network.Ethernets == nil { + config.Network.Ethernets = make(map[string]*netplanInterface) + } + + // 为检测到的接口添加配置 + config.Network.Ethernets[activeIface] = &netplanInterface{ + Nameservers: &netplanNameservers{ + Addresses: dnsAddresses, + }, + } + + // 设置默认版本 + if config.Network.Version == 0 { + config.Network.Version = 2 + } + } + + // 序列化为 YAML + output, err := yaml.Marshal(&config) + if err != nil { + return "", fmt.Errorf("failed to marshal netplan config: %w", err) + } + + return string(output), nil +} + +// detectActiveInterface 检测当前活动的网络接口名称 +// 返回第一个非 lo/docker/veth/br- 的活动接口 +func detectActiveInterface() string { + // 尝试获取默认路由的网络接口 + output, err := shell.Execf("ip route show default 2>/dev/null | awk '/default/ {print $5}' | head -n1") + if err == nil { + iface := strings.TrimSpace(output) + if iface != "" && isValidNetworkInterface(iface) { + return iface + } + } + + // 回退:获取所有 UP 状态的接口 + output, err = shell.Execf("ip -o link show up 2>/dev/null | awk -F': ' '{print $2}'") + if err == nil { + lines := strings.Split(strings.TrimSpace(output), "\n") + for _, line := range lines { + iface := strings.TrimSpace(line) + if isValidNetworkInterface(iface) { + return iface + } + } + } + + return "" +} + +// isValidNetworkInterface 检查接口名是否为有效的物理/外部网络接口 +func isValidNetworkInterface(name string) bool { + if name == "" { + return false + } + + // 排除虚拟接口和回环接口 + excludePrefixes := []string{ + "lo", // 回环 + "docker", // Docker + "veth", // Docker/容器虚拟网卡 + "br-", // Docker 桥接 + "virbr", // libvirt 虚拟桥接 + "vnet", // 虚拟网络 + "tun", // VPN 隧道 + "tap", // TAP 设备 + "flannel", // Kubernetes flannel + "cni", // Kubernetes CNI + "cali", // Calico + } + + for _, prefix := range excludePrefixes { + if strings.HasPrefix(name, prefix) { + return false + } + } + + return true +} + +// setDNSWithResolvConf 直接修改 /etc/resolv.conf 设置 DNS +func setDNSWithResolvConf(dns1, dns2 string) error { + var dns string + dns += "nameserver " + dns1 + "\n" + if dns2 != "" { + dns += "nameserver " + dns2 + "\n" + } + + if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil { + return fmt.Errorf("failed to write /etc/resolv.conf: %w", err) + } + + return nil +} diff --git a/pkg/dns/dns_test.go b/pkg/dns/dns_test.go new file mode 100644 index 00000000..825faa0f --- /dev/null +++ b/pkg/dns/dns_test.go @@ -0,0 +1,325 @@ +package dns + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" +) + +type DNSTestSuite struct { + suite.Suite +} + +func TestDNSTestSuite(t *testing.T) { + suite.Run(t, &DNSTestSuite{}) +} + +func (s *DNSTestSuite) SetupTest() { + if _, err := os.Stat("testdata"); os.IsNotExist(err) { + s.NoError(os.MkdirAll("testdata", 0755)) + } +} + +func (s *DNSTestSuite) TearDownTest() { + s.NoError(os.RemoveAll("testdata")) +} + +func (s *DNSTestSuite) TestManagerString() { + s.Equal("NetworkManager", ManagerNetworkManager.String()) + s.Equal("netplan", ManagerNetplan.String()) + s.Equal("resolv.conf", ManagerResolvConf.String()) + s.Equal("unknown", ManagerUnknown.String()) +} + +func (s *DNSTestSuite) TestDetectManager() { + // DetectManager 会返回一个有效的 Manager 类型 + manager := DetectManager() + s.True(manager >= ManagerUnknown && manager <= ManagerResolvConf) +} + +func (s *DNSTestSuite) TestUpdateNetplanDNS() { + // 测试基本的 netplan 配置更新 + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: true` + + result, err := updateNetplanDNS(content, "8.8.8.8", "8.8.4.4") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "8.8.8.8") + s.Contains(result, "8.8.4.4") +} + +func (s *DNSTestSuite) TestUpdateNetplanDNSWithExisting() { + // 测试替换现有的 DNS 配置 + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: true + nameservers: + addresses: [1.1.1.1, 1.0.0.1]` + + result, err := updateNetplanDNS(content, "8.8.8.8", "8.8.4.4") + s.NoError(err) + s.Contains(result, "8.8.8.8") + s.Contains(result, "8.8.4.4") + // 旧的 DNS 应该被移除 + s.NotContains(result, "1.1.1.1") + s.NotContains(result, "1.0.0.1") +} + +// TestUpdateNetplanDNSWithWifi 测试 wifi 网络接口配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithWifi() { + content := `network: + version: 2 + wifis: + wlan0: + dhcp4: true + access-points: + "my-wifi": + password: "secret"` + + result, err := updateNetplanDNS(content, "8.8.8.8", "") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "8.8.8.8") +} + +// TestUpdateNetplanDNSWithMultipleInterfaces 测试多接口配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithMultipleInterfaces() { + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: true + eth1: + addresses: + - 192.168.1.100/24 + gateway4: 192.168.1.1` + + result, err := updateNetplanDNS(content, "8.8.8.8", "114.114.114.114") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "8.8.8.8") + s.Contains(result, "114.114.114.114") +} + +// TestUpdateNetplanDNSWithRoutes 测试带路由配置的接口 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithRoutes() { + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: false + addresses: + - 10.0.0.10/24 + routes: + - to: default + via: 10.0.0.1` + + result, err := updateNetplanDNS(content, "223.5.5.5", "223.6.6.6") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "223.5.5.5") + s.Contains(result, "223.6.6.6") + // 路由配置应该被保留 + s.Contains(result, "routes:") +} + +// TestUpdateNetplanDNSWithBond 测试 bond 网络配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithBond() { + content := `network: + version: 2 + ethernets: + eth0: {} + eth1: {} + bonds: + bond0: + interfaces: + - eth0 + - eth1 + addresses: + - 10.0.0.100/24 + parameters: + mode: active-backup + primary: eth0` + + result, err := updateNetplanDNS(content, "8.8.8.8", "8.8.4.4") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "8.8.8.8") + // bond 配置应该被保留 + s.Contains(result, "bonds:") + s.Contains(result, "bond0:") +} + +// TestUpdateNetplanDNSWithRenderer 测试带 renderer 的配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithRenderer() { + content := `network: + version: 2 + renderer: networkd + ethernets: + ens3: + dhcp4: true` + + result, err := updateNetplanDNS(content, "8.8.8.8", "") + s.NoError(err) + s.Contains(result, "renderer: networkd") + s.Contains(result, "nameservers:") +} + +// TestUpdateNetplanDNSPreserveSearch 测试保留 search 域 +func (s *DNSTestSuite) TestUpdateNetplanDNSPreserveSearch() { + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: true + nameservers: + addresses: [1.1.1.1] + search: [example.com, local]` + + result, err := updateNetplanDNS(content, "8.8.8.8", "8.8.4.4") + s.NoError(err) + s.Contains(result, "8.8.8.8") + s.Contains(result, "8.8.4.4") + // 旧 DNS 应该被替换 + s.NotContains(result, "1.1.1.1") + // search 域应该被保留 + s.Contains(result, "search:") + s.Contains(result, "example.com") +} + +// TestUpdateNetplanDNSEmptyConfig 测试空配置 +// 注意:当配置为空时,会尝试检测活动网络接口并自动添加配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSEmptyConfig() { + content := `network: + version: 2` + + result, err := updateNetplanDNS(content, "8.8.8.8", "") + // 如果系统有活动网络接口,应该成功 + // 如果没有(如在 CI 环境),应该返回错误 + if err == nil { + s.Contains(result, "nameservers:") + s.Contains(result, "8.8.8.8") + } +} + +// TestUpdateNetplanDNSInvalidYAML 测试无效的 YAML +func (s *DNSTestSuite) TestUpdateNetplanDNSInvalidYAML() { + content := `network: + version: 2 + ethernets: + eth0: + invalid yaml here` + + _, err := updateNetplanDNS(content, "8.8.8.8", "") + s.Error(err) +} + +// TestUpdateNetplanDNSWithMatch 测试带 match 规则的配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithMatch() { + content := `network: + version: 2 + ethernets: + id0: + match: + macaddress: "00:11:22:33:44:55" + set-name: eth0 + dhcp4: true` + + result, err := updateNetplanDNS(content, "8.8.8.8", "") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "match:") + s.Contains(result, "macaddress") +} + +// TestUpdateNetplanDNSWithVlan 测试 VLAN 配置 +func (s *DNSTestSuite) TestUpdateNetplanDNSWithVlan() { + content := `network: + version: 2 + ethernets: + eth0: + dhcp4: false + vlans: + vlan100: + id: 100 + link: eth0 + addresses: + - 192.168.100.1/24` + + result, err := updateNetplanDNS(content, "8.8.8.8", "8.8.4.4") + s.NoError(err) + s.Contains(result, "nameservers:") + s.Contains(result, "vlans:") + s.Contains(result, "vlan100:") +} + +func (s *DNSTestSuite) TestFindNetplanConfig() { + // findNetplanConfig 应该能正常执行不崩溃 + // 在实际系统上可能会找到配置文件也可能找不到 + configPath, err := findNetplanConfig() + if err == nil { + // 如果找到了配置文件,验证文件确实存在 + s.FileExists(configPath) + } + // 无论是否找到配置文件,函数都应该正常返回 +} + +func (s *DNSTestSuite) TestSetDNSWithResolvConf() { + // 这个测试需要 root 权限才能写入 /etc/resolv.conf + // 在非特权环境中跳过 + s.T().Skip("需要 root 权限") +} + +func (s *DNSTestSuite) TestGetDNS() { + // GetDNS 应该能返回当前的 DNS 配置 + dns, manager, err := GetDNS() + // 即使出错也不应该 panic + if err != nil { + s.T().Logf("获取 DNS 出错(可能没有权限): %v", err) + return + } + s.NotNil(dns) + s.True(manager >= ManagerUnknown && manager <= ManagerResolvConf) +} + +// TestIsValidNetworkInterface 测试网络接口名称验证 +func (s *DNSTestSuite) TestIsValidNetworkInterface() { + // 有效的接口名 + s.True(isValidNetworkInterface("eth0")) + s.True(isValidNetworkInterface("ens3")) + s.True(isValidNetworkInterface("enp0s3")) + s.True(isValidNetworkInterface("wlan0")) + s.True(isValidNetworkInterface("bond0")) + s.True(isValidNetworkInterface("br0")) + + // 无效的接口名(虚拟接口) + s.False(isValidNetworkInterface("lo")) + s.False(isValidNetworkInterface("docker0")) + s.False(isValidNetworkInterface("veth12345")) + s.False(isValidNetworkInterface("br-abc123")) + s.False(isValidNetworkInterface("virbr0")) + s.False(isValidNetworkInterface("tun0")) + s.False(isValidNetworkInterface("tap0")) + s.False(isValidNetworkInterface("flannel.1")) + s.False(isValidNetworkInterface("cni0")) + s.False(isValidNetworkInterface("cali12345")) + s.False(isValidNetworkInterface("")) +} + +// TestDetectActiveInterface 测试活动接口检测 +func (s *DNSTestSuite) TestDetectActiveInterface() { + // 这个测试依赖系统环境,只验证函数不会 panic + iface := detectActiveInterface() + // 在有网络的环境下应该能检测到接口 + // 在 CI 环境可能返回空字符串 + if iface != "" { + s.True(isValidNetworkInterface(iface)) + } +} diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 66b0e533..0b4b2b57 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -5020,6 +5020,10 @@ msgstr "Private Key" msgid "DNS modifications will revert to default after system restart." msgstr "DNS modifications will revert to default after system restart." +#: src/views/toolbox/SystemView.vue:93 +msgid "Current DNS manager: %{ manager }" +msgstr "Current DNS manager: %{ manager }" + #: src/views/toolbox/SystemView.vue:92 msgid "Enter primary DNS server" msgstr "" diff --git a/web/src/locales/frontend.pot b/web/src/locales/frontend.pot index 38ee32cd..5d1958cb 100644 --- a/web/src/locales/frontend.pot +++ b/web/src/locales/frontend.pot @@ -5163,6 +5163,10 @@ msgstr "" msgid "DNS modifications will revert to default after system restart." msgstr "" +#: src/views/toolbox/SystemView.vue:93 +msgid "Current DNS manager: %{ manager }" +msgstr "" + #: src/views/toolbox/SystemView.vue:92 msgid "Enter primary DNS server" msgstr "" diff --git a/web/src/locales/zh_CN.po b/web/src/locales/zh_CN.po index 583a4520..50b1a624 100644 --- a/web/src/locales/zh_CN.po +++ b/web/src/locales/zh_CN.po @@ -4874,6 +4874,10 @@ msgstr "下载私钥" msgid "DNS modifications will revert to default after system restart." msgstr "DNS 修改将在系统重启后恢复为默认设置。" +#: src/views/toolbox/SystemView.vue:93 +msgid "Current DNS manager: %{ manager }" +msgstr "当前 DNS 管理方式:%{ manager }" + #: src/views/toolbox/SystemView.vue:92 msgid "Enter primary DNS server" msgstr "输入主 DNS 服务器" diff --git a/web/src/locales/zh_TW.po b/web/src/locales/zh_TW.po index 22a259fc..47e99484 100644 --- a/web/src/locales/zh_TW.po +++ b/web/src/locales/zh_TW.po @@ -4856,6 +4856,10 @@ msgstr "" msgid "DNS modifications will revert to default after system restart." msgstr "DNS 修改將在系統重新啟動後恢復為預設設置。" +#: src/views/toolbox/SystemView.vue:93 +msgid "Current DNS manager: %{ manager }" +msgstr "目前 DNS 管理方式:%{ manager }" + #: src/views/toolbox/SystemView.vue:92 msgid "Enter primary DNS server" msgstr "" diff --git a/web/src/views/toolbox/SystemView.vue b/web/src/views/toolbox/SystemView.vue index cb07f7a1..bde1a491 100644 --- a/web/src/views/toolbox/SystemView.vue +++ b/web/src/views/toolbox/SystemView.vue @@ -22,9 +22,12 @@ const timezone = ref('') const timezones = ref([]) const time = ref(DateTime.now().toMillis()) +const dnsManager = ref('') + useRequest(system.dns()).onSuccess(({ data }) => { - dns1.value = data[0] - dns2.value = data[1] + dns1.value = data.dns?.[0] ?? '' + dns2.value = data.dns?.[1] ?? '' + dnsManager.value = data.manager }) useRequest(system.swap()).onSuccess(({ data }) => { swap.value = data.size @@ -84,7 +87,14 @@ const handleSyncTime = () => { - + + {{ + $gettext('Current DNS manager: %{ manager }', { + manager: dnsManager + }) + }} + + {{ $gettext('DNS modifications will revert to default after system restart.') }}