diff --git a/cmd/cloudflared/tunnel/subcommand_context_teamnet.go b/cmd/cloudflared/tunnel/subcommand_context_teamnet.go index a0786551..99264d0e 100644 --- a/cmd/cloudflared/tunnel/subcommand_context_teamnet.go +++ b/cmd/cloudflared/tunnel/subcommand_context_teamnet.go @@ -1,13 +1,18 @@ package tunnel import ( + "net" + "github.com/cloudflare/cloudflared/teamnet" + "github.com/pkg/errors" ) +const noClientMsg = "error while creating backend client" + func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error) { client, err := sc.client() if err != nil { - return nil, err + return nil, errors.Wrap(err, noClientMsg) } return client.ListRoutes(filter) } @@ -15,7 +20,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Rout func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) { client, err := sc.client() if err != nil { - return teamnet.Route{}, err + return teamnet.Route{}, errors.Wrap(err, noClientMsg) } return client.AddRoute(newRoute) } + +func (sc *subcommandContext) deleteRoute(network net.IPNet) error { + client, err := sc.client() + if err != nil { + return errors.Wrap(err, noClientMsg) + } + return client.DeleteRoute(network) +} diff --git a/cmd/cloudflared/tunnel/teamnet_subcommands.go b/cmd/cloudflared/tunnel/teamnet_subcommands.go index 4bbc5487..a4c724f0 100644 --- a/cmd/cloudflared/tunnel/teamnet_subcommands.go +++ b/cmd/cloudflared/tunnel/teamnet_subcommands.go @@ -40,7 +40,14 @@ func buildRouteIPSubcommand() *cli.Command { UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]", Description: `Shows all Cloudflare for Teams private routes. Using flags to specify filters means that only routes which match that filter get shown.`, - Flags: teamnet.Flags, + Flags: teamnet.FilterFlags, + }, + { + Name: "delete", + Action: cliutil.ErrorHandler(deleteRouteCommand), + Usage: "Delete a row of the routing table", + UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]", + Description: `Deletes the Cloudflare for Teams private route for a given CIDR`, }, }, } @@ -80,7 +87,7 @@ func addRouteCommand(c *cli.Context) error { return err } if c.NArg() < 2 { - return fmt.Errorf("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with") + return errors.New("You must supply at least 2 arguments, first the network you wish to route (in CIDR form e.g. 1.2.3.4/32) and then the tunnel ID to proxy with") } args := c.Args() _, network, err := net.ParseCIDR(args.Get(0)) @@ -111,6 +118,28 @@ func addRouteCommand(c *cli.Context) error { return nil } +func deleteRouteCommand(c *cli.Context) error { + sc, err := newSubcommandContext(c) + if err != nil { + return err + } + if c.NArg() != 1 { + return errors.New("You must supply exactly one argument, the network whose route you want to delete (in CIDR form e.g. 1.2.3.4/32)") + } + _, network, err := net.ParseCIDR(c.Args().First()) + if err != nil { + return errors.Wrap(err, "Invalid network CIDR") + } + if network == nil { + return errors.New("Invalid network CIDR") + } + if err := sc.deleteRoute(*network); err != nil { + return errors.Wrap(err, "API error") + } + fmt.Printf("Successfully deleted route for %s\n", network) + return nil +} + func formatAndPrintRouteList(routes []*teamnet.Route) { const ( minWidth = 0 diff --git a/teamnet/filter.go b/teamnet/filter.go index fda8d3ca..6cd8946b 100644 --- a/teamnet/filter.go +++ b/teamnet/filter.go @@ -35,7 +35,7 @@ var ( Usage: "Show only routes with this comment.", } // Flags contains all filter flags. - Flags = []cli.Flag{ + FilterFlags = []cli.Flag{ &filterDeleted, &filterTunnelID, &filterSubset, diff --git a/tunnelstore/client.go b/tunnelstore/client.go index b0bb365b..591ebdcb 100644 --- a/tunnelstore/client.go +++ b/tunnelstore/client.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/url" "path" @@ -197,6 +198,7 @@ type Client interface { // Teamnet endpoints ListRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) + DeleteRoute(network net.IPNet) error } type RESTClient struct { diff --git a/tunnelstore/client_teamnet.go b/tunnelstore/client_teamnet.go index decf3aa3..30031e43 100644 --- a/tunnelstore/client_teamnet.go +++ b/tunnelstore/client_teamnet.go @@ -2,6 +2,7 @@ package tunnelstore import ( "io" + "net" "net/http" "net/url" "path" @@ -10,6 +11,7 @@ import ( "github.com/pkg/errors" ) +// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account. func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error) { endpoint := r.baseEndpoints.accountRoutes endpoint.RawQuery = filter.Encode() @@ -26,6 +28,7 @@ func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.Route, error return nil, r.statusCodeToError("list routes", resp) } +// AddRoute calls the Tunnelstore POST endpoint for a given route. func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) { endpoint := r.baseEndpoints.accountRoutes endpoint.Path = path.Join(endpoint.Path, url.PathEscape(newRoute.Network.String())) @@ -42,6 +45,24 @@ func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) return teamnet.Route{}, r.statusCodeToError("add route", resp) } +// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route. +func (r *RESTClient) DeleteRoute(network net.IPNet) error { + endpoint := r.baseEndpoints.accountRoutes + endpoint.Path = path.Join(endpoint.Path, url.PathEscape(network.String())) + resp, err := r.sendRequest("DELETE", endpoint, nil) + if err != nil { + return errors.Wrap(err, "REST request failed") + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + _, err := parseRoute(resp.Body) + return err + } + + return r.statusCodeToError("delete route", resp) +} + func parseListRoutes(body io.ReadCloser) ([]*teamnet.Route, error) { var routes []*teamnet.Route err := parseResponse(body, &routes)