package interfaces import ( "fmt" "net" "os" "os/exec" "path/filepath" "sort" "strings" "networktool/internal/config" "networktool/internal/model" ) type Service struct { cfg config.Config } func New(cfg config.Config) *Service { return &Service{cfg: cfg} } func (s *Service) List() (model.InterfacesResponse, error) { items, err := s.listPhysicalInterfaces() if err != nil { return model.InterfacesResponse{}, err } management := s.detectManagement(items) for i := range items { items[i].IsManagement = items[i].SystemName == management if items[i].IsManagement { items[i].Name = "LAN2" items[i].Role = "control" } else { items[i].Name = "候选接口" items[i].Role = "business" } } suggested, requiresSelection := suggestTarget(items, management) for i := range items { items[i].IsSuggestedTarget = items[i].SystemName == suggested if items[i].IsSuggestedTarget { items[i].Name = "LAN1" } } return model.InterfacesResponse{ ManagementInterface: management, SuggestedTargetInterface: suggested, RequiresTargetSelection: requiresSelection, Interfaces: items, }, nil } func (s *Service) EnsureMaintenanceAddress() error { items, err := s.listPhysicalInterfaces() if err != nil { return err } if len(items) == 0 { return fmt.Errorf("no physical ethernet interfaces found") } management := s.detectManagement(items) if management == "" { return fmt.Errorf("failed to detect management interface") } for _, item := range items { if item.SystemName != management { continue } for _, addr := range item.IPv4 { if addr.Address == s.cfg.MaintenanceIP { return nil } } break } cmd := exec.Command("ip", "addr", "add", s.cfg.MaintenanceCIDR, "dev", management) output, err := cmd.CombinedOutput() if err != nil { if strings.Contains(string(output), "File exists") { return nil } return fmt.Errorf("ip addr add failed on %s: %s", management, strings.TrimSpace(string(output))) } return nil } func (s *Service) listPhysicalInterfaces() ([]model.NetworkInterface, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } items := make([]model.NetworkInterface, 0) for _, iface := range ifaces { if !isPhysicalEthernet(iface.Name) { continue } item := model.NetworkInterface{ SystemName: iface.Name, MAC: iface.HardwareAddr.String(), LinkUp: readLinkUp(iface.Name), Gateway: "", DNS: []string{}, } item.IPv4 = readIPv4(iface) items = append(items, item) } sort.Slice(items, func(i, j int) bool { return items[i].SystemName < items[j].SystemName }) return items, nil } func (s *Service) detectManagement(items []model.NetworkInterface) string { for _, item := range items { for _, addr := range item.IPv4 { if addr.Address == s.cfg.MaintenanceIP { return item.SystemName } } } if len(items) == 2 { return items[1].SystemName } if len(items) > 0 { return items[len(items)-1].SystemName } return "" } func suggestTarget(items []model.NetworkInterface, management string) (string, bool) { candidates := make([]string, 0) for _, item := range items { if item.SystemName != management { candidates = append(candidates, item.SystemName) } } if len(candidates) == 1 { return candidates[0], false } return "", len(candidates) > 1 } func readIPv4(iface net.Interface) []model.IPv4Address { addrs, err := iface.Addrs() if err != nil { return nil } result := make([]model.IPv4Address, 0) for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok || ipNet.IP.To4() == nil { continue } prefix, _ := ipNet.Mask.Size() source := "unknown" if ipNet.IP.String() == "169.254.100.2" { source = "static" } result = append(result, model.IPv4Address{Address: ipNet.IP.String(), Prefix: prefix, Source: source}) } return result } func isPhysicalEthernet(name string) bool { if name == "lo" { return false } for _, prefix := range []string{"docker", "br-", "veth", "virbr", "tun", "tap", "wg", "zt", "vmnet"} { if strings.HasPrefix(name, prefix) { return false } } devicePath := filepath.Join("/sys/class/net", name, "device") if _, err := os.Stat(devicePath); err != nil { return false } return true } func readLinkUp(name string) bool { carrierPath := filepath.Join("/sys/class/net", name, "carrier") data, err := os.ReadFile(carrierPath) if err == nil { return strings.TrimSpace(string(data)) == "1" } statePath := filepath.Join("/sys/class/net", name, "operstate") data, err = os.ReadFile(statePath) if err != nil { return false } return strings.TrimSpace(string(data)) == "up" }