Files
panel/pkg/network/network.go
T
2026-08-15 00:24:14 +08:00

512 lines
15 KiB
Go

package network
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"time"
)
const (
ModeAuto = "auto"
ModeManual = "manual"
ModeDisabled = "disabled"
// ConfirmTimeout 变更后等待确认的时长,超时未确认自动回滚,
// 避免配错导致远程失联后无法恢复
ConfirmTimeout = 30 * time.Second
)
// ErrValidation 配置不合法,服务层据此返回 422
var ErrValidation = errors.New("invalid network configuration")
type FamilyConfig struct {
Mode string `json:"mode"`
Addresses []string `json:"addresses"`
Gateway string `json:"gateway"`
AutoDNS bool `json:"auto_dns"`
DNS []string `json:"dns"`
}
type Config struct {
Name string `json:"name"`
MTU int `json:"mtu"`
IPv4 FamilyConfig `json:"ipv4"`
IPv6 FamilyConfig `json:"ipv6"`
}
// FamilyState 网卡当前生效的网络状态,自动获取时配置里没有这些值,用于回填表单
type FamilyState struct {
Addresses []string `json:"addresses"`
Gateway string `json:"gateway"`
DNS []string `json:"dns"`
}
type Interface struct {
Name string `json:"name"`
Type string `json:"type"`
State string `json:"state"`
MAC string `json:"mac"`
CurrentMTU int `json:"current_mtu"`
ConfiguredMTU int `json:"configured_mtu"`
CurrentIPv4 FamilyState `json:"current_ipv4"`
CurrentIPv6 FamilyState `json:"current_ipv6"`
Editable bool `json:"editable"`
Reason string `json:"reason"`
IPv4 FamilyConfig `json:"ipv4"`
IPv6 FamilyConfig `json:"ipv6"`
}
type Result struct {
Manager string `json:"manager"`
Items []Interface `json:"items"`
// Pending 为真表示有变更等待确认
Pending bool `json:"pending"`
}
// backend 封装不同网络管理器的配置读写
type backend interface {
Name() string
available(ctx context.Context) bool
// Load 填充网卡的持久化配置,不可编辑时写入 Reason
Load(ctx context.Context, items []Interface) error
// Apply 应用配置,返回撤销本次变更的回滚函数
Apply(ctx context.Context, item Interface, config Config) (func(context.Context) error, error)
}
// Service 网卡配置管理,同一时间只允许一个变更处于待确认状态
type Service struct {
apply sync.Mutex
mu sync.RWMutex
backend backend
rollback func(context.Context) error
timer *time.Timer
}
func New() *Service {
return &Service{}
}
func (s *Service) Interfaces(ctx context.Context) (*Result, error) {
items, err := runtimeInterfaces()
if err != nil {
return nil, err
}
ipv4Gateways, ipv6Gateways := defaultGateways(ctx, true), defaultGateways(ctx, false)
for i := range items {
ipv4, ipv6 := currentDNS(ctx, items[i].Name)
items[i].CurrentIPv4.Gateway, items[i].CurrentIPv4.DNS = ipv4Gateways[items[i].Name], ipv4
items[i].CurrentIPv6.Gateway, items[i].CurrentIPv6.DNS = ipv6Gateways[items[i].Name], ipv6
}
current := s.detect(ctx)
if current == nil {
for i := range items {
items[i].Reason = "unsupported network manager"
}
return &Result{Manager: "unsupported", Items: items}, nil
}
if err = current.Load(ctx, items); err != nil {
return nil, err
}
return &Result{Manager: current.Name(), Items: items, Pending: s.pending()}, nil
}
// Update 应用网卡配置,成功后进入待确认状态,超时未确认自动回滚
func (s *Service) Update(ctx context.Context, config Config) error {
if err := Validate(config); err != nil {
return err
}
s.apply.Lock()
defer s.apply.Unlock()
if s.pending() {
return fmt.Errorf("%w: a previous change is still waiting for confirmation", ErrValidation)
}
current := s.detect(ctx)
if current == nil {
return fmt.Errorf("%w: unsupported network manager", ErrValidation)
}
items, err := runtimeInterfaces()
if err != nil {
return err
}
index := slices.IndexFunc(items, func(item Interface) bool { return item.Name == config.Name })
if index < 0 {
return fmt.Errorf("%w: network interface does not exist", ErrValidation)
}
// 只加载目标网卡,避免为改一张网卡读遍全部配置
target := items[index : index+1]
if err = current.Load(ctx, target); err != nil {
return err
}
if !target[0].Editable {
return fmt.Errorf("%w: %s", ErrValidation, target[0].Reason)
}
rollback, err := current.Apply(ctx, target[0], config)
if err != nil {
return err
}
if err = verify(config); err != nil {
return errors.Join(err, rollback(context.WithoutCancel(ctx)))
}
s.mu.Lock()
s.rollback = rollback
s.timer = time.AfterFunc(ConfirmTimeout, s.expire)
s.mu.Unlock()
return nil
}
func (s *Service) Confirm() error {
if s.take() == nil {
return fmt.Errorf("%w: no change is waiting for confirmation", ErrValidation)
}
return nil
}
func (s *Service) Rollback(ctx context.Context) error {
s.apply.Lock()
defer s.apply.Unlock()
rollback := s.take()
if rollback == nil {
return fmt.Errorf("%w: no change is waiting for confirmation", ErrValidation)
}
return rollback(ctx)
}
// expire 等待确认超时,自动回滚
func (s *Service) expire() {
s.apply.Lock()
defer s.apply.Unlock()
rollback := s.take()
if rollback == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
_ = rollback(ctx)
}
// pending 是否有变更等待确认
func (s *Service) pending() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.rollback != nil
}
// take 取出待确认的回滚函数并清空状态,返回 nil 表示没有待确认变更
func (s *Service) take() func(context.Context) error {
s.mu.Lock()
defer s.mu.Unlock()
rollback := s.rollback
if rollback != nil {
s.timer.Stop()
s.timer, s.rollback = nil, nil
}
return rollback
}
// detect 探测网络管理器,结果在进程生命周期内缓存
func (s *Service) detect(ctx context.Context) backend {
s.mu.RLock()
cached := s.backend
s.mu.RUnlock()
if cached != nil {
return cached
}
var found backend
for _, candidate := range []backend{&netplanBackend{}, &networkManagerBackend{}, &ifupdownBackend{}} {
if candidate.available(ctx) {
found = candidate
break
}
}
if found == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.backend == nil {
s.backend = found
}
return s.backend
}
// verify 确认配置已在网卡上生效,挡住应用命令返回成功但实际未生效的情况
func verify(config Config) error {
current, err := net.InterfaceByName(config.Name)
if err != nil {
return fmt.Errorf("network interface %s disappeared after applying: %w", config.Name, err)
}
if current.Flags&net.FlagUp == 0 {
return fmt.Errorf("network interface %s is down after applying", config.Name)
}
ipv4, ipv6 := currentAddresses(*current)
if err = verifyAddresses(config.Name, config.IPv4, ipv4); err != nil {
return err
}
return verifyAddresses(config.Name, config.IPv6, ipv6)
}
func verifyAddresses(name string, config FamilyConfig, applied []string) error {
if config.Mode != ModeManual {
return nil
}
for _, address := range config.Addresses {
if !slices.Contains(applied, address) {
return fmt.Errorf("address %s was not applied to %s", address, name)
}
}
return nil
}
func Validate(config Config) error {
if config.Name == "" || filepath.Base(config.Name) != config.Name || strings.ContainsAny(config.Name, " \t\r\n/") {
return fmt.Errorf("%w: invalid network interface name", ErrValidation)
}
if config.MTU != 0 && (config.MTU < 68 || config.MTU > 65535) {
return fmt.Errorf("%w: MTU must be 0 or between 68 and 65535", ErrValidation)
}
if config.IPv4.Mode == ModeDisabled && config.IPv6.Mode == ModeDisabled {
return fmt.Errorf("%w: IPv4 and IPv6 cannot both be disabled", ErrValidation)
}
if config.IPv6.Mode != ModeDisabled && config.MTU != 0 && config.MTU < 1280 {
return fmt.Errorf("%w: MTU must not be lower than 1280 when IPv6 is enabled", ErrValidation)
}
if err := validateFamily("IPv4", config.IPv4, true); err != nil {
return err
}
return validateFamily("IPv6", config.IPv6, false)
}
func validateFamily(name string, config FamilyConfig, ipv4 bool) error {
if !slices.Contains([]string{ModeAuto, ModeManual, ModeDisabled}, config.Mode) {
return fmt.Errorf("%w: invalid %s mode", ErrValidation, name)
}
if config.Mode == ModeDisabled {
if len(config.Addresses) > 0 || config.Gateway != "" || len(config.DNS) > 0 {
return fmt.Errorf("%w: disabled %s configuration must be empty", ErrValidation, name)
}
return nil
}
if config.Mode == ModeManual && len(config.Addresses) == 0 {
return fmt.Errorf("%w: manual %s configuration requires an address", ErrValidation, name)
}
addresses := make(map[netip.Prefix]struct{}, len(config.Addresses))
for _, value := range config.Addresses {
prefix, err := netip.ParsePrefix(value)
if err != nil || prefix.Addr().Is4() != ipv4 {
return fmt.Errorf("%w: invalid %s CIDR: %s", ErrValidation, name, value)
}
if _, exists := addresses[prefix]; exists {
return fmt.Errorf("%w: duplicate %s address: %s", ErrValidation, name, value)
}
addresses[prefix] = struct{}{}
}
if config.Gateway != "" {
gateway, err := netip.ParseAddr(config.Gateway)
if err != nil || gateway.Is4() != ipv4 {
return fmt.Errorf("%w: invalid %s gateway", ErrValidation, name)
}
}
dns := make(map[netip.Addr]struct{}, len(config.DNS))
for _, value := range config.DNS {
server, err := netip.ParseAddr(value)
if err != nil || server.Is4() != ipv4 {
return fmt.Errorf("%w: invalid %s DNS: %s", ErrValidation, name, value)
}
if _, exists := dns[server]; exists {
return fmt.Errorf("%w: duplicate %s DNS: %s", ErrValidation, name, value)
}
dns[server] = struct{}{}
}
return nil
}
// runtimeInterfaces 列出内核中可管理的物理网卡
func runtimeInterfaces() ([]Interface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
items := make([]Interface, 0, len(interfaces))
for _, current := range interfaces {
kind := interfaceType(current.Name)
if kind == "" {
continue
}
state := "down"
if current.Flags&net.FlagUp != 0 {
state = "up"
}
ipv4, ipv6 := currentAddresses(current)
items = append(items, Interface{
Name: current.Name, Type: kind, State: state, MAC: current.HardwareAddr.String(),
CurrentMTU: current.MTU,
CurrentIPv4: FamilyState{Addresses: ipv4},
CurrentIPv6: FamilyState{Addresses: ipv6},
IPv4: emptyFamily(), IPv6: emptyFamily(),
})
}
slices.SortFunc(items, func(a, b Interface) int { return strings.Compare(a.Name, b.Name) })
return items, nil
}
// interfaceType 识别网卡类型,容器与隧道等虚拟设备不纳入管理
func interfaceType(name string) string {
base := filepath.Join("/sys/class/net", name)
switch {
case name == "lo":
return ""
case exists(filepath.Join(base, "bonding")):
return "bond"
case exists(filepath.Join(base, "bridge")):
return "bridge"
case hasVLANParent(base):
return "vlan"
case !exists(filepath.Join(base, "device")):
// 无对应硬件设备即为虚拟网卡(docker0、veth、tun 等)
return ""
case exists(filepath.Join(base, "wireless")), exists(filepath.Join(base, "phy80211")):
return "wifi"
default:
return "ethernet"
}
}
func currentAddresses(current net.Interface) ([]string, []string) {
addresses, _ := current.Addrs()
ipv4, ipv6 := make([]string, 0), make([]string, 0)
for _, address := range addresses {
prefix, err := netip.ParsePrefix(address.String())
if err != nil || prefix.Addr().IsUnspecified() {
continue
}
if prefix.Addr().Is4() {
ipv4 = append(ipv4, address.String())
} else if !prefix.Addr().IsLinkLocalUnicast() {
ipv6 = append(ipv6, address.String())
}
}
return unique(ipv4), unique(ipv6)
}
func emptyFamily() FamilyConfig {
return FamilyConfig{Mode: ModeDisabled, Addresses: []string{}, AutoDNS: true, DNS: []string{}}
}
func unique(values []string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value = strings.TrimSpace(value); value != "" && !slices.Contains(result, value) {
result = append(result, value)
}
}
slices.Sort(result)
return result
}
// currentDNS 读取网卡当前生效的 DNS,按地址族分开返回。
// systemd-resolved 按网卡维护 DNS,此时 resolv.conf 内只有本机 stub 地址
func currentDNS(ctx context.Context, name string) ([]string, []string) {
var servers []string
if output, err := run(ctx, "resolvectl", "dns", name); err == nil {
// 输出形如 Link 2 (ens5): 183.60.83.19 183.60.82.98
if _, values, ok := strings.Cut(output, ":"); ok {
servers = strings.Fields(values)
}
} else if content, readErr := os.ReadFile("/etc/resolv.conf"); readErr == nil {
for line := range strings.SplitSeq(string(content), "\n") {
if fields := strings.Fields(line); len(fields) == 2 && fields[0] == "nameserver" {
servers = append(servers, fields[1])
}
}
}
ipv4, ipv6 := make([]string, 0), make([]string, 0)
for _, server := range servers {
address, err := netip.ParseAddr(server)
// 本机 stub 解析器不是真实上游,回填无意义
if err != nil || address.IsLoopback() {
continue
}
if address.Is4() {
ipv4 = append(ipv4, server)
} else {
ipv6 = append(ipv6, server)
}
}
return ipv4, ipv6
}
// defaultGateways 读取各网卡当前生效的默认网关
func defaultGateways(ctx context.Context, ipv4 bool) map[string]string {
family := "-6"
if ipv4 {
family = "-4"
}
output, err := run(ctx, "ip", family, "route", "show", "default")
if err != nil {
return nil
}
gateways := make(map[string]string)
for line := range strings.SplitSeq(output, "\n") {
fields := strings.Fields(line)
var gateway, device string
for i := 0; i+1 < len(fields); i++ {
switch fields[i] {
case "via":
gateway = fields[i+1]
case "dev":
device = fields[i+1]
}
}
// 同一网卡可能有多条默认路由,取指标最优的第一条
if gateway != "" && device != "" && gateways[device] == "" {
gateways[device] = gateway
}
}
return gateways
}
func hasCommand(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}
func run(ctx context.Context, name string, args ...string) (string, error) {
output, err := exec.CommandContext(ctx, name, args...).CombinedOutput()
if err != nil {
return "", fmt.Errorf("%s: %w: %s", name, err, strings.TrimSpace(string(output)))
}
return strings.TrimSpace(string(output)), nil
}
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// hasVLANParent VLAN 在 sysfs 下有指向父设备的 lower_* 链接,
// 而 bond 与 bridge 已在前面按各自目录识别
func hasVLANParent(base string) bool {
matches, _ := filepath.Glob(filepath.Join(base, "lower_*"))
return len(matches) > 0
}