Repository: cloudflare/cloudflared

Issue:
Built-in Docker Healthcheck
**Describe the feature you'd like**
I would like the docker image to contain curl or wget so it's possible to create a docker healthcheck that docker-compose or docker swarm can use.

It would be nice the healthchecking is just native to the cloudflare/cloudflared image like so.
```Dockerfile
FROM cloudflare/cloudflared:2023.10.0 as cloudflared
FROM alpine:3.18.4

COPY --from=cloudflared /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=cloudflared /usr/local/bin/cloudflared /usr/local/bin

ENV TUNNEL_METRICS="localhost:2000"

HEALTHCHECK --interval=5s --retries=6 --timeout=3s CMD wget -q ${TUNNEL_METRICS}/ready -O -
```

If building from alpine is something you don't want to do, then provide a `cloudflare tunnel health` command that can be used like so
```Dockerfile
HEALTHCHECK --interval=5s --retries=6 --timeout=3s CMD cloudflare tunnel health --ready-endpoint localhost:2000/ready
```
or like so
```yml
services:
  cloudflared:
    image: cloudflare/cloudflared
    environment:
      TUNNEL_METRICS: "localhost:2000"
    healthcheck:
      command: cloudflared tunnel health --ready-endpoint ${TUNNEL_METRICS}/ready
      interval: 5s
      retries: 6
      timeout: 3s
```

**Describe alternatives you've considered**
Build my own image built on-top of the cloudflare/cloudflared image or use third-party images built on top of cloudflare's

Discussion:
@mordilloSan: I second this. Would be very helpful when using auto heal. @Letgamer: Using the following Dockerfile could be working:
```
FROM tarampampam/curl as curl

FROM erisamoe/cloudflared
COPY --from=curl /bin/curl /bin/
```
Credits:
https://github.com/sidevesh/cloudflared-with-health-check@maggie44: I think that PR is helpful, although it may also be worth reevaluating whether the current `base-debian11` containers still make sense as well. If the goal is to have a minimal container, it may as well be `scratch`. The Dockerfile (https://github.com/cloudflare/cloudflared/blob/master/Dockerfile) mentions `# use a distroless base image with glibc` but I'm not sure why glibc would be required for the Golang compiled binary. 

However, I would find a more helpful base image to be `busybox`. It contains some basic tools that would help with debugging. I am currently running Cloudflared on a Kubernetes cluster and for some reason it isn't seeing my service which it is supposed to redirect to. It would be really helpful if I could get in to that container and be able to ping my service endpoint to help with debugging my Kubernetes infrastructure. I tend to favour `busybox` over Alpine as it is even more minimal than Alpine, and subsequently has very few updates. As a maintainer you often find yourself constantly bumping the Alpine container version for no reason just to try and keep up. 

There is a case to be made that a `scratch` container has additional security benefits, but it's a really hard case to make. If someone has got as far as being able to send `exec` commands to your `busybox` container, it's game over either way. @CodaBool: > Using the following Dockerfile could be working `COPY --from=tarampampam/curl /bin/curl /bin/`

No one is going to use copy curl from a unreputable image like that. But the general method is right. It is better however, to use `wget` for something like this. It's much smaller in size (~1Mb when using busybox's version).

> Here is an example

```dockerfile
FROM gcr.io/distroless/java21-debian12:nonroot
COPY --from=busybox:stable-glibc /bin/wget /usr/bin/wget
```

