mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-11 21:36:38 +00:00

This commits makes sure that cloudflared starts using the new API endpoints for managing routes. Additionally, the delete route operation still allows deleting by CIDR and VNet but it is being marked as deprecated in favor of specifying the route ID. The goal of this change is to make it simpler for the user to delete routes without specifying Vnet.
238 lines
6.6 KiB
Go
238 lines
6.6 KiB
Go
package cfapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// 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 CIDR `json:"network"`
|
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
|
// Optional field. When unset, it means the Route belongs to the default virtual network.
|
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
|
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.
|
|
type CIDR net.IPNet
|
|
|
|
func (c CIDR) String() string {
|
|
n := net.IPNet(c)
|
|
return n.String()
|
|
}
|
|
|
|
func (c CIDR) MarshalJSON() ([]byte, error) {
|
|
str := c.String()
|
|
json, err := json.Marshal(str)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error serializing CIDR into JSON")
|
|
}
|
|
return json, nil
|
|
}
|
|
|
|
// UnmarshalJSON parses a JSON string into net.IPNet
|
|
func (c *CIDR) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return errors.Wrap(err, "error parsing cidr string")
|
|
}
|
|
_, network, err := net.ParseCIDR(s)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error parsing invalid network from backend")
|
|
}
|
|
if network == nil {
|
|
return fmt.Errorf("backend returned invalid network %s", s)
|
|
}
|
|
*c = CIDR(*network)
|
|
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
|
|
// 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).
|
|
func (r NewRoute) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(&struct {
|
|
Network string `json:"network"`
|
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
|
Comment string `json:"comment"`
|
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
|
}{
|
|
Network: r.Network.String(),
|
|
TunnelID: r.TunnelID,
|
|
Comment: r.Comment,
|
|
VNetID: r.VNetID,
|
|
})
|
|
}
|
|
|
|
// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
|
|
type DetailedRoute struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Network CIDR `json:"network"`
|
|
TunnelID uuid.UUID `json:"tunnel_id"`
|
|
// Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
|
|
VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
|
|
Comment string `json:"comment"`
|
|
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.
|
|
func (r *DetailedRoute) IsZero() bool {
|
|
return r.TunnelID == uuid.Nil
|
|
}
|
|
|
|
// TableString outputs a table row summarizing the route, to be used
|
|
// when showing the user their routing table.
|
|
func (r DetailedRoute) TableString() string {
|
|
deletedColumn := "-"
|
|
if !r.DeletedAt.IsZero() {
|
|
deletedColumn = r.DeletedAt.Format(time.RFC3339)
|
|
}
|
|
vnetColumn := "default"
|
|
if r.VNetID != nil {
|
|
vnetColumn = r.VNetID.String()
|
|
}
|
|
|
|
return fmt.Sprintf(
|
|
"%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
|
|
r.ID,
|
|
r.Network.String(),
|
|
vnetColumn,
|
|
r.Comment,
|
|
r.TunnelID,
|
|
r.TunnelName,
|
|
r.CreatedAt.Format(time.RFC3339),
|
|
deletedColumn,
|
|
)
|
|
}
|
|
|
|
type GetRouteByIpParams struct {
|
|
Ip net.IP
|
|
// Optional field. If unset, backend will assume the default vnet for the account.
|
|
VNetID *uuid.UUID
|
|
}
|
|
|
|
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
|
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
|
endpoint := r.baseEndpoints.accountRoutes
|
|
endpoint.RawQuery = filter.Encode()
|
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
return parseListDetailedRoutes(resp.Body)
|
|
}
|
|
|
|
return nil, r.statusCodeToError("list routes", resp)
|
|
}
|
|
|
|
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
|
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
|
endpoint := r.baseEndpoints.accountRoutes
|
|
endpoint.Path = path.Join(endpoint.Path)
|
|
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
|
if err != nil {
|
|
return Route{}, errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
return parseRoute(resp.Body)
|
|
}
|
|
|
|
return Route{}, r.statusCodeToError("add route", resp)
|
|
}
|
|
|
|
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
|
func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
|
|
endpoint := r.baseEndpoints.accountRoutes
|
|
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.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)
|
|
}
|
|
|
|
// GetByIP checks which route will proxy a given IP.
|
|
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
|
endpoint := r.baseEndpoints.accountRoutes
|
|
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
|
setVnetParam(&endpoint, params.VNetID)
|
|
|
|
resp, err := r.sendRequest("GET", endpoint, nil)
|
|
if err != nil {
|
|
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
return parseDetailedRoute(resp.Body)
|
|
}
|
|
|
|
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
|
}
|
|
|
|
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
|
var routes []*DetailedRoute
|
|
err := parseResponse(body, &routes)
|
|
return routes, err
|
|
}
|
|
|
|
func parseRoute(body io.ReadCloser) (Route, error) {
|
|
var route Route
|
|
err := parseResponse(body, &route)
|
|
return route, err
|
|
}
|
|
|
|
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
|
var route DetailedRoute
|
|
err := parseResponse(body, &route)
|
|
return route, err
|
|
}
|
|
|
|
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute 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()
|
|
}
|