ip route show includes state flags like 'linkdown' that ip route add does not accept, causing restore to fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
326 lines
8.8 KiB
Go
326 lines
8.8 KiB
Go
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
|
|
}
|