If you wanted to save 500Kb-1Mb and add security the alternative is add healthcheck Go code to the repo. This could either be a separate static binary with a http call. Or add onto the CLI with a  flag, ~~like how [watchtower does it](https://github.com/containrrr/watchtower/pull/1725/files)~~ (looking at this a second time, it seems watchtower is actually [just checking if another watchtower process is running](https://github.com/containrrr/watchtower/issues/1257#issuecomment-1666477169), which is not really a healthcheck). It would need to follow the docker spec and do exit 0 on healthy. Exit 1 on unhealthy.

I have 2 PRs I did recently with other dockerfiles that expand the app to have a healthcheck feature. So, those could be looked as general examples (but none of them are in go, they are in [rust](https://github.com/szabodanika/microbin/pull/264), and in [node+python](https://github.com/immich-app/immich/pull/9583))

> If the goal is to have a minimal container, it may as well be scratch

debian distroless is fine. No need to change off of it. Tons of Go projects work this way.
@maggie44: A good rule of thumb with Docker images, is to put in what you need and no more. Distroless image is far more than is needed. Not a deal breaker or concern, but unnecessary if it's a pure go build. 

`wget` a good option if space saving is a concern.

Also a `tiny-curl` version, but would suggest building from source rather than importing packages from others: https://github.com/maggie44/docker-tiny-curl

Or, as mentioned above, make the base image `busybox` or `Alpine` which includes `wget`, more secure and stable than building and copying in your own, and is 4mb (`busybox` decompressed) instead of distroless image's 18mb even before copying in wget. @firecow: All of this is irrelevant, if the cloudflared binary could execute a command to check the `/ready` endpoint

https://github.com/cloudflare/cloudflared/pull/1135 We need this merged, to close this issue.@kamaradski: I'm testing the curl method:

```
FROM tarampampam/curl as curl
FROM erisamoe/cloudflared
COPY --from=curl /bin/curl /bin/
```
in AWS ECS like this:
```
resource "aws_ecs_task_definition" "ecs_cloudflared_task_dev" {
  family                   = "cloudflare-tunnel"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 512
  memory                   = 1024
  execution_role_arn       = aws_iam_role.ecs_task_execution_role_dev.arn
  task_role_arn            = aws_iam_role.ecs_task_execution_role_dev.arn
  tags                     = module.meta_ecs_cloudflared_task_dev.tags

  container_definitions = jsonencode([
    {
      name      = module.meta_ecs_cloudflared_task_dev.name
      image     = "${aws_ecr_repository.ecr_cloudflare_tunnel_dev.repository_url}:latest",
      essential = true
      command = [
        "tunnel",
        "--metrics", "0.0.0.0:3333",
        "--no-autoupdate",
        "run",
        "--token", cloudflare_tunnel.cloudflare_tunnel_dev.tunnel_token
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = module.meta_cloudwatch_loggroup_dev.name
          "awslogs-region"        = local.base.region
          "awslogs-stream-prefix" = "cloudflare-tunnel"
        }
      }

      portMappings = [
        {
          containerPort = 3333
          hostPort      = 3333
          protocol      = "tcp"
        }
      ]

      # https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_healthcheck
      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost:3333/metrics || exit 1"],
        interval    = 10 # Interval in seconds between health checks
        timeout     = 10 # Timeout in seconds for each health check
        retries     = 3  # Number of retries before marking the container as unhealthy
        startPeriod = 10 # Optional: grace period to allow the container to start
      }
    }
  ])
}
```
But the healthcheck is not working in ECS. (it does locally... :shrug: ). So i'm really waiting for https://github.com/cloudflare/cloudflared/pull/1135 to get released and have a proper standard solution.@huyz: This is badly needed. For me, the Cloudflared docker container locks up every few days for unknown reasons @jahands: I ran into this and am surprised there's not a buitl-in way to do healthchecks.

I [wrote up](https://jacobhands.com/blog/cloudflare-tunnel-health-checks-using-docker-compose/) my workaround where I ended up adding a separate service to my docker-compose.yml for health checks.@CodaBool: Wait @jahands don't you work at cloudflare?

Can you help have PR  [#1135](https://github.com/cloudflare/cloudflared/pull/1135) get official attention?

~~@firecow #1135 is the better PR right? Why did you open this one? #1135 is a no dependency or bloat solution, why add curl~~

Nvm 1135 adds the command this one runs it@huyz: @rubymignot Why? https://github.com/cloudflare/cloudflared/pull/1135 has long been merged into the official image

Solution patch:
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index c821e5a5..4090692f 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,4 +1,3 @@
-
 on:
   pull_request: {}
   workflow_dispatch: {}
@@ -12,14 +11,14 @@ name: Semgrep config
 jobs:
   semgrep:
     name: semgrep/ci
-    runs-on: ubuntu-20.04
+    runs-on: ubuntu-latest
     env:
       SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
       SEMGREP_URL: https://cloudflare.semgrep.dev
       SEMGREP_APP_URL: https://cloudflare.semgrep.dev
       SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version
     container:
-      image: returntocorp/semgrep
+      image: semgrep/semgrep
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - run: semgrep ci
diff --git a/Makefile b/Makefile
index c8652bec..116bcc8f 100644
--- a/Makefile
+++ b/Makefile
@@ -165,6 +165,18 @@ cover:
 	# Generate the HTML report that can be viewed from the browser in CI.
 	$Q go tool cover -html ".cover/c.out" -o .cover/all.html
 
+.PHONY: fuzz
+fuzz:
+	@go test -fuzz=FuzzIPDecoder -fuzztime=600s ./packet
+	@go test -fuzz=FuzzICMPDecoder -fuzztime=600s ./packet
+	@go test -fuzz=FuzzSessionWrite -fuzztime=600s ./quic/v3
+	@go test -fuzz=FuzzSessionServe -fuzztime=600s ./quic/v3
+	@go test -fuzz=FuzzRegistrationDatagram -fuzztime=600s ./quic/v3
+	@go test -fuzz=FuzzPayloadDatagram -fuzztime=600s ./quic/v3
+	@go test -fuzz=FuzzRegistrationResponseDatagram -fuzztime=600s ./quic/v3
+	@go test -fuzz=FuzzNewIdentity -fuzztime=600s ./tracing
+	@go test -fuzz=FuzzNewAccessValidator -fuzztime=600s ./validation
+
 .PHONY: install-go
 install-go:
 	rm -rf ${CF_GO_PATH}
diff --git a/README.md b/README.md
index 7a3d3bbf..e0db8dc0 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ Once installed, you can authenticate `cloudflared` into your Cloudflare account
 
 ## TryCloudflare
 
-Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/trycloudflare).
+Want to test Cloudflare Tunnel before adding a website to Cloudflare? You can do so with TryCloudflare using the documentation [available here](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/).
 
 ## Deprecated versions
 
diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go
index 92f20bd7..f7482154 100644
--- a/cmd/cloudflared/tunnel/cmd.go
+++ b/cmd/cloudflared/tunnel/cmd.go
@@ -139,6 +139,7 @@ func Commands() []*cli.Command {
 		buildVirtualNetworkSubcommand(false),
 		buildRunCommand(),
 		buildListCommand(),
+		buildReadyCommand(),
 		buildInfoCommand(),
 		buildIngressSubcommand(),
 		buildDeleteCommand(),
diff --git a/cmd/cloudflared/tunnel/subcommands.go b/cmd/cloudflared/tunnel/subcommands.go
index bef86887..dbe9e80a 100644
--- a/cmd/cloudflared/tunnel/subcommands.go
+++ b/cmd/cloudflared/tunnel/subcommands.go
@@ -5,6 +5,8 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"io"
+	"net/http"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -397,6 +399,39 @@ func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected boo
 	return strings.Join(output, ", ")
 }
 
+func buildReadyCommand() *cli.Command {
+	return &cli.Command{
+		Name:               "ready",
+		Action:             cliutil.ConfiguredAction(readyCommand),
+		Usage:              "Call /ready endpoint and return proper exit code",
+		UsageText:          "cloudflared tunnel [tunnel command options] ready [subcommand options]",
+		Description:        "cloudflared tunnel ready will return proper exit code based on the /ready endpoint",
+		Flags:              []cli.Flag{},
+		CustomHelpTemplate: commandHelpTemplate(),
+	}
+}
+
+func readyCommand(c *cli.Context) error {
+	metricsOpts := c.String("metrics")
+	if !c.IsSet("metrics") {
+		return fmt.Errorf("--metrics has to be provided")
+	}
+
+	requestURL := fmt.Sprintf("http://%s/ready", metricsOpts)
+	res, err := http.Get(requestURL)
+	if err != nil {
+		return err
+	}
+	if res.StatusCode != 200 {
+		body, err := io.ReadAll(res.Body)
+		if err != nil {
+			return err
+		}
+		return fmt.Errorf("http://%s/ready endpoint returned status code %d\n%s", metricsOpts, res.StatusCode, body)
+	}
+	return nil
+}
+
 func buildInfoCommand() *cli.Command {
 	return &cli.Command{
 		Name:        "info",



Test patch:
diff --git a/packet/decoder_test.go b/packet/decoder_test.go
index b8770d74..4dc96886 100644
--- a/packet/decoder_test.go
+++ b/packet/decoder_test.go
@@ -254,3 +254,18 @@ func (u *UDP) EncodeLayers() ([]gopacket.SerializableLayer, error) {
 	udpLayer.SetNetworkLayerForChecksum(ipLayers[0].(gopacket.NetworkLayer))
 	return append(ipLayers, &udpLayer), nil
 }
+
+func FuzzIPDecoder(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		ipDecoder := NewIPDecoder()
+		ipDecoder.Decode(RawPacket{Data: data})
+
+	})
+}
+
+func FuzzICMPDecoder(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		icmpDecoder := NewICMPDecoder()
+		icmpDecoder.Decode(RawPacket{Data: data})
+	})
+}
diff --git a/quic/v3/datagram_test.go b/quic/v3/datagram_test.go
index ff46ef24..2c5f06fb 100644
--- a/quic/v3/datagram_test.go
+++ b/quic/v3/datagram_test.go
@@ -350,3 +350,30 @@ func compareRegistrationDatagrams(t *testing.T, l *v3.UDPSessionRegistrationData
 		l.IdleDurationHint == r.IdleDurationHint &&
 		l.Traced == r.Traced
 }
+
+func FuzzRegistrationDatagram(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		unmarshaled := v3.UDPSessionRegistrationDatagram{}
+		err := unmarshaled.UnmarshalBinary(data)
+		if err == nil {
+			_, _ = unmarshaled.MarshalBinary()
+		}
+	})
+}
+
+func FuzzPayloadDatagram(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		unmarshaled := v3.UDPSessionPayloadDatagram{}
+		_ = unmarshaled.UnmarshalBinary(data)
+	})
+}
+
+func FuzzRegistrationResponseDatagram(f *testing.F) {
+	f.Fuzz(func(t *testing.T, data []byte) {
+		unmarshaled := v3.UDPSessionRegistrationResponseDatagram{}
+		err := unmarshaled.UnmarshalBinary(data)
+		if err == nil {
+			_, _ = unmarshaled.MarshalBinary()
+		}
+	})
+}
diff --git a/tracing/tracing_test.go b/tracing/tracing_test.go
index 5c478ed6..826e4f12 100644
--- a/tracing/tracing_test.go
+++ b/tracing/tracing_test.go
@@ -72,3 +72,9 @@ func TestAddingSpansWithNilMap(t *testing.T) {
 	// a panic shouldn't occur
 	tr.AddSpans(nil)
 }
+
+func FuzzNewIdentity(f *testing.F) {
+	f.Fuzz(func(t *testing.T, trace string) {
+		_, _ = NewIdentity(trace)
+	})
+}
diff --git a/validation/validation_test.go b/validation/validation_test.go
index 9f4a2ceb..3e4534cf 100644
--- a/validation/validation_test.go
+++ b/validation/validation_test.go
@@ -197,3 +197,10 @@ func createSecureMockServerAndClient(handler http.Handler) (*httptest.Server, *h
 
 	return server, client, nil
 }
+
+func FuzzNewAccessValidator(f *testing.F) {
+	f.Fuzz(func(t *testing.T, domain string, issuer string, applicationAUD string) {
+		ctx := context.Background()
+		_, _ = NewAccessValidator(ctx, domain, issuer, applicationAUD)
+	})
+}