281 lines
8.4 KiB
Go
281 lines
8.4 KiB
Go
package tunnel
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"text/tabwriter"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
|
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
|
"github.com/cloudflare/cloudflared/teamnet"
|
|
|
|
"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 {
|
|
return &cli.Command{
|
|
Name: "ip",
|
|
Usage: "Configure and query Cloudflare WARP routing to private IP networks made available through Cloudflare Tunnels.",
|
|
UsageText: "cloudflared tunnel [--config FILEPATH] route COMMAND [arguments...]",
|
|
Description: `cloudflared can provision routes for any IP space in your corporate network. Users enrolled in
|
|
your Cloudflare for Teams organization can reach those IPs through the Cloudflare WARP
|
|
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{
|
|
{
|
|
Name: "add",
|
|
Action: cliutil.ConfiguredAction(addRouteCommand),
|
|
Usage: "Add a new network to the routing table reachable via a Tunnel",
|
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip add [flags] [CIDR] [TUNNEL] [COMMENT?]",
|
|
Description: `Adds a network IP route space (represented as a CIDR) to your routing table.
|
|
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
|
|
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
|
|
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",
|
|
Aliases: []string{"list"},
|
|
Action: cliutil.ConfiguredAction(showRoutesCommand),
|
|
Usage: "Show the routing table",
|
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip show [flags]",
|
|
Description: `Shows your organization private routing table. You can use flags to filter the results.`,
|
|
Flags: showRoutesFlags(),
|
|
},
|
|
{
|
|
Name: "delete",
|
|
Action: cliutil.ConfiguredAction(deleteRouteCommand),
|
|
Usage: "Delete a row from your organization's private routing table",
|
|
UsageText: "cloudflared tunnel [--config FILEPATH] route ip delete [flags] [CIDR]",
|
|
Description: `Deletes the row for a given CIDR from your routing table. That portion of your network
|
|
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",
|
|
Action: cliutil.ConfiguredAction(getRouteByIPCommand),
|
|
Usage: "Check which row of the routing table matches a given 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. This helps check
|
|
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},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func showRoutesFlags() []cli.Flag {
|
|
flags := make([]cli.Flag, 0)
|
|
flags = append(flags, teamnet.FilterFlags...)
|
|
flags = append(flags, outputFormatFlag)
|
|
return flags
|
|
}
|
|
|
|
func showRoutesCommand(c *cli.Context) error {
|
|
sc, err := newSubcommandContext(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filter, err := teamnet.NewFromCLI(c)
|
|
if err != nil {
|
|
return errors.Wrap(err, "invalid config for routing filters")
|
|
}
|
|
|
|
warningChecker := updater.StartWarningCheck(c)
|
|
defer warningChecker.LogWarningIfAny(sc.log)
|
|
|
|
routes, err := sc.listRoutes(filter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if outputFormat := c.String(outputFormatFlag.Name); outputFormat != "" {
|
|
return renderOutput(outputFormat, routes)
|
|
}
|
|
|
|
if len(routes) > 0 {
|
|
formatAndPrintRouteList(routes)
|
|
} else {
|
|
fmt.Println("No routes were found for the given filter flags. You can use 'cloudflared tunnel route ip add' to add a route.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func addRouteCommand(c *cli.Context) error {
|
|
sc, err := newSubcommandContext(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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")
|
|
}
|
|
|
|
args := c.Args()
|
|
|
|
_, network, err := net.ParseCIDR(args.Get(0))
|
|
if err != nil {
|
|
return errors.Wrap(err, "Invalid network CIDR")
|
|
}
|
|
if network == nil {
|
|
return errors.New("Invalid network CIDR")
|
|
}
|
|
|
|
tunnelRef := args.Get(1)
|
|
tunnelID, err := sc.findID(tunnelRef)
|
|
if err != nil {
|
|
return errors.Wrap(err, "Invalid tunnel")
|
|
}
|
|
|
|
comment := ""
|
|
if c.NArg() >= 3 {
|
|
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{
|
|
Comment: comment,
|
|
Network: *network,
|
|
TunnelID: tunnelID,
|
|
VNetID: vnetId,
|
|
})
|
|
if err != nil {
|
|
return errors.Wrap(err, "API error")
|
|
}
|
|
fmt.Printf("Successfully added route for %s over tunnel %s\n", network, tunnelID)
|
|
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")
|
|
}
|
|
|
|
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")
|
|
}
|
|
fmt.Printf("Successfully deleted route for %s\n", network)
|
|
return nil
|
|
}
|
|
|
|
func getRouteByIPCommand(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, an IP whose route will be queried (e.g. 1.2.3.4 or 2001:0db8:::7334)")
|
|
}
|
|
|
|
ipInput := c.Args().First()
|
|
ip := net.ParseIP(ipInput)
|
|
if ip == nil {
|
|
return fmt.Errorf("Invalid IP %s", ipInput)
|
|
}
|
|
|
|
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 {
|
|
return errors.Wrap(err, "API error")
|
|
}
|
|
if route.IsZero() {
|
|
fmt.Printf("No route matches the IP %s\n", ip)
|
|
} else {
|
|
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
|
const (
|
|
minWidth = 0
|
|
tabWidth = 8
|
|
padding = 1
|
|
padChar = ' '
|
|
flags = 0
|
|
)
|
|
|
|
writer := tabwriter.NewWriter(os.Stdout, minWidth, tabWidth, padding, padChar, flags)
|
|
defer writer.Flush()
|
|
|
|
// Print column headers with tabbed columns
|
|
_, _ = 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
|
|
for _, route := range routes {
|
|
formattedStr := route.TableString()
|
|
_, _ = fmt.Fprintln(writer, formattedStr)
|
|
}
|
|
}
|