cloudflare/cloudflared

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
2025.11.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

cfapi/tunnel.go

237lines · modecode

1package cfapi
2
3import (
4 "fmt"
5 "io"
6 "net"
7 "net/http"
8 "net/url"
9 "path"
10 "time"
11
12 "github.com/google/uuid"
13 "github.com/pkg/errors"
14)
15
16var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
17
18type Tunnel struct {
19 ID uuid.UUID `json:"id"`
20 Name string `json:"name"`
21 CreatedAt time.Time `json:"created_at"`
22 DeletedAt time.Time `json:"deleted_at"`
23 Connections []Connection `json:"connections"`
24}
25
26type TunnelWithToken struct {
27 Tunnel
28 Token string `json:"token"`
29}
30
31type Connection struct {
32 ColoName string `json:"colo_name"`
33 ID uuid.UUID `json:"id"`
34 IsPendingReconnect bool `json:"is_pending_reconnect"`
35 OriginIP net.IP `json:"origin_ip"`
36 OpenedAt time.Time `json:"opened_at"`
37}
38
39type ActiveClient struct {
40 ID uuid.UUID `json:"id"`
41 Features []string `json:"features"`
42 Version string `json:"version"`
43 Arch string `json:"arch"`
44 RunAt time.Time `json:"run_at"`
45 Connections []Connection `json:"conns"`
46}
47
48type newTunnel struct {
49 Name string `json:"name"`
50 TunnelSecret []byte `json:"tunnel_secret"`
51}
52
53type managementRequest struct {
54 Resources []string `json:"resources"`
55}
56
57type CleanupParams struct {
58 queryParams url.Values
59}
60
61func NewCleanupParams() *CleanupParams {
62 return &CleanupParams{
63 queryParams: url.Values{},
64 }
65}
66
67func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
68 cp.queryParams.Set("client_id", clientID.String())
69}
70
71func (cp CleanupParams) encode() string {
72 return cp.queryParams.Encode()
73}
74
75func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) {
76 if name == "" {
77 return nil, errors.New("tunnel name required")
78 }
79 if _, err := uuid.Parse(name); err == nil {
80 return nil, errors.New("you cannot use UUIDs as tunnel names")
81 }
82 body := &newTunnel{
83 Name: name,
84 TunnelSecret: tunnelSecret,
85 }
86
87 resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
88 if err != nil {
89 return nil, errors.Wrap(err, "REST request failed")
90 }
91 defer resp.Body.Close()
92
93 switch resp.StatusCode {
94 case http.StatusOK:
95 var tunnel TunnelWithToken
96 if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
97 return nil, serdeErr
98 }
99 return &tunnel, nil
100 case http.StatusConflict:
101 return nil, ErrTunnelNameConflict
102 }
103
104 return nil, r.statusCodeToError("create tunnel", resp)
105}
106
107func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
108 endpoint := r.baseEndpoints.accountLevel
109 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
110 resp, err := r.sendRequest("GET", endpoint, nil)
111 if err != nil {
112 return nil, errors.Wrap(err, "REST request failed")
113 }
114 defer resp.Body.Close()
115
116 if resp.StatusCode == http.StatusOK {
117 return unmarshalTunnel(resp.Body)
118 }
119
120 return nil, r.statusCodeToError("get tunnel", resp)
121}
122
123func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error) {
124 endpoint := r.baseEndpoints.accountLevel
125 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/token", tunnelID))
126 resp, err := r.sendRequest("GET", endpoint, nil)
127 if err != nil {
128 return "", errors.Wrap(err, "REST request failed")
129 }
130 defer resp.Body.Close()
131
132 if resp.StatusCode == http.StatusOK {
133 err = parseResponse(resp.Body, &token)
134 return token, err
135 }
136
137 return "", r.statusCodeToError("get tunnel token", resp)
138}
139
140func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) {
141 endpoint := r.baseEndpoints.accountLevel
142 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID))
143
144 body := &managementRequest{
145 Resources: []string{"logs"},
146 }
147
148 resp, err := r.sendRequest("POST", endpoint, body)
149 if err != nil {
150 return "", errors.Wrap(err, "REST request failed")
151 }
152 defer resp.Body.Close()
153
154 if resp.StatusCode == http.StatusOK {
155 err = parseResponse(resp.Body, &token)
156 return token, err
157 }
158
159 return "", r.statusCodeToError("get tunnel token", resp)
160}
161
162func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
163 endpoint := r.baseEndpoints.accountLevel
164 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
165 // Cascade will delete all tunnel dependencies (connections, routes, etc.) that
166 // are linked to the deleted tunnel.
167 if cascade {
168 endpoint.RawQuery = "cascade=true"
169 }
170 resp, err := r.sendRequest("DELETE", endpoint, nil)
171 if err != nil {
172 return errors.Wrap(err, "REST request failed")
173 }
174 defer resp.Body.Close()
175
176 return r.statusCodeToError("delete tunnel", resp)
177}
178
179func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
180 fetchFn := func(page int) (*http.Response, error) {
181 endpoint := r.baseEndpoints.accountLevel
182 filter.Page(page)
183 endpoint.RawQuery = filter.encode()
184 rsp, err := r.sendRequest("GET", endpoint, nil)
185 if err != nil {
186 return nil, errors.Wrap(err, "REST request failed")
187 }
188 if rsp.StatusCode != http.StatusOK {
189 rsp.Body.Close()
190 return nil, r.statusCodeToError("list tunnels", rsp)
191 }
192 return rsp, nil
193 }
194
195 return fetchExhaustively[Tunnel](fetchFn)
196}
197
198func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
199 endpoint := r.baseEndpoints.accountLevel
200 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
201 resp, err := r.sendRequest("GET", endpoint, nil)
202 if err != nil {
203 return nil, errors.Wrap(err, "REST request failed")
204 }
205 defer resp.Body.Close()
206
207 if resp.StatusCode == http.StatusOK {
208 return parseConnectionsDetails(resp.Body)
209 }
210
211 return nil, r.statusCodeToError("list connection details", resp)
212}
213
214func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
215 var clients []*ActiveClient
216 err := parseResponse(reader, &clients)
217 return clients, err
218}
219
220func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
221 endpoint := r.baseEndpoints.accountLevel
222 endpoint.RawQuery = params.encode()
223 endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
224 resp, err := r.sendRequest("DELETE", endpoint, nil)
225 if err != nil {
226 return errors.Wrap(err, "REST request failed")
227 }
228 defer resp.Body.Close()
229
230 return r.statusCodeToError("cleanup connections", resp)
231}
232
233func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
234 var tunnel Tunnel
235 err := parseResponse(reader, &tunnel)
236 return &tunnel, err
237}
238