diff --git a/README.md b/README.md index 9f6c8563..c23f3b3b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ An open source implementation of the Tailscale coordination server. - [x] Node registration through the web flow - [x] Network changes are relied to the nodes - [x] ~~Multiuser~~ Namespace support +- [x] Basic routing (advertise & accept) - [ ] Share nodes between ~~users~~ namespaces - [ ] Node registration via pre-auth keys - [ ] ACLs diff --git a/app.go b/app.go index b29ff985..75fd1f7d 100644 --- a/app.go +++ b/app.go @@ -1,12 +1,15 @@ package headscale import ( + "encoding/json" "fmt" "log" "os" "sync" "github.com/gin-gonic/gin" + "github.com/jinzhu/gorm/dialects/postgres" + "inet.af/netaddr" "tailscale.com/tailcfg" "tailscale.com/wgengine/wgcfg" ) @@ -113,3 +116,72 @@ func (h *Headscale) RegisterMachine(key string, namespace string) error { fmt.Println("Machine registered 🎉") return nil } + +func (h *Headscale) ListNodeRoutes(namespace string, nodeName string) error { + m, err := h.GetMachine(namespace, nodeName) + if err != nil { + return err + } + + hi, err := m.GetHostInfo() + if err != nil { + return err + } + fmt.Println(hi.RoutableIPs) + return nil +} + +func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr string) error { + m, err := h.GetMachine(namespace, nodeName) + if err != nil { + return err + } + hi, err := m.GetHostInfo() + if err != nil { + return err + } + route, err := netaddr.ParseIPPrefix(routeStr) + if err != nil { + return err + } + + for _, rIP := range hi.RoutableIPs { + if rIP == route { + db, err := h.db() + if err != nil { + log.Printf("Cannot open DB: %s", err) + return err + } + defer db.Close() + routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest + m.EnabledRoutes = postgres.Jsonb{RawMessage: json.RawMessage(routes)} + db.Save(&m) + db.Close() + + peers, _ := h.getPeers(*m) + h.pollMu.Lock() + for _, p := range *peers { + if pUp, ok := h.clientsPolling[uint64(p.ID)]; ok { + pUp <- []byte{} + } else { + } + } + h.pollMu.Unlock() + return nil + } + } + return fmt.Errorf("Could not find routable range") + +} + +func eqCIDRs(a, b []netaddr.IPPrefix) bool { + if len(a) != len(b) || ((a == nil) != (b == nil)) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} diff --git a/cmd/headscale/headscale.go b/cmd/headscale/headscale.go index efaf4222..28feb66b 100644 --- a/cmd/headscale/headscale.go +++ b/cmd/headscale/headscale.go @@ -102,7 +102,7 @@ var createNamespaceCmd = &cobra.Command{ var listNamespacesCmd = &cobra.Command{ Use: "list", - Short: "Creates a new namespace", + Short: "List all the namespaces", Run: func(cmd *cobra.Command, args []string) { h, err := getHeadscaleApp() if err != nil { @@ -120,6 +120,55 @@ var listNamespacesCmd = &cobra.Command{ }, } +var nodeCmd = &cobra.Command{ + Use: "node", + Short: "Manage the nodes of Headscale", +} + +var listRoutesCmd = &cobra.Command{ + Use: "list-routes NAMESPACE NODE", + Short: "List the routes exposed by this node", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 2 { + return fmt.Errorf("Missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + err = h.ListNodeRoutes(args[0], args[1]) + if err != nil { + fmt.Println(err) + return + } + }, +} + +var enableRouteCmd = &cobra.Command{ + Use: "enable-route", + Short: "Allows exposing a route declared by this node to the rest of the nodes", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 3 { + return fmt.Errorf("Missing parameters") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + h, err := getHeadscaleApp() + if err != nil { + log.Fatalf("Error initializing: %s", err) + } + err = h.EnableNodeRoute(args[0], args[1], args[2]) + if err != nil { + fmt.Println(err) + return + } + }, +} + func main() { viper.SetConfigName("config") viper.AddConfigPath(".") @@ -136,6 +185,10 @@ func main() { namespaceCmd.AddCommand(createNamespaceCmd) namespaceCmd.AddCommand(listNamespacesCmd) + headscaleCmd.AddCommand(nodeCmd) + nodeCmd.AddCommand(listRoutesCmd) + nodeCmd.AddCommand(enableRouteCmd) + if err := headscaleCmd.Execute(); err != nil { fmt.Println(err) os.Exit(-1) diff --git a/machine.go b/machine.go index ea15cb27..bd3e9c45 100644 --- a/machine.go +++ b/machine.go @@ -8,6 +8,7 @@ import ( "strconv" "time" + "github.com/davecgh/go-spew/spew" "github.com/jinzhu/gorm/dialects/postgres" "inet.af/netaddr" "tailscale.com/tailcfg" @@ -29,8 +30,9 @@ type Machine struct { LastSeen *time.Time Expiry *time.Time - HostInfo postgres.Jsonb - Endpoints postgres.Jsonb + HostInfo postgres.Jsonb + Endpoints postgres.Jsonb + EnabledRoutes postgres.Jsonb CreatedAt time.Time UpdatedAt time.Time @@ -64,14 +66,34 @@ func (m Machine) toNode() (*tailcfg.Node, error) { } addrs := []netaddr.IPPrefix{} - allowedIPs := []netaddr.IPPrefix{} - ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", m.IPAddress)) if err != nil { return nil, err } - addrs = append(addrs, ip) // missing the ipv6 ? - allowedIPs = append(allowedIPs, ip) // looks like the client expect this + addrs = append(addrs, ip) // missing the ipv6 ? + + allowedIPs := []netaddr.IPPrefix{} + allowedIPs = append(allowedIPs, ip) + + routesStr := []string{} + if len(m.EnabledRoutes.RawMessage) != 0 { + allwIps, err := m.EnabledRoutes.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(allwIps, &routesStr) + if err != nil { + return nil, err + } + } + + for _, aip := range routesStr { + ip, err := netaddr.ParseIPPrefix(aip) + if err != nil { + return nil, err + } + allowedIPs = append(allowedIPs, ip) + } endpoints := []string{} if len(m.Endpoints.RawMessage) != 0 { @@ -126,6 +148,7 @@ func (m Machine) toNode() (*tailcfg.Node, error) { MachineAuthorized: m.Registered, } + spew.Dump(n) // n.Key.MarshalText() return &n, nil } @@ -156,3 +179,32 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) { sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID }) return &peers, nil } + +func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error) { + machines, err := h.ListMachinesInNamespace(namespace) + if err != nil { + return nil, err + } + + for _, m := range *machines { + if m.Name == name { + return &m, nil + } + } + return nil, fmt.Errorf("not found") +} + +func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) { + hostinfo := tailcfg.Hostinfo{} + if len(m.HostInfo.RawMessage) != 0 { + hi, err := m.HostInfo.MarshalJSON() + if err != nil { + return nil, err + } + err = json.Unmarshal(hi, &hostinfo) + if err != nil { + return nil, err + } + } + return &hostinfo, nil +}