mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-29 00:19:58 +00:00
TUN-3669: Teamnet commands to add/show Teamnet routes.
This commit is contained in:
88
teamnet/api.go
Normal file
88
teamnet/api.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package teamnet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Route is a mapping from customer's IP space to a tunnel.
|
||||
// Each route allows the customer to route eyeballs in their corporate network
|
||||
// to certain private IP ranges. Each Route represents an IP range in their
|
||||
// network, and says that eyeballs can reach that route using the corresponding
|
||||
// tunnel.
|
||||
type Route struct {
|
||||
Network net.IPNet
|
||||
TunnelID uuid.UUID
|
||||
Comment string
|
||||
CreatedAt time.Time
|
||||
DeletedAt time.Time
|
||||
}
|
||||
|
||||
// TableString outputs a table row summarizing the route, to be used
|
||||
// when showing the user their routing table.
|
||||
func (r Route) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !r.DeletedAt.IsZero() {
|
||||
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t",
|
||||
r.Network.String(),
|
||||
r.Comment,
|
||||
r.TunnelID,
|
||||
r.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
// UnmarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||
func (r *Route) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// This is the raw JSON format that cloudflared receives from tunnelstore.
|
||||
// Note it does not understand types like IPNet.
|
||||
var resp struct {
|
||||
Network string `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string `json:"comment"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the raw JSON into a properly-typed response.
|
||||
_, network, err := net.ParseCIDR(resp.Network)
|
||||
if err != nil || network == nil {
|
||||
return fmt.Errorf("backend returned invalid network %s", resp.Network)
|
||||
}
|
||||
r.Network = *network
|
||||
r.TunnelID = resp.TunnelID
|
||||
r.Comment = resp.Comment
|
||||
r.CreatedAt = resp.CreatedAt
|
||||
r.DeletedAt = resp.DeletedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewRoute has all the parameters necessary to add a new route to the table.
|
||||
type NewRoute struct {
|
||||
Network net.IPNet
|
||||
TunnelID uuid.UUID
|
||||
Comment string
|
||||
}
|
||||
|
||||
// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
|
||||
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
Network string `json:"network"`
|
||||
TunnelID uuid.UUID `json:"tunnel_id"`
|
||||
Comment string `json:"comment"`
|
||||
}{
|
||||
TunnelID: r.TunnelID,
|
||||
Comment: r.Comment,
|
||||
})
|
||||
}
|
67
teamnet/api_test.go
Normal file
67
teamnet/api_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package teamnet
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUnmarshalRoute(t *testing.T) {
|
||||
// Response from the teamnet route backend
|
||||
data := `{
|
||||
"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
|
||||
}`
|
||||
var r Route
|
||||
err := r.UnmarshalJSON([]byte(data))
|
||||
|
||||
// Check everything worked
|
||||
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, r.Network)
|
||||
require.Equal(t, "test", r.Comment)
|
||||
}
|
||||
|
||||
func TestMarshalNewRoute(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
newRoute := NewRoute{
|
||||
Network: *network,
|
||||
TunnelID: uuid.New(),
|
||||
Comment: "hi",
|
||||
}
|
||||
|
||||
// Test where receiver is struct
|
||||
serialized, err := json.Marshal(newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
|
||||
// Test where receiver is pointer to struct
|
||||
serialized, err = json.Marshal(&newRoute)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.Contains(string(serialized), "tunnel_id"))
|
||||
}
|
||||
|
||||
func TestRouteTableString(t *testing.T) {
|
||||
_, network, err := net.ParseCIDR("1.2.3.4/32")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, network)
|
||||
r := Route{
|
||||
Network: *network,
|
||||
}
|
||||
row := r.TableString()
|
||||
fmt.Println(row)
|
||||
require.True(t, strings.HasPrefix(row, "1.2.3.4/32"))
|
||||
}
|
138
teamnet/filter.go
Normal file
138
teamnet/filter.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package teamnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
filterDeleted = cli.BoolFlag{
|
||||
Name: "filter-is-deleted",
|
||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||
}
|
||||
filterTunnelID = cli.StringFlag{
|
||||
Name: "filter-tunnel-id",
|
||||
Usage: "Show only routes with the given tunnel ID.",
|
||||
}
|
||||
filterSubset = cli.StringFlag{
|
||||
Name: "filter-network-is-subset-of",
|
||||
Aliases: []string{"nsub"},
|
||||
Usage: "Show only routes whose network is a subset of the given network.",
|
||||
}
|
||||
filterSuperset = cli.StringFlag{
|
||||
Name: "filter-network-is-superset-of",
|
||||
Aliases: []string{"nsup"},
|
||||
Usage: "Show only routes whose network is a superset of the given network.",
|
||||
}
|
||||
filterComment = cli.StringFlag{
|
||||
Name: "filter-comment-is",
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
// Flags contains all filter flags.
|
||||
Flags = []cli.Flag{
|
||||
&filterDeleted,
|
||||
&filterTunnelID,
|
||||
&filterSubset,
|
||||
&filterSuperset,
|
||||
&filterComment,
|
||||
}
|
||||
)
|
||||
|
||||
// Filter which routes get queried.
|
||||
type Filter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||
f := &Filter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
||||
u, err := uuid.Parse(tunnelID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Couldn't parse UUID from --filter-tunnel-id")
|
||||
}
|
||||
f.tunnelID(u)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Parses a CIDR from the flag. If the flag was unset, returns (nil, nil).
|
||||
func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
if !c.IsSet(flag.Name) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
_, subset, err := net.ParseCIDR(c.String(flag.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if subset == nil {
|
||||
return nil, fmt.Errorf("Invalid CIDR supplied for %s", flag.Name)
|
||||
}
|
||||
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *Filter) commentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *Filter) notDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *Filter) deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) existedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *Filter) tunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f Filter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
Reference in New Issue
Block a user