TUN-5362: Adjust route ip commands to be aware of virtual networks
This commit is contained in:
parent
eec6b87eea
commit
571380b3f5
|
@ -1,8 +1,6 @@
|
||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
@ -26,18 +24,18 @@ func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route,
|
||||||
return client.AddRoute(newRoute)
|
return client.AddRoute(newRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) deleteRoute(network net.IPNet) error {
|
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, noClientMsg)
|
return errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.DeleteRoute(network)
|
return client.DeleteRoute(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *subcommandContext) getRouteByIP(ip net.IP) (teamnet.DetailedRoute, error) {
|
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
||||||
client, err := sc.client()
|
client, err := sc.client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||||
}
|
}
|
||||||
return client.GetByIP(ip)
|
return client.GetByIP(params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||||
|
@ -15,26 +16,45 @@ import (
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
vnetFlag = &cli.StringFlag{
|
||||||
|
Name: "virtual-network",
|
||||||
|
Aliases: []string{"vn"},
|
||||||
|
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func buildRouteIPSubcommand() *cli.Command {
|
func buildRouteIPSubcommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "ip",
|
Name: "ip",
|
||||||
Usage: "Configure and query Cloudflare WARP routing to services or private networks available through this tunnel.",
|
Usage: "Configure and query Cloudflare WARP routing to private IP networks made available through Cloudflare Tunnels.",
|
||||||
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
||||||
Description: `cloudflared can provision private routes from any IP space to origins in your corporate network.
|
Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in
|
||||||
Users enrolled in your Cloudflare for Teams organization can reach those routes through the
|
your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP
|
||||||
Cloudflare WARP client. You can also build rules to determine who can reach certain routes.`,
|
client. You can then configure L7/L4 filtering on https://dash.teams.cloudflare.com to
|
||||||
|
determine who can reach certain routes.
|
||||||
|
By default IP routes all exist within a single virtual network. If you use the same IP
|
||||||
|
space(s) in different physical private networks, all meant to be reachable via IP routes,
|
||||||
|
then you have to manage the ambiguous IP routes by associating them to virtual networks.
|
||||||
|
See "cloudflared tunnel network --help" for more information.`,
|
||||||
Subcommands: []*cli.Command{
|
Subcommands: []*cli.Command{
|
||||||
{
|
{
|
||||||
Name: "add",
|
Name: "add",
|
||||||
Action: cliutil.ConfiguredAction(addRouteCommand),
|
Action: cliutil.ConfiguredAction(addRouteCommand),
|
||||||
Usage: "Add any new network to the routing table reachable via the tunnel",
|
Usage: "Add a new network to the routing table reachable via a Tunnel",
|
||||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [CIDR] [TUNNEL] [COMMENT?]",
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [flags] [CIDR] [TUNNEL] [COMMENT?]",
|
||||||
Description: `Adds any network route space (represented as a CIDR) to your routing table.
|
Description: `Adds a network IP route space (represented as a CIDR) to your routing table.
|
||||||
That network space becomes reachable for requests egressing from a user's machine
|
That network IP space becomes reachable for requests egressing from a user's machine
|
||||||
as long as it is using Cloudflare WARP client and is enrolled in the same account
|
as long as it is using Cloudflare WARP client and is enrolled in the same account
|
||||||
that is running the tunnel chosen here. Further, those requests will be proxied to
|
that is running the Tunnel chosen here. Further, those requests will be proxied to
|
||||||
the specified tunnel, and reach an IP in the given CIDR, as long as that IP is
|
the specified Tunnel, and reach an IP in the given CIDR, as long as that IP is
|
||||||
reachable from the tunnel.`,
|
reachable from cloudflared.
|
||||||
|
If the CIDR exists in more than one private network, to be connected with Cloudflare
|
||||||
|
Tunnels, then you have to manage those IP routes with virtual networks (see
|
||||||
|
"cloudflared tunnel network --help)". In those cases, you then have to tell
|
||||||
|
which virtual network's routing table you want to add the route to with:
|
||||||
|
"cloudflared tunnel route ip add --virtual-network [ID/name] [CIDR] [TUNNEL]".`,
|
||||||
|
Flags: []cli.Flag{vnetFlag},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "show",
|
Name: "show",
|
||||||
|
@ -49,17 +69,22 @@ reachable from the tunnel.`,
|
||||||
Name: "delete",
|
Name: "delete",
|
||||||
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
||||||
Usage: "Delete a row from your organization's private routing table",
|
Usage: "Delete a row from your organization's private routing table",
|
||||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [CIDR]",
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
|
||||||
Description: `Deletes the row for a given CIDR from your routing table. That portion
|
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
|
||||||
of your network will no longer be reachable by the WARP clients.`,
|
will no longer be reachable by the WARP clients. Note that if you use virtual
|
||||||
|
networks, then you have to tell which virtual network whose routing table you
|
||||||
|
have a row deleted from.`,
|
||||||
|
Flags: []cli.Flag{vnetFlag},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "get",
|
Name: "get",
|
||||||
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
|
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
|
||||||
Usage: "Check which row of the routing table matches a given IP.",
|
Usage: "Check which row of the routing table matches a given IP.",
|
||||||
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [IP]",
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip get [flags] [IP]",
|
||||||
Description: `Checks which row of the routing table will be used to proxy a given IP.
|
Description: `Checks which row of the routing table will be used to proxy a given IP. This helps check
|
||||||
This helps check and validate your config.`,
|
and validate your config. Note that if you use virtual networks, then you have
|
||||||
|
to tell which virtual network whose routing table you want to use.`,
|
||||||
|
Flags: []cli.Flag{vnetFlag},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -112,7 +137,9 @@ func addRouteCommand(c *cli.Context) error {
|
||||||
if c.NArg() < 2 {
|
if c.NArg() < 2 {
|
||||||
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")
|
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()
|
args := c.Args()
|
||||||
|
|
||||||
_, network, err := net.ParseCIDR(args.Get(0))
|
_, network, err := net.ParseCIDR(args.Get(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Invalid network CIDR")
|
return errors.Wrap(err, "Invalid network CIDR")
|
||||||
|
@ -120,19 +147,32 @@ func addRouteCommand(c *cli.Context) error {
|
||||||
if network == nil {
|
if network == nil {
|
||||||
return errors.New("Invalid network CIDR")
|
return errors.New("Invalid network CIDR")
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnelRef := args.Get(1)
|
tunnelRef := args.Get(1)
|
||||||
tunnelID, err := sc.findID(tunnelRef)
|
tunnelID, err := sc.findID(tunnelRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Invalid tunnel")
|
return errors.Wrap(err, "Invalid tunnel")
|
||||||
}
|
}
|
||||||
|
|
||||||
comment := ""
|
comment := ""
|
||||||
if c.NArg() >= 3 {
|
if c.NArg() >= 3 {
|
||||||
comment = args.Get(2)
|
comment = args.Get(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var vnetId *uuid.UUID
|
||||||
|
if c.IsSet(vnetFlag.Name) {
|
||||||
|
id, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
vnetId = &id
|
||||||
|
}
|
||||||
|
|
||||||
_, err = sc.addRoute(teamnet.NewRoute{
|
_, err = sc.addRoute(teamnet.NewRoute{
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
Network: *network,
|
Network: *network,
|
||||||
TunnelID: tunnelID,
|
TunnelID: tunnelID,
|
||||||
|
VNetID: vnetId,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "API error")
|
return errors.Wrap(err, "API error")
|
||||||
|
@ -146,9 +186,11 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.NArg() != 1 {
|
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)")
|
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())
|
_, network, err := net.ParseCIDR(c.Args().First())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Invalid network CIDR")
|
return errors.Wrap(err, "Invalid network CIDR")
|
||||||
|
@ -156,7 +198,20 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||||
if network == nil {
|
if network == nil {
|
||||||
return errors.New("Invalid network CIDR")
|
return errors.New("Invalid network CIDR")
|
||||||
}
|
}
|
||||||
if err := sc.deleteRoute(*network); err != nil {
|
|
||||||
|
params := teamnet.DeleteRouteParams{
|
||||||
|
Network: *network,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet(vnetFlag.Name) {
|
||||||
|
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params.VNetID = &vnetId
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.deleteRoute(params); err != nil {
|
||||||
return errors.Wrap(err, "API error")
|
return errors.Wrap(err, "API error")
|
||||||
}
|
}
|
||||||
fmt.Printf("Successfully deleted route for %s\n", network)
|
fmt.Printf("Successfully deleted route for %s\n", network)
|
||||||
|
@ -177,7 +232,20 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||||
}
|
}
|
||||||
route, err := sc.getRouteByIP(ip)
|
|
||||||
|
params := teamnet.GetRouteByIpParams{
|
||||||
|
Ip: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IsSet(vnetFlag.Name) {
|
||||||
|
vnetId, err := getVnetId(sc, c.String(vnetFlag.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params.VNetID = &vnetId
|
||||||
|
}
|
||||||
|
|
||||||
|
route, err := sc.getRouteByIP(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "API error")
|
return errors.Wrap(err, "API error")
|
||||||
}
|
}
|
||||||
|
@ -202,7 +270,7 @@ func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
||||||
defer writer.Flush()
|
defer writer.Flush()
|
||||||
|
|
||||||
// Print column headers with tabbed columns
|
// Print column headers with tabbed columns
|
||||||
_, _ = fmt.Fprintln(writer, "NETWORK\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
_, _ = fmt.Fprintln(writer, "NETWORK\tVIRTUAL NET ID\tCOMMENT\tTUNNEL ID\tTUNNEL NAME\tCREATED\tDELETED\t")
|
||||||
|
|
||||||
// Loop through routes, create formatted string for each, and print using tabwriter
|
// Loop through routes, create formatted string for each, and print using tabwriter
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
|
|
|
@ -16,11 +16,13 @@ import (
|
||||||
// network, and says that eyeballs can reach that route using the corresponding
|
// network, and says that eyeballs can reach that route using the corresponding
|
||||||
// tunnel.
|
// tunnel.
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Network CIDR `json:"network"`
|
Network CIDR `json:"network"`
|
||||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
Comment string `json:"comment"`
|
// Optional field. When unset, it means the Route belongs to the default virtual network.
|
||||||
CreatedAt time.Time `json:"created_at"`
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||||
DeletedAt time.Time `json:"deleted_at"`
|
Comment string `json:"comment"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt time.Time `json:"deleted_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
|
// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
|
||||||
|
@ -62,27 +64,33 @@ type NewRoute struct {
|
||||||
Network net.IPNet
|
Network net.IPNet
|
||||||
TunnelID uuid.UUID
|
TunnelID uuid.UUID
|
||||||
Comment string
|
Comment string
|
||||||
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||||
|
VNetID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||||
}{
|
}{
|
||||||
TunnelID: r.TunnelID,
|
TunnelID: r.TunnelID,
|
||||||
Comment: r.Comment,
|
Comment: r.Comment,
|
||||||
|
VNetID: r.VNetID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
||||||
type DetailedRoute struct {
|
type DetailedRoute struct {
|
||||||
Network CIDR `json:"network"`
|
Network CIDR `json:"network"`
|
||||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||||
Comment string `json:"comment"`
|
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
||||||
CreatedAt time.Time `json:"created_at"`
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
||||||
DeletedAt time.Time `json:"deleted_at"`
|
Comment string `json:"comment"`
|
||||||
TunnelName string `json:"tunnel_name"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt time.Time `json:"deleted_at"`
|
||||||
|
TunnelName string `json:"tunnel_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsZero checks if DetailedRoute is the zero value.
|
// IsZero checks if DetailedRoute is the zero value.
|
||||||
|
@ -97,9 +105,15 @@ func (r DetailedRoute) TableString() string {
|
||||||
if !r.DeletedAt.IsZero() {
|
if !r.DeletedAt.IsZero() {
|
||||||
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
||||||
}
|
}
|
||||||
|
vnetColumn := "default"
|
||||||
|
if r.VNetID != nil {
|
||||||
|
vnetColumn = r.VNetID.String()
|
||||||
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||||
r.Network.String(),
|
r.Network.String(),
|
||||||
|
vnetColumn,
|
||||||
r.Comment,
|
r.Comment,
|
||||||
r.TunnelID,
|
r.TunnelID,
|
||||||
r.TunnelName,
|
r.TunnelName,
|
||||||
|
@ -107,3 +121,15 @@ func (r DetailedRoute) TableString() string {
|
||||||
deletedColumn,
|
deletedColumn,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DeleteRouteParams struct {
|
||||||
|
Network net.IPNet
|
||||||
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||||
|
VNetID *uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetRouteByIpParams struct {
|
||||||
|
Ip net.IP
|
||||||
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||||
|
VNetID *uuid.UUID
|
||||||
|
}
|
||||||
|
|
|
@ -12,77 +12,154 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnmarshalRoute(t *testing.T) {
|
func TestUnmarshalRoute(t *testing.T) {
|
||||||
// Response from the teamnet route backend
|
testCases := []struct {
|
||||||
data := `{
|
Json string
|
||||||
"network":"10.1.2.40/29",
|
HasVnet bool
|
||||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
}{
|
||||||
"comment":"test",
|
{
|
||||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
`{
|
||||||
"deleted_at":null
|
"network":"10.1.2.40/29",
|
||||||
}`
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
var r Route
|
"comment":"test",
|
||||||
err := json.Unmarshal([]byte(data), &r)
|
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||||
|
"deleted_at":null
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"network":"10.1.2.40/29",
|
||||||
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
|
"comment":"test",
|
||||||
|
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||||
|
"deleted_at":null,
|
||||||
|
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9"
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Check everything worked
|
for _, testCase := range testCases {
|
||||||
require.NoError(t, err)
|
data := testCase.Json
|
||||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
|
||||||
require.Equal(t, "test", r.Comment)
|
var r Route
|
||||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
err := json.Unmarshal([]byte(data), &r)
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, CIDR(*cidr), r.Network)
|
// Check everything worked
|
||||||
require.Equal(t, "test", r.Comment)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, CIDR(*cidr), r.Network)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
|
||||||
|
if testCase.HasVnet {
|
||||||
|
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, r.VNetID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
func TestDetailedRouteJsonRoundtrip(t *testing.T) {
|
||||||
// Response from the teamnet route backend
|
testCases := []struct {
|
||||||
data := `{
|
Json string
|
||||||
"network":"10.1.2.40/29",
|
HasVnet bool
|
||||||
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
}{
|
||||||
"comment":"test",
|
{
|
||||||
"created_at":"2020-12-22T02:00:15.587008Z",
|
`{
|
||||||
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
"network":"10.1.2.40/29",
|
||||||
"tunnel_name":"Mr. Tun"
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
}`
|
"comment":"test",
|
||||||
var r DetailedRoute
|
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||||
err := json.Unmarshal([]byte(data), &r)
|
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||||
|
"tunnel_name":"Mr. Tun"
|
||||||
|
}`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`{
|
||||||
|
"network":"10.1.2.40/29",
|
||||||
|
"tunnel_id":"fba6ffea-807f-4e7a-a740-4184ee1b82c8",
|
||||||
|
"virtual_network_id":"38c95083-8191-4110-8339-3f438d44fdb9",
|
||||||
|
"comment":"test",
|
||||||
|
"created_at":"2020-12-22T02:00:15.587008Z",
|
||||||
|
"deleted_at":"2021-01-14T05:01:42.183002Z",
|
||||||
|
"tunnel_name":"Mr. Tun"
|
||||||
|
}`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Check everything worked
|
for _, testCase := range testCases {
|
||||||
require.NoError(t, err)
|
data := testCase.Json
|
||||||
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
|
||||||
require.Equal(t, "test", r.Comment)
|
|
||||||
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, CIDR(*cidr), r.Network)
|
|
||||||
require.Equal(t, "test", r.Comment)
|
|
||||||
require.Equal(t, "Mr. Tun", r.TunnelName)
|
|
||||||
|
|
||||||
bytes, err := json.Marshal(r)
|
var r DetailedRoute
|
||||||
require.NoError(t, err)
|
err := json.Unmarshal([]byte(data), &r)
|
||||||
obtainedJson := string(bytes)
|
|
||||||
data = strings.Replace(data, "\t", "", -1)
|
// Check everything worked
|
||||||
data = strings.Replace(data, "\n", "", -1)
|
require.NoError(t, err)
|
||||||
require.Equal(t, data, obtainedJson)
|
require.Equal(t, uuid.MustParse("fba6ffea-807f-4e7a-a740-4184ee1b82c8"), r.TunnelID)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
_, cidr, err := net.ParseCIDR("10.1.2.40/29")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, CIDR(*cidr), r.Network)
|
||||||
|
require.Equal(t, "test", r.Comment)
|
||||||
|
require.Equal(t, "Mr. Tun", r.TunnelName)
|
||||||
|
|
||||||
|
if testCase.HasVnet {
|
||||||
|
require.Equal(t, uuid.MustParse("38c95083-8191-4110-8339-3f438d44fdb9"), *r.VNetID)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, r.VNetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
obtainedJson := string(bytes)
|
||||||
|
data = strings.Replace(data, "\t", "", -1)
|
||||||
|
data = strings.Replace(data, "\n", "", -1)
|
||||||
|
require.Equal(t, data, obtainedJson)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalNewRoute(t *testing.T) {
|
func TestMarshalNewRoute(t *testing.T) {
|
||||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, network)
|
require.NotNil(t, network)
|
||||||
newRoute := NewRoute{
|
vnetId := uuid.New()
|
||||||
Network: *network,
|
|
||||||
TunnelID: uuid.New(),
|
newRoutes := []NewRoute{
|
||||||
Comment: "hi",
|
{
|
||||||
|
Network: *network,
|
||||||
|
TunnelID: uuid.New(),
|
||||||
|
Comment: "hi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Network: *network,
|
||||||
|
TunnelID: uuid.New(),
|
||||||
|
Comment: "hi",
|
||||||
|
VNetID: &vnetId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test where receiver is struct
|
for _, newRoute := range newRoutes {
|
||||||
serialized, err := json.Marshal(newRoute)
|
// Test where receiver is struct
|
||||||
require.NoError(t, err)
|
serialized, err := json.Marshal(newRoute)
|
||||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
require.NoError(t, err)
|
||||||
|
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||||
|
|
||||||
// Test where receiver is pointer to struct
|
// Test where receiver is pointer to struct
|
||||||
serialized, err = json.Marshal(&newRoute)
|
serialized, err = json.Marshal(&newRoute)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||||
|
|
||||||
|
if newRoute.VNetID == nil {
|
||||||
|
require.False(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||||
|
} else {
|
||||||
|
require.True(t, strings.Contains(string(serialized), "virtual_network_id"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouteTableString(t *testing.T) {
|
func TestRouteTableString(t *testing.T) {
|
||||||
|
|
|
@ -35,6 +35,11 @@ var (
|
||||||
Name: "filter-comment-is",
|
Name: "filter-comment-is",
|
||||||
Usage: "Show only routes with this comment.",
|
Usage: "Show only routes with this comment.",
|
||||||
}
|
}
|
||||||
|
filterVnet = cli.StringFlag{
|
||||||
|
Name: "filter-virtual-network-id",
|
||||||
|
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||||
|
}
|
||||||
|
|
||||||
// Flags contains all filter flags.
|
// Flags contains all filter flags.
|
||||||
FilterFlags = []cli.Flag{
|
FilterFlags = []cli.Flag{
|
||||||
&filterDeleted,
|
&filterDeleted,
|
||||||
|
@ -42,6 +47,7 @@ var (
|
||||||
&filterSubset,
|
&filterSubset,
|
||||||
&filterSuperset,
|
&filterSuperset,
|
||||||
&filterComment,
|
&filterComment,
|
||||||
|
&filterVnet,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,11 +88,19 @@ func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||||
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
||||||
u, err := uuid.Parse(tunnelID)
|
u, err := uuid.Parse(tunnelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "Couldn't parse UUID from --filter-tunnel-id")
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
|
||||||
}
|
}
|
||||||
f.tunnelID(u)
|
f.tunnelID(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vnetId := c.String(filterVnet.Name); vnetId != "" {
|
||||||
|
u, err := uuid.Parse(vnetId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
|
||||||
|
}
|
||||||
|
f.vnetID(u)
|
||||||
|
}
|
||||||
|
|
||||||
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
if maxFetch := c.Int("max-fetch-size"); maxFetch > 0 {
|
||||||
f.MaxFetchSize(uint(maxFetch))
|
f.MaxFetchSize(uint(maxFetch))
|
||||||
}
|
}
|
||||||
|
@ -138,6 +152,10 @@ func (f *Filter) tunnelID(id uuid.UUID) {
|
||||||
f.queryParams.Set("tunnel_id", id.String())
|
f.queryParams.Set("tunnel_id", id.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Filter) vnetID(id uuid.UUID) {
|
||||||
|
f.queryParams.Set("virtual_network_id", id.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Filter) MaxFetchSize(max uint) {
|
func (f *Filter) MaxFetchSize(max uint) {
|
||||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||||
}
|
}
|
||||||
|
|
|
@ -236,8 +236,8 @@ type Client interface {
|
||||||
// Teamnet endpoints
|
// Teamnet endpoints
|
||||||
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
|
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
|
||||||
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
||||||
DeleteRoute(network net.IPNet) error
|
DeleteRoute(params teamnet.DeleteRouteParams) error
|
||||||
GetByIP(ip net.IP) (teamnet.DetailedRoute, error)
|
GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error)
|
||||||
|
|
||||||
// Virtual Networks endpoints
|
// Virtual Networks endpoints
|
||||||
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
|
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
|
||||||
|
|
|
@ -2,11 +2,11 @@ package tunnelstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/cloudflare/cloudflared/teamnet"
|
"github.com/cloudflare/cloudflared/teamnet"
|
||||||
|
@ -47,9 +47,11 @@ func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||||
func (r *RESTClient) DeleteRoute(network net.IPNet) error {
|
func (r *RESTClient) DeleteRoute(params teamnet.DeleteRouteParams) error {
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(network.String()))
|
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||||
|
setVnetParam(&endpoint, params.VNetID)
|
||||||
|
|
||||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "REST request failed")
|
return errors.Wrap(err, "REST request failed")
|
||||||
|
@ -65,9 +67,11 @@ func (r *RESTClient) DeleteRoute(network net.IPNet) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByIP checks which route will proxy a given IP.
|
// GetByIP checks which route will proxy a given IP.
|
||||||
func (r *RESTClient) GetByIP(ip net.IP) (teamnet.DetailedRoute, error) {
|
func (r *RESTClient) GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
||||||
endpoint := r.baseEndpoints.accountRoutes
|
endpoint := r.baseEndpoints.accountRoutes
|
||||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(ip.String()))
|
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||||
|
setVnetParam(&endpoint, params.VNetID)
|
||||||
|
|
||||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||||
|
@ -98,3 +102,13 @@ func parseDetailedRoute(body io.ReadCloser) (teamnet.DetailedRoute, error) {
|
||||||
err := parseResponse(body, &route)
|
err := parseResponse(body, &route)
|
||||||
return route, err
|
return route, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setVnetParam overwrites the URL's query parameters with a query param to scope the Route action to a certain
|
||||||
|
// virtual network (if one is provided).
|
||||||
|
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||||
|
queryParams := url.Values{}
|
||||||
|
if vnetID != nil {
|
||||||
|
queryParams.Set("virtual_network_id", vnetID.String())
|
||||||
|
}
|
||||||
|
endpoint.RawQuery = queryParams.Encode()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue