cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2026.1.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/ip_route.go

235lines · 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.
140// Due to pagination on the server side it will call the endpoint multiple times if needed.
141func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
142 fetchFn := func(page int) (*http.Response, error) {
143 endpoint := r.baseEndpoints.accountRoutes
144 filter.Page(page)
145 endpoint.RawQuery = filter.Encode()
146 rsp, err := r.sendRequest("GET", endpoint, nil)
147
148 if err != nil {
149 return nil, errors.Wrap(err, "REST request failed")
150 }
151 if rsp.StatusCode != http.StatusOK {
152 rsp.Body.Close()
153 return nil, r.statusCodeToError("list routes", rsp)
154 }
155 return rsp, nil
156 }
157 return fetchExhaustively[DetailedRoute](fetchFn)
158}
159
160// AddRoute calls the Tunnelstore POST endpoint for a given route.
161func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
162 endpoint := r.baseEndpoints.accountRoutes
163 endpoint.Path = path.Join(endpoint.Path)
164 resp, err := r.sendRequest("POST", endpoint, newRoute)
165 if err != nil {
166 return Route{}, errors.Wrap(err, "REST request failed")
167 }
168 defer resp.Body.Close()
169
170 if resp.StatusCode == http.StatusOK {
171 return parseRoute(resp.Body)
172 }
173
174 return Route{}, r.statusCodeToError("add route", resp)
175}
176
177// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
178func (r *RESTClient) DeleteRoute(id uuid.UUID) error {
179 endpoint := r.baseEndpoints.accountRoutes
180 endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
181
182 resp, err := r.sendRequest("DELETE", endpoint, nil)
183 if err != nil {
184 return errors.Wrap(err, "REST request failed")
185 }
186 defer resp.Body.Close()
187
188 if resp.StatusCode == http.StatusOK {
189 _, err := parseRoute(resp.Body)
190 return err
191 }
192
193 return r.statusCodeToError("delete route", resp)
194}
195
196// GetByIP checks which route will proxy a given IP.
197func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
198 endpoint := r.baseEndpoints.accountRoutes
199 endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
200 setVnetParam(&endpoint, params.VNetID)
201
202 resp, err := r.sendRequest("GET", endpoint, nil)
203 if err != nil {
204 return DetailedRoute{}, errors.Wrap(err, "REST request failed")
205 }
206 defer resp.Body.Close()
207
208 if resp.StatusCode == http.StatusOK {
209 return parseDetailedRoute(resp.Body)
210 }
211
212 return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
213}
214
215func parseRoute(body io.ReadCloser) (Route, error) {
216 var route Route
217 err := parseResponse(body, &route)
218 return route, err
219}
220
221func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
222 var route DetailedRoute
223 err := parseResponse(body, &route)
224 return route, err
225}
226
227// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
228// virtual network (if one is provided).
229func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
230 queryParams := url.Values{}
231 if vnetID != nil {
232 queryParams.Set("virtual_network_id", vnetID.String())
233 }
234 endpoint.RawQuery = queryParams.Encode()
235}
236