cloudflare/cloudflared

Public

mirrored from https://github.com/cloudflare/cloudflaredAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2024.1.5

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

cfapi/ip_route.go

237lines · modecode

1package cfapi
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "net/url"
10 "path"
11 "time"
12
13 "github.com/google/uuid"
14 "github.com/pkg/errors"
15)
16
17// Route is a mapping from customer's IP space to a tunnel.
18// Each route allows the customer to route eyeballs in their corporate network
19// to certain private IP ranges. Each Route represents an IP range in their
20// network, and says that eyeballs can reach that route using the corresponding
21// tunnel.
22type Route struct {
23 Network CIDR `json:"network"`
24 TunnelID uuid.UUID `json:"tunnel_id"`
25 // Optional field. When unset, it means the Route belongs to the default virtual network.
26 VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
27 Comment string `json:"comment"`
28 CreatedAt time.Time `json:"created_at"`
29 DeletedAt time.Time `json:"deleted_at"`
30}
31
32// CIDR is just a newtype wrapper around net.IPNet. It adds JSON unmarshalling.
33type CIDR net.IPNet
34
35func (c CIDR) String() string {
36 n := net.IPNet(c)
37 return n.String()
38}
39
40func (c CIDR) MarshalJSON() ([]byte, error) {
41 str := c.String()
42 json, err := json.Marshal(str)
43 if err != nil {
44 return nil, errors.Wrap(err, "error serializing CIDR into JSON")
45 }
46 return json, nil
47}
48
49// UnmarshalJSON parses a JSON string into net.IPNet
50func (c *CIDR) UnmarshalJSON(data []byte) error {
51 var s string
52 if err := json.Unmarshal(data, &s); err != nil {
53 return errors.Wrap(err, "error parsing cidr string")
54 }
55 _, network, err := net.ParseCIDR(s)
56 if err != nil {
57 return errors.Wrap(err, "error parsing invalid network from backend")
58 }
59 if network == nil {
60 return fmt.Errorf("backend returned invalid network %s", s)
61 }
62 *c = CIDR(*network)
63 return nil
64}
65
66// NewRoute has all the parameters necessary to add a new route to the table.
67type NewRoute struct {
68 Network net.IPNet
69 TunnelID uuid.UUID
70 Comment string
71 // Optional field. If unset, backend will assume the default vnet for the account.
72 VNetID *uuid.UUID
73}
74
75// MarshalJSON handles fields with non-JSON types (e.g. net.IPNet).
76func (r NewRoute) MarshalJSON() ([]byte, error) {
77 return json.Marshal(&struct {
78 Network string `json:"network"`
79 TunnelID uuid.UUID `json:"tunnel_id"`
80 Comment string `json:"comment"`
81 VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
82 }{
83 Network: r.Network.String(),
84 TunnelID: r.TunnelID,
85 Comment: r.Comment,
86 VNetID: r.VNetID,
87 })
88}
89
90// DetailedRoute is just a Route with some extra fields, e.g. TunnelName.
91type DetailedRoute struct {
92 ID uuid.UUID `json:"id"`
93 Network CIDR `json:"network"`
94 TunnelID uuid.UUID `json:"tunnel_id"`
95 // Optional field. When unset, it means the DetailedRoute belongs to the default virtual network.
96 VNetID *uuid.UUID `json:"virtual_network_id,omitempty"`
97 Comment string `json:"comment"`
98 CreatedAt time.Time `json:"created_at"`
99 DeletedAt time.Time `json:"deleted_at"`
100 TunnelName string `json:"tunnel_name"`
101}
102
103// IsZero checks if DetailedRoute is the zero value.
104func (r *DetailedRoute) IsZero() bool {
105 return r.TunnelID == uuid.Nil
106}
107
108// TableString outputs a table row summarizing the route, to be used
109// when showing the user their routing table.
110func (r DetailedRoute) TableString() string {
111 deletedColumn := "-"
112 if !r.DeletedAt.IsZero() {
113 deletedColumn = r.DeletedAt.Format(time.RFC3339)
114 }
115 vnetColumn := "default"
116 if r.VNetID != nil {
117 vnetColumn = r.VNetID.String()
118 }
119
120 return fmt.Sprintf(
121 "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
122 r.ID,
123 r.Network.String(),
124 vnetColumn,
125 r.Comment,
126 r.TunnelID,
127 r.TunnelName,
128 r.CreatedAt.Format(time.RFC3339),
129 deletedColumn,
130 )
131}
132
133type GetRouteByIpParams struct {
134 Ip net.IP
135 // Optional field. If unset, backend will assume the default vnet for the account.
136 VNetID *uuid.UUID
137}
138
139// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
140func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
141 endpoint := r.baseEndpoints.accountRoutes
142 endpoint.RawQuery = filter.Encode()
143 resp, err := r.sendRequest("GET", endpoint, nil)
144 if err != nil {
145 return nil, errors.Wrap(err, "REST request failed")
146 }
147 defer resp.Body.Close()
148
149 if resp.StatusCode == http.StatusOK {
150 return parseListDetailedRoutes(resp.Body)
151 }
152
153 return nil, r.statusCodeToError("list routes", resp)
154}
155
156// AddRoute calls the Tunnelstore POST endpoint for a given route.
157func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
158 endpoint := r.baseEndpoints.accountRoutes
159 endpoint.Path = path.Join(endpoint.Path)
160 resp, err := r.sendRequest("POST", endpoint, newRoute)
161 if err != nil {
162 return Route{}, errors.Wrap(err, "REST request failed")
163 }
164 defer resp.Body.Close()
165
166 if resp.StatusCode == http.StatusOK {
167 return parseRoute(resp.Body)
168 }
169
170 return Route{}, r.statusCodeToError("add route", resp)
171}
172
173// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
174func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
175 endpoint := r.baseEndpoints.accountRoutes
176 endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
177
178 resp, err := r.sendRequest("DELETE", endpoint, nil)
179 if err != nil {
180 return errors.Wrap(err, "REST request failed")
181 }
182 defer resp.Body.Close()
183
184 if resp.StatusCode == http.StatusOK {
185 _, err := parseRoute(resp.Body)
186 return err
187 }
188
189 return r.statusCodeToError("delete route", resp)
190}
191
192// GetByIP checks which route will proxy a given IP.
193func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
194 endpoint := r.baseEndpoints.accountRoutes
195 endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
196 setVnetParam(&endpoint, params.VNetID)
197
198 resp, err := r.sendRequest("GET", endpoint, nil)
199 if err != nil {
200 return DetailedRoute{}, errors.Wrap(err, "REST request failed")
201 }
202 defer resp.Body.Close()
203
204 if resp.StatusCode == http.StatusOK {
205 return parseDetailedRoute(resp.Body)
206 }
207
208 return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
209}
210
211func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
212 var routes []*DetailedRoute
213 err := parseResponse(body, &routes)
214 return routes, err
215}
216
217func parseRoute(body io.ReadCloser) (Route, error) {
218 var route Route
219 err := parseResponse(body, &route)
220 return route, err
221}
222
223func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
224 var route DetailedRoute
225 err := parseResponse(body, &route)
226 return route, err
227}
228
229// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
230// virtual network (if one is provided).
231func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
232 queryParams := url.Values{}
233 if vnetID != nil {
234 queryParams.Set("virtual_network_id", vnetID.String())
235 }
236 endpoint.RawQuery = queryParams.Encode()
237}
238