2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00
Files
panel/pkg/dns/dns.go
2026-01-24 05:49:04 +08:00

588 lines
15 KiB
Go

// 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()
var dns []string
var err error
switch manager {
case ManagerNetworkManager:
dns, err = getDNSFromNetworkManager()
case ManagerNetplan:
dns, err = getDNSFromNetplan()
default:
dns, err = getDNSFromResolvConf()
}
// 如果从配置源读取失败或为空,回退到 resolv.conf
if err != nil || len(dns) == 0 {
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
}
// getDNSFromNetworkManager 从 NetworkManager 获取 DNS
func getDNSFromNetworkManager() ([]string, error) {
// 获取默认路由的网络接口
iface := detectActiveInterface()
if iface == "" {
return nil, fmt.Errorf("no active network interface found")
}
output, err := shell.Execf("nmcli -t -f IP4.DNS device show %s", iface)
if err != nil {
return nil, err
}
var dns []string
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
// 格式: IP4.DNS[1]:8.8.8.8
if strings.HasPrefix(line, "IP4.DNS") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
dnsAddr := strings.TrimSpace(parts[1])
if dnsAddr != "" {
dns = append(dns, dnsAddr)
}
}
}
}
return dns, nil
}
// getDNSFromNetplan 从 netplan 配置文件获取 DNS
func getDNSFromNetplan() ([]string, error) {
configPath, err := findNetplanConfig()
if err != nil {
return nil, err
}
content, err := io.Read(configPath)
if err != nil {
return nil, err
}
var config netplanConfig
if err = yaml.Unmarshal([]byte(content), &config); err != nil {
return nil, err
}
// 从所有接口收集 DNS
var dns []string
seen := make(map[string]bool)
collectDNS := func(iface *netplanInterface) {
if iface != nil && iface.Nameservers != nil {
for _, addr := range iface.Nameservers.Addresses {
if !seen[addr] {
seen[addr] = true
dns = append(dns, addr)
}
}
}
}
for _, iface := range config.Network.Ethernets {
collectDNS(iface)
}
for _, iface := range config.Network.Wifis {
collectDNS(iface)
}
for _, iface := range config.Network.Bonds {
collectDNS(iface)
}
for _, iface := range config.Network.Bridges {
collectDNS(iface)
}
for _, iface := range config.Network.Vlans {
collectDNS(iface)
}
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, 0644); 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
}