package aerotun import ( "encoding/json" "fmt" "net" "os/exec" ) func runIP(args ...string) ([]byte, error) { out, err := exec.Command("ip", args...).CombinedOutput() if err != nil { return out, fmt.Errorf("ip %v: %s: %w", args, out, err) } return out, nil } type routeManager struct { run func(...string) ([]byte, error) owned [][]string } func (r *routeManager) add(args ...string) error { if _, err := r.run(append([]string{"-4", "route", "add"}, args...)...); err != nil { return err } r.owned = append(r.owned, append([]string(nil), args...)) return nil } func (r *routeManager) cleanup() { for i := len(r.owned) - 1; i >= 0; i-- { _, _ = r.run(append([]string{"-4", "route", "del"}, r.owned[i]...)...) } r.owned = nil } func (r *routeManager) install(name string, endpoint net.IP) (err error) { if endpoint.To4() == nil { return fmt.Errorf("default route currently requires an IPv4 remote endpoint") } defer func() { if err != nil { r.cleanup() } }() out, err := r.run("-j", "-4", "route", "get", endpoint.String()) if err != nil { return err } var paths []struct { Dev string `json:"dev"` Gateway string `json:"gateway"` } if err = json.Unmarshal(out, &paths); err != nil { return err } if len(paths) != 1 || paths[0].Dev == "" || paths[0].Dev == name { return fmt.Errorf("cannot find independent underlay route to remote endpoint") } host := endpoint.String() + "/32" // Preserve a pre-existing host route. Never replace routes owned by others. out, err = r.run("-j", "-4", "route", "show", "exact", host) if err != nil { return err } var existing []json.RawMessage if err = json.Unmarshal(out, &existing); err != nil { return err } if len(existing) == 0 { args := []string{host} if paths[0].Gateway != "" { args = append(args, "via", paths[0].Gateway) } args = append(args, "dev", paths[0].Dev) if err = r.add(args...); err != nil { return err } } // Split defaults leave the original default intact, so stopping/rolling back // removes only our routes. A pinned endpoint stays outside the tunnel. if err = r.add("0.0.0.0/1", "dev", name); err != nil { return err } if err = r.add("128.0.0.0/1", "dev", name); err != nil { return err } return nil }