2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 04:22:33 +08:00

feat(dns): 优化系统工具箱DNS设置以适配现代Linux系统 (#1202)

* 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: 耗子 <haozi@loli.email>
This commit is contained in:
Copilot
2026-01-09 06:33:23 +08:00
committed by GitHub
parent f2d3911266
commit 47537e282b
8 changed files with 845 additions and 16 deletions

View File

@@ -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
}

484
pkg/dns/dns.go Normal file
View File

@@ -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
}

325
pkg/dns/dns_test.go Normal file
View File

@@ -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))
}
}

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 服务器"

View File

@@ -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 ""

View File

@@ -22,9 +22,12 @@ const timezone = ref('')
const timezones = ref<any[]>([])
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 = () => {
<n-tabs v-model:value="currentTab" type="line" placement="left" animated>
<n-tab-pane name="dns" tab="DNS">
<n-flex vertical>
<n-alert type="warning">
<n-alert type="info">
{{
$gettext('Current DNS manager: %{ manager }', {
manager: dnsManager
})
}}
</n-alert>
<n-alert v-if="dnsManager === 'resolv.conf'" type="warning">
{{ $gettext('DNS modifications will revert to default after system restart.') }}
</n-alert>
<n-form>