package platform import ( "bytes" "errors" "fmt" "os" "os/exec" "sort" "strings" ) func (s *System) ListInterfaces() ([]InterfaceInfo, error) { names, err := listInterfaceNames() if err != nil { return nil, err } out := make([]InterfaceInfo, 0, len(names)) for _, name := range names { state := "unknown" if up, err := interfaceAdminState(name); err == nil { if up { state = "up" } else { state = "down" } } ipv4, err := interfaceIPv4Addrs(name) if err != nil { ipv4 = nil } out = append(out, InterfaceInfo{Name: name, State: state, IPv4: ipv4}) } return out, nil } func (s *System) DefaultRoute() string { raw, err := exec.Command("ip", "route", "show", "default").Output() if err != nil { return "" } fields := strings.Fields(string(raw)) for i := 0; i < len(fields)-1; i++ { if fields[i] == "via" { return fields[i+1] } } return "" } func (s *System) CaptureNetworkSnapshot() (NetworkSnapshot, error) { names, err := listInterfaceNames() if err != nil { return NetworkSnapshot{}, err } snapshot := NetworkSnapshot{ Interfaces: make([]NetworkInterfaceSnapshot, 0, len(names)), } for _, name := range names { up, err := interfaceAdminState(name) if err != nil { return NetworkSnapshot{}, err } ipv4, err := interfaceIPv4Addrs(name) if err != nil { return NetworkSnapshot{}, err } snapshot.Interfaces = append(snapshot.Interfaces, NetworkInterfaceSnapshot{ Name: name, Up: up, IPv4: ipv4, }) } if raw, err := exec.Command("ip", "route", "show", "default").Output(); err == nil { for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { line = strings.TrimSpace(line) if line != "" { snapshot.DefaultRoutes = append(snapshot.DefaultRoutes, line) } } } if raw, err := os.ReadFile("/etc/resolv.conf"); err == nil { snapshot.ResolvConf = string(raw) } return snapshot, nil } func (s *System) RestoreNetworkSnapshot(snapshot NetworkSnapshot) error { var errs []string for _, iface := range snapshot.Interfaces { if err := exec.Command("ip", "link", "set", "dev", iface.Name, "up").Run(); err != nil { errs = append(errs, fmt.Sprintf("%s: bring up before restore: %v", iface.Name, err)) continue } if err := exec.Command("ip", "addr", "flush", "dev", iface.Name).Run(); err != nil { errs = append(errs, fmt.Sprintf("%s: flush addresses: %v", iface.Name, err)) } for _, cidr := range iface.IPv4 { if raw, err := exec.Command("ip", "addr", "add", cidr, "dev", iface.Name).CombinedOutput(); err != nil { detail := strings.TrimSpace(string(raw)) if detail != "" { errs = append(errs, fmt.Sprintf("%s: restore address %s: %v: %s", iface.Name, cidr, err, detail)) } else { errs = append(errs, fmt.Sprintf("%s: restore address %s: %v", iface.Name, cidr, err)) } } } state := "down" if iface.Up { state = "up" } if err := exec.Command("ip", "link", "set", "dev", iface.Name, state).Run(); err != nil { errs = append(errs, fmt.Sprintf("%s: restore state %s: %v", iface.Name, state, err)) } } if err := exec.Command("ip", "route", "del", "default").Run(); err != nil { var exitErr *exec.ExitError if !errors.As(err, &exitErr) { errs = append(errs, fmt.Sprintf("clear default route: %v", err)) } } for _, route := range snapshot.DefaultRoutes { fields := strings.Fields(route) if len(fields) == 0 { continue } // Strip state flags that ip-route(8) does not accept as add arguments. filtered := fields[:0] for _, f := range fields { switch f { case "linkdown", "dead", "onlink", "pervasive": // skip default: filtered = append(filtered, f) } } args := append([]string{"route", "add"}, filtered...) if raw, err := exec.Command("ip", args...).CombinedOutput(); err != nil { detail := strings.TrimSpace(string(raw)) if detail != "" { errs = append(errs, fmt.Sprintf("restore route %q: %v: %s", route, err, detail)) } else { errs = append(errs, fmt.Sprintf("restore route %q: %v", route, err)) } } } if err := os.WriteFile("/etc/resolv.conf", []byte(snapshot.ResolvConf), 0644); err != nil { errs = append(errs, fmt.Sprintf("restore resolv.conf: %v", err)) } if len(errs) > 0 { return errors.New(strings.Join(errs, "; ")) } return nil } func (s *System) DHCPOne(iface string) (string, error) { var out bytes.Buffer if err := exec.Command("ip", "link", "set", iface, "up").Run(); err != nil { fmt.Fprintf(&out, "WARN: ip link set up failed: %v\n", err) } if raw, err := exec.Command("dhclient", "-r", iface).CombinedOutput(); err == nil { out.Write(raw) } else if len(raw) > 0 { out.Write(raw) } raw, err := exec.Command("dhclient", "-4", "-v", iface).CombinedOutput() out.Write(raw) if err != nil { return out.String(), err } return out.String(), nil } func (s *System) DHCPAll() (string, error) { ifaces, err := listInterfaceNames() if err != nil { return "", err } var out strings.Builder for _, iface := range ifaces { fmt.Fprintf(&out, "[%s]\n", iface) log, err := s.DHCPOne(iface) out.WriteString(log) if err != nil { fmt.Fprintf(&out, "ERROR: %v\n", err) } out.WriteString("\n") } return out.String(), nil } func (s *System) SetStaticIPv4(cfg StaticIPv4Config) (string, error) { if cfg.Interface == "" || cfg.Address == "" || cfg.Prefix == "" { return "", fmt.Errorf("interface, address, and prefix are required") } dns := cfg.DNS if len(dns) == 0 { dns = []string{"77.88.8.8", "77.88.8.1", "1.1.1.1", "8.8.8.8"} } var out strings.Builder _ = exec.Command("ip", "link", "set", cfg.Interface, "up").Run() _ = exec.Command("ip", "addr", "flush", "dev", cfg.Interface).Run() if raw, err := exec.Command("ip", "addr", "add", cfg.Address+"/"+cfg.Prefix, "dev", cfg.Interface).CombinedOutput(); err != nil { return string(raw), err } out.WriteString("address configured\n") if cfg.Gateway != "" { _ = exec.Command("ip", "route", "del", "default").Run() if raw, err := exec.Command("ip", "route", "add", "default", "via", cfg.Gateway, "dev", cfg.Interface).CombinedOutput(); err != nil { return out.String() + string(raw), err } out.WriteString("default route configured\n") } var resolv strings.Builder for _, dnsServer := range dns { dnsServer = strings.TrimSpace(dnsServer) if dnsServer == "" { continue } fmt.Fprintf(&resolv, "nameserver %s\n", dnsServer) } if err := os.WriteFile("/etc/resolv.conf", []byte(resolv.String()), 0644); err != nil { return out.String(), err } out.WriteString("dns configured\n") return out.String(), nil } // SetInterfaceState brings a network interface up or down. func (s *System) SetInterfaceState(iface string, up bool) error { state := "down" if up { state = "up" } return exec.Command("ip", "link", "set", "dev", iface, state).Run() } // GetInterfaceState returns true if the interface is UP. func (s *System) GetInterfaceState(iface string) (bool, error) { return interfaceAdminState(iface) } func interfaceAdminState(iface string) (bool, error) { raw, err := exec.Command("ip", "-o", "link", "show", "dev", iface).Output() if err != nil { return false, err } return parseInterfaceAdminState(string(raw)) } func parseInterfaceAdminState(raw string) (bool, error) { start := strings.IndexByte(raw, '<') if start == -1 { return false, fmt.Errorf("ip link output missing flags") } end := strings.IndexByte(raw[start+1:], '>') if end == -1 { return false, fmt.Errorf("ip link output missing flag terminator") } flags := strings.Split(raw[start+1:start+1+end], ",") for _, flag := range flags { if strings.TrimSpace(flag) == "UP" { return true, nil } } return false, nil } func interfaceIPv4Addrs(iface string) ([]string, error) { raw, err := exec.Command("ip", "-o", "-4", "addr", "show", "dev", iface).Output() if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { return nil, nil } return nil, err } var ipv4 []string for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { fields := strings.Fields(line) if len(fields) >= 4 { ipv4 = append(ipv4, fields[3]) } } return ipv4, nil } func listInterfaceNames() ([]string, error) { raw, err := exec.Command("ip", "-o", "link", "show").Output() if err != nil { return nil, err } var out []string for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { fields := strings.SplitN(line, ": ", 3) if len(fields) < 2 { continue } name := fields[1] if name == "lo" || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "virbr") || strings.HasPrefix(name, "veth") || strings.HasPrefix(name, "tun") || strings.HasPrefix(name, "tap") || strings.HasPrefix(name, "br-") || strings.HasPrefix(name, "bond") || strings.HasPrefix(name, "dummy") { continue } out = append(out, name) } sort.Strings(out) return out, nil }