Something went wrong. Try again.
Cloudflare Tunnel client with NetBSD support
Something went wrong. Try again.
6.8 kB · 235 lines
Go
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236package 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.IPNetfunc (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.// Due to pagination on the server side it will call the endpoint multiple times if needed.func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) { fetchFn := func(page int) (*http.Response, error) { endpoint := r.baseEndpoints.accountRoutes filter.Page(page) endpoint.RawQuery = filter.Encode() rsp, err := r.sendRequest("GET", endpoint, nil)
if err != nil { return nil, errors.Wrap(err, "REST request failed") } if rsp.StatusCode != http.StatusOK { rsp.Body.Close() return nil, r.statusCodeToError("list routes", rsp) } return rsp, nil } return fetchExhaustively[DetailedRoute](fetchFn)}
// 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 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()}