TUN-3669: Teamnet commands to add/show Teamnet routes.

This commit is contained in:
Adam Chalmers
2020-12-21 20:06:46 -06:00
parent 2ea491b1d0
commit 94c639d225
8 changed files with 528 additions and 7 deletions

88
teamnet/api.go Normal file
View 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
View 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
View 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()
}