Compare commits

...

19 Commits

Author SHA1 Message Date
Devin Carr a1a9f3813e Release 2024.4.0 2024-04-08 14:09:14 -07:00
GoncaloGarcia 7deb4340b4 Format code 2024-04-02 14:58:05 -07:00
Steven Kreitzer b5be8a6fa4 feat: auto tls sni
Signed-off-by: Steven Kreitzer <skre@skre.me>
2024-04-02 14:56:44 -07:00
Alexandru Tocar a665d3245a feat: provide short version (#1206)
Provides a short version output to assist with CLI parsing.
---------

Co-authored-by: Alex Tocar <alex.tocar@ueuie.dev>
2024-04-02 08:31:18 -07:00
chungthuang a48691fe78 Merge pull request #1125 from Shakahs/master
[access] Add environment variables for TCP tunnel hostname / destination / URL.
2024-04-02 10:25:21 -05:00
chungthuang b723a1a426 Merge pull request #1130 from crrodriguez/checkInPingGroupBugs
fix checkInPingGroup bugs
2024-04-02 10:24:51 -05:00
GoncaloGarcia bb29a0e194 Release 2024.3.0 2024-03-19 18:08:31 +00:00
GoncaloGarcia 86476e6248 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
Before this commit the commands that listed tunnels and tunnel routes would be limited to 1000 results by the server.

Now, the commands will call the endpoints until the result set is exhausted. This can take a long time if there are
thousands of pages available, since each request is executed synchronously.
From a user's perspective, nothing changes.
2024-03-19 16:35:40 +00:00
João "Pisco" Fernandes da6fac4133 TUN-8297: Improve write timeout logging on safe_stream.go
## Summary:
In order to properly monitor what is happening with the new write timeouts that we introduced
in TUN-8244 we need proper logging. Right now we were logging write timeouts when the safe
stream was being closed which didn't make sense because it was miss leading, so this commit
prevents that by adding a flag that allows us to know whether we are closing the stream or not.
2024-03-13 13:30:45 +00:00
João "Pisco" Fernandes 47ad3238dd TUN-8290: Remove || true from postrm.sh 2024-03-07 16:22:56 +00:00
João "Pisco" Fernandes 4f7165530c TUN-8275: Skip write timeout log on "no network activity"
## Summary
To avoid having to verbose logs we need to only log when an
actual issue occurred. Therefore, we will be skipping any error
logging if the write timeout is caused by no network activity
which just means that nothing is being sent through the stream.
2024-03-06 16:05:48 +00:00
Nikita Sivukhin a36fa07aba fix typo in errcheck for response parsing logic in CreateTunnel routine 2024-03-06 10:29:55 +00:00
Nanashi e846943e66 Update postrm.sh to fix incomplete uninstall 2024-03-06 10:29:55 +00:00
YueYue 652c82daa9 Update linux_service.go
Fix service fail to start due to unavaliable network
2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna a6760a6cbf ci/check: bump actions/setup-go to v5 2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna 204d55ecec ci: bump actions/checkout to v4 2024-03-06 10:29:55 +00:00
K.B.Dharun Krishna 1f4511ca6e check.yaml: bump actions/setup-go to v4 2024-03-06 10:29:55 +00:00
Cristian Rodríguez fbe357b1e6 fix checkInPingGroup bugs
- Must check for the *effective* GID.
- Must allow range from  0 to 4294967294 in current kernels.
2023-12-24 14:04:55 -03:00
Shak Saleemi 1776d3d335 Add environment variables for TCP tunnel hostname / destination / URL. 2023-12-15 16:02:36 -08:00
19 changed files with 225 additions and 113 deletions
+2 -2
View File
@@ -9,10 +9,10 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Test
run: make test
+19
View File
@@ -1,3 +1,22 @@
2024.4.0
- 2024-04-02 feat: provide short version (#1206)
- 2024-04-02 Format code
- 2024-01-18 feat: auto tls sni
- 2023-12-24 fix checkInPingGroup bugs
- 2023-12-15 Add environment variables for TCP tunnel hostname / destination / URL.
2024.3.0
- 2024-03-14 TUN-8281: Run cloudflared query list tunnels/routes endpoint in a paginated way
- 2024-03-13 TUN-8297: Improve write timeout logging on safe_stream.go
- 2024-03-07 TUN-8290: Remove `|| true` from postrm.sh
- 2024-03-05 TUN-8275: Skip write timeout log on "no network activity"
- 2024-01-23 Update postrm.sh to fix incomplete uninstall
- 2024-01-05 fix typo in errcheck for response parsing logic in CreateTunnel routine
- 2023-12-23 Update linux_service.go
- 2023-12-07 ci: bump actions/checkout to v4
- 2023-12-07 ci/check: bump actions/setup-go to v5
- 2023-04-28 check.yaml: bump actions/setup-go to v4
2024.2.1
- 2024-02-20 TUN-8242: Update Changes.md file with new remote diagnostics behaviour
- 2024-02-19 TUN-8238: Fix type mismatch introduced by fast-forward
+69 -8
View File
@@ -109,20 +109,34 @@ func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (
return r.client.Do(req)
}
func parseResponse(reader io.Reader, data interface{}) error {
func parseResponseEnvelope(reader io.Reader) (*response, error) {
// Schema for Tunnelstore responses in the v1 API.
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
var result response
// First, parse the wrapper and check the API call succeeded
if err := json.NewDecoder(reader).Decode(&result); err != nil {
return errors.Wrap(err, "failed to decode response")
return nil, errors.Wrap(err, "failed to decode response")
}
if err := result.checkErrors(); err != nil {
return err
return nil, err
}
if !result.Success {
return ErrAPINoSuccess
return nil, ErrAPINoSuccess
}
return &result, nil
}
func parseResponse(reader io.Reader, data interface{}) error {
result, err := parseResponseEnvelope(reader)
if err != nil {
return err
}
return parseResponseBody(result, data)
}
func parseResponseBody(result *response, data interface{}) error {
// At this point we know the API call succeeded, so, parse out the inner
// result into the datatype provided as a parameter.
if err := json.Unmarshal(result.Result, &data); err != nil {
@@ -131,11 +145,58 @@ func parseResponse(reader io.Reader, data interface{}) error {
return nil
}
func fetchExhaustively[T any](requestFn func(int) (*http.Response, error)) ([]*T, error) {
page := 0
var fullResponse []*T
for {
page += 1
envelope, parsedBody, err := fetchPage[T](requestFn, page)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("Error Parsing page %d", page))
}
fullResponse = append(fullResponse, parsedBody...)
if envelope.Pagination.Count < envelope.Pagination.PerPage || len(fullResponse) >= envelope.Pagination.TotalCount {
break
}
}
return fullResponse, nil
}
func fetchPage[T any](requestFn func(int) (*http.Response, error), page int) (*response, []*T, error) {
pageResp, err := requestFn(page)
if err != nil {
return nil, nil, errors.Wrap(err, "REST request failed")
}
defer pageResp.Body.Close()
if pageResp.StatusCode == http.StatusOK {
envelope, err := parseResponseEnvelope(pageResp.Body)
if err != nil {
return nil, nil, err
}
var parsedRspBody []*T
return envelope, parsedRspBody, parseResponseBody(envelope, &parsedRspBody)
}
return nil, nil, errors.New(fmt.Sprintf("Failed to fetch page. Server returned: %d", pageResp.StatusCode))
}
type response struct {
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Success bool `json:"success,omitempty"`
Errors []apiErr `json:"errors,omitempty"`
Messages []string `json:"messages,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Pagination Pagination `json:"result_info,omitempty"`
}
type Pagination struct {
Count int `json:"count,omitempty"`
Page int `json:"page,omitempty"`
PerPage int `json:"per_page,omitempty"`
TotalCount int `json:"total_count,omitempty"`
}
func (r *response) checkErrors() error {
+15 -17
View File
@@ -137,20 +137,24 @@ type GetRouteByIpParams struct {
}
// 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) {
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()
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 resp.StatusCode == http.StatusOK {
return parseListDetailedRoutes(resp.Body)
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 nil, r.statusCodeToError("list routes", resp)
return fetchExhaustively[DetailedRoute](fetchFn)
}
// AddRoute calls the Tunnelstore POST endpoint for a given route.
@@ -208,12 +212,6 @@ func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
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)
+4
View File
@@ -167,6 +167,10 @@ func (f *IpRouteFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f *IpRouteFilter) Page(page int) {
f.queryParams.Set("page", strconv.Itoa(page))
}
func (f IpRouteFilter) Encode() string {
return f.queryParams.Encode()
}
+15 -18
View File
@@ -93,7 +93,7 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWith
switch resp.StatusCode {
case http.StatusOK:
var tunnel TunnelWithToken
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
if serdeErr := parseResponse(resp.Body, &tunnel); serdeErr != nil {
return nil, serdeErr
}
return &tunnel, nil
@@ -177,25 +177,22 @@ func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error {
}
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
endpoint := r.baseEndpoints.accountLevel
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 parseListTunnels(resp.Body)
fetchFn := func(page int) (*http.Response, error) {
endpoint := r.baseEndpoints.accountLevel
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 tunnels", rsp)
}
return rsp, nil
}
return nil, r.statusCodeToError("list tunnels", resp)
}
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
var tunnels []*Tunnel
err := parseResponse(body, &tunnels)
return tunnels, err
return fetchExhaustively[Tunnel](fetchFn)
}
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
+4
View File
@@ -50,6 +50,10 @@ func (f *TunnelFilter) MaxFetchSize(max uint) {
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
}
func (f *TunnelFilter) Page(page int) {
f.queryParams.Set("page", strconv.Itoa(page))
}
func (f TunnelFilter) encode() string {
return f.queryParams.Encode()
}
-47
View File
@@ -3,7 +3,6 @@ package cfapi
import (
"bytes"
"fmt"
"io"
"net"
"reflect"
"strings"
@@ -16,52 +15,6 @@ import (
var loc, _ = time.LoadLocation("UTC")
func Test_parseListTunnels(t *testing.T) {
type args struct {
body string
}
tests := []struct {
name string
args args
want []*Tunnel
wantErr bool
}{
{
name: "empty list",
args: args{body: `{"success": true, "result": []}`},
want: []*Tunnel{},
},
{
name: "success is false",
args: args{body: `{"success": false, "result": []}`},
wantErr: true,
},
{
name: "errors are present",
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
wantErr: true,
},
{
name: "invalid response",
args: args{body: `abc`},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := io.NopCloser(bytes.NewReader([]byte(tt.args.body)))
got, err := parseListTunnels(body)
if (err != nil) != tt.wantErr {
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
}
})
}
}
func Test_unmarshalTunnel(t *testing.T) {
type args struct {
body string
+5 -2
View File
@@ -132,15 +132,18 @@ func Commands() []*cli.Command {
Name: sshHostnameFlag,
Aliases: []string{"tunnel-host", "T"},
Usage: "specify the hostname of your application.",
EnvVars: []string{"TUNNEL_SERVICE_HOSTNAME"},
},
&cli.StringFlag{
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
Name: sshDestinationFlag,
Usage: "specify the destination address of your SSH server.",
EnvVars: []string{"TUNNEL_SERVICE_DESTINATION"},
},
&cli.StringFlag{
Name: sshURLFlag,
Aliases: []string{"listener", "L"},
Usage: "specify the host:port to forward data to Cloudflare edge.",
EnvVars: []string{"TUNNEL_SERVICE_URL"},
},
&cli.StringSliceFlag{
Name: sshHeaderFlag,
+4 -2
View File
@@ -55,7 +55,8 @@ var systemdAllTemplates = map[string]ServiceTemplate{
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredService),
Content: `[Unit]
Description=cloudflared
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
TimeoutStartSec=0
@@ -72,7 +73,8 @@ WantedBy=multi-user.target
Path: fmt.Sprintf("/etc/systemd/system/%s", cloudflaredUpdateService),
Content: `[Unit]
Description=Update cloudflared
After=network.target
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/bin/bash -c '{{ .Path }} update; code=$?; if [ $code -eq 11 ]; then systemctl restart cloudflared; exit 0; fi; exit $code'
+11
View File
@@ -134,11 +134,22 @@ To determine if an update happened in a script, check for error code 11.`,
{
Name: "version",
Action: func(c *cli.Context) (err error) {
if c.Bool("short") {
fmt.Println(strings.Split(c.App.Version, " ")[0])
return nil
}
version(c)
return nil
},
Usage: versionText,
Description: versionText,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "short",
Aliases: []string{"s"},
Usage: "print just the version number",
},
},
},
}
cmds = append(cmds, tunnel.Commands()...)
+2
View File
@@ -205,6 +205,8 @@ type OriginRequestConfig struct {
HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"`
// Hostname on the origin server certificate.
OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"`
// Auto configure the Hostname on the origin server certificate.
MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"`
// Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare.
CAPool *string `yaml:"caPool" json:"caPool,omitempty"`
+19
View File
@@ -32,6 +32,7 @@ const (
ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout"
HTTPHostHeaderFlag = "http-host-header"
OriginServerNameFlag = "origin-server-name"
MatchSNIToHostFlag = "match-sni-to-host"
NoTLSVerifyFlag = "no-tls-verify"
NoChunkedEncodingFlag = "no-chunked-encoding"
ProxyAddressFlag = "proxy-address"
@@ -118,6 +119,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
var keepAliveTimeout = defaultKeepAliveTimeout
var httpHostHeader string
var originServerName string
var matchSNItoHost bool
var caPool string
var noTLSVerify bool
var disableChunkedEncoding bool
@@ -150,6 +152,9 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
if flag := OriginServerNameFlag; c.IsSet(flag) {
originServerName = c.String(flag)
}
if flag := MatchSNIToHostFlag; c.IsSet(flag) {
matchSNItoHost = c.Bool(flag)
}
if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) {
caPool = c.String(flag)
}
@@ -185,6 +190,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig {
KeepAliveTimeout: keepAliveTimeout,
HTTPHostHeader: httpHostHeader,
OriginServerName: originServerName,
MatchSNIToHost: matchSNItoHost,
CAPool: caPool,
NoTLSVerify: noTLSVerify,
DisableChunkedEncoding: disableChunkedEncoding,
@@ -229,6 +235,9 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
if c.OriginServerName != nil {
out.OriginServerName = *c.OriginServerName
}
if c.MatchSNIToHost != nil {
out.MatchSNIToHost = *c.MatchSNIToHost
}
if c.CAPool != nil {
out.CAPool = *c.CAPool
}
@@ -287,6 +296,8 @@ type OriginRequestConfig struct {
HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"`
// Hostname on the origin server certificate.
OriginServerName string `yaml:"originServerName" json:"originServerName"`
// Auto configure the Hostname on the origin server certificate.
MatchSNIToHost bool `yaml:"matchSNItoHost" json:"matchSNItoHost"`
// Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare.
CAPool string `yaml:"caPool" json:"caPool"`
@@ -362,6 +373,12 @@ func (defaults *OriginRequestConfig) setOriginServerName(overrides config.Origin
}
}
func (defaults *OriginRequestConfig) setMatchSNIToHost(overrides config.OriginRequestConfig) {
if val := overrides.MatchSNIToHost; val != nil {
defaults.MatchSNIToHost = *val
}
}
func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) {
if val := overrides.CAPool; val != nil {
defaults.CAPool = *val
@@ -447,6 +464,7 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi
cfg.setTCPKeepAlive(overrides)
cfg.setHTTPHostHeader(overrides)
cfg.setOriginServerName(overrides)
cfg.setMatchSNIToHost(overrides)
cfg.setCAPool(overrides)
cfg.setNoTLSVerify(overrides)
cfg.setDisableChunkedEncoding(overrides)
@@ -501,6 +519,7 @@ func ConvertToRawOriginConfig(c OriginRequestConfig) config.OriginRequestConfig
KeepAliveTimeout: keepAliveTimeout,
HTTPHostHeader: emptyStringToNil(c.HTTPHostHeader),
OriginServerName: emptyStringToNil(c.OriginServerName),
MatchSNIToHost: defaultBoolToNil(c.MatchSNIToHost),
CAPool: emptyStringToNil(c.CAPool),
NoTLSVerify: defaultBoolToNil(c.NoTLSVerify),
DisableChunkedEncoding: defaultBoolToNil(c.DisableChunkedEncoding),
+5 -5
View File
@@ -78,19 +78,19 @@ func checkInPingGroup() error {
if err != nil {
return err
}
groupID := os.Getgid()
groupID := uint64(os.Getegid())
// Example content: 999 59999
found := findGroupIDRegex.FindAll(file, 2)
if len(found) == 2 {
groupMin, err := strconv.ParseInt(string(found[0]), 10, 32)
groupMin, err := strconv.ParseUint(string(found[0]), 10, 32)
if err != nil {
return errors.Wrapf(err, "failed to determine minimum ping group ID")
}
groupMax, err := strconv.ParseInt(string(found[1]), 10, 32)
groupMax, err := strconv.ParseUint(string(found[1]), 10, 32)
if err != nil {
return errors.Wrapf(err, "failed to determine minimum ping group ID")
return errors.Wrapf(err, "failed to determine maximum ping group ID")
}
if groupID < int(groupMin) || groupID > int(groupMax) {
if groupID < groupMin || groupID > groupMax {
return fmt.Errorf("Group ID %d is not between ping group %d to %d", groupID, groupMin, groupMax)
}
return nil
+21
View File
@@ -2,7 +2,9 @@ package ingress
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"github.com/rs/zerolog"
@@ -48,9 +50,28 @@ func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("X-Forwarded-Host", req.Host)
req.Host = o.hostHeader
}
if o.matchSNIToHost {
o.SetOriginServerName(req)
}
return o.transport.RoundTrip(req)
}
func (o *httpService) SetOriginServerName(req *http.Request) {
o.transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := o.transport.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
return tls.Client(conn, &tls.Config{
RootCAs: o.transport.TLSClientConfig.RootCAs,
InsecureSkipVerify: o.transport.TLSClientConfig.InsecureSkipVerify,
ServerName: req.Host,
}), nil
}
}
func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) {
if o.defaultResp {
o.log.Warn().Msgf(ErrNoIngressRulesCLI.Error())
+5 -3
View File
@@ -68,9 +68,10 @@ func (o unixSocketPath) MarshalJSON() ([]byte, error) {
}
type httpService struct {
url *url.URL
hostHeader string
transport *http.Transport
url *url.URL
hostHeader string
transport *http.Transport
matchSNIToHost bool
}
func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
@@ -80,6 +81,7 @@ func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRe
}
o.hostHeader = cfg.HTTPHostHeader
o.transport = transport
o.matchSNIToHost = cfg.MatchSNIToHost
return nil
}
+4 -4
View File
@@ -204,25 +204,25 @@ func TestMarshalJSON(t *testing.T) {
{
name: "Nil",
path: nil,
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
want: true,
},
{
name: "Nil regex",
path: &Regexp{Regexp: nil},
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
want: true,
},
{
name: "Empty",
path: &Regexp{Regexp: regexp.MustCompile("")},
expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
want: true,
},
{
name: "Basic",
path: &Regexp{Regexp: regexp.MustCompile("/echo")},
expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`,
want: true,
},
}
+2 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash
set -eu
rm /usr/local/bin/cloudflared
rm /usr/local/etc/cloudflared/.installedFromPackageManager || true
rm -f /usr/local/bin/cloudflared
rm -f /usr/local/etc/cloudflared/.installedFromPackageManager
+19 -3
View File
@@ -4,6 +4,7 @@ import (
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/quic-go/quic-go"
@@ -11,11 +12,15 @@ import (
"github.com/rs/zerolog/log"
)
// The error that is throw by the writer when there is `no network activity`.
var idleTimeoutError = quic.IdleTimeoutError{}
type SafeStreamCloser struct {
lock sync.Mutex
stream quic.Stream
writeTimeout time.Duration
log *zerolog.Logger
closing atomic.Bool
}
func NewSafeStreamCloser(stream quic.Stream, writeTimeout time.Duration, log *zerolog.Logger) *SafeStreamCloser {
@@ -41,24 +46,35 @@ func (s *SafeStreamCloser) Write(p []byte) (n int, err error) {
}
nBytes, err := s.stream.Write(p)
if err != nil {
s.handleTimeout(err)
s.handleWriteError(err)
}
return nBytes, err
}
// Handles the timeout error in case it happened, by canceling the stream write.
func (s *SafeStreamCloser) handleTimeout(err error) {
func (s *SafeStreamCloser) handleWriteError(err error) {
// If we are closing the stream we just ignore any write error.
if s.closing.Load() {
return
}
var netErr net.Error
if errors.As(err, &netErr) {
if netErr.Timeout() {
s.log.Error().Err(netErr).Msg("Closing quic stream due to timeout while writing")
// We don't need to log if what cause the timeout was no network activity.
if !errors.Is(netErr, &idleTimeoutError) {
s.log.Error().Err(netErr).Msg("Closing quic stream due to timeout while writing")
}
// We need to explicitly cancel the write so that it frees all buffers.
s.stream.CancelWrite(0)
}
}
}
func (s *SafeStreamCloser) Close() error {
// Set this stream to a closing state.
s.closing.Store(true)
// Make sure a possible writer does not block the lock forever. We need it, so we can close the writer
// side of the stream safely.
_ = s.stream.SetWriteDeadline(time.Now())