├── .github └── workflows │ ├── ci.yml │ └── review.yml ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── examples └── Dockerfile ├── go.mod ├── go.sum ├── grpc_healthcheck.go ├── helpers.go ├── http_healthcheck.go ├── init.go ├── main.go └── scripts └── test.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | tags: [ v* ] 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | 12 | tests: 13 | name: tests 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 1.x 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version-file: 'go.mod' 23 | 24 | - name: Run test.sh 25 | run: | 26 | scripts/test.sh 27 | 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | permissions: 32 | packages: write 33 | contents: write 34 | pull-requests: read 35 | checks: write 36 | needs: [ tests ] 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | 41 | - name: Set up Go 1.x 42 | uses: actions/setup-go@v5 43 | with: 44 | go-version-file: 'go.mod' 45 | 46 | - name: Set up QEMU 47 | uses: docker/setup-qemu-action@v3 48 | 49 | - name: Docker Login 50 | uses: docker/login-action@v3 51 | with: 52 | registry: ghcr.io 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | - name: golangci-lint 57 | uses: golangci/golangci-lint-action@v6 58 | 59 | - name: Prepare a snapshot release 60 | if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 61 | uses: goreleaser/goreleaser-action@v3 62 | with: 63 | version: latest 64 | args: release --clean --snapshot 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | DOCKER_CLI_EXPERIMENTAL: "enabled" 68 | 69 | - name: Release a new version 70 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 71 | uses: goreleaser/goreleaser-action@v4 72 | with: 73 | version: latest 74 | args: release --clean 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 77 | DOCKER_CLI_EXPERIMENTAL: "enabled" 78 | 79 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Update review branch 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | review: 8 | name: Review 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | ref: main 16 | fetch-depth: 0 17 | 18 | - name: Update review branch 19 | id: update-review 20 | run: | 21 | git push origin main:review 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # build artifacts 18 | build/ 19 | dist/ 20 | release/ 21 | lprobe 22 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: lprobe 2 | 3 | before: 4 | hooks: 5 | - go mod download 6 | 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | ldflags: ["-w -s"] 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | goarch: 16 | - amd64 17 | - arm64 18 | 19 | archives: 20 | - format: binary 21 | name_template: "{{ .Binary }}-{{ .Os }}-{{ .Arch }}" 22 | 23 | checksum: 24 | name_template: "checksums.txt" 25 | algorithm: sha256 26 | 27 | snapshot: 28 | name_template: "{{ incpatch .Version }}-snap" 29 | 30 | changelog: 31 | use: github 32 | 33 | dockers: 34 | - image_templates: ["ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64"] 35 | goos: linux 36 | goarch: amd64 37 | dockerfile: Dockerfile 38 | use: buildx 39 | build_flag_templates: 40 | - --platform=linux/amd64 41 | - --label=org.opencontainers.image.title={{ .ProjectName }} 42 | - --label=org.opencontainers.image.description={{ .ProjectName }} 43 | - --label=org.opencontainers.image.url=https://github.com/fivexl/{{ .ProjectName }} 44 | - --label=org.opencontainers.image.source=https://github.com/fivexl/{{ .ProjectName }} 45 | - --label=org.opencontainers.image.version={{ .Version }} 46 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} 47 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 48 | - --label=org.opencontainers.image.licenses=Apache-2.0 49 | - image_templates: ["ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8"] 50 | goos: linux 51 | goarch: arm64 52 | dockerfile: Dockerfile 53 | use: buildx 54 | build_flag_templates: 55 | - --platform=linux/arm64/v8 56 | - --label=org.opencontainers.image.title={{ .ProjectName }} 57 | - --label=org.opencontainers.image.description={{ .ProjectName }} 58 | - --label=org.opencontainers.image.url=https://github.com/fivexl/{{ .ProjectName }} 59 | - --label=org.opencontainers.image.source=https://github.com/fivexl/{{ .ProjectName }} 60 | - --label=org.opencontainers.image.version={{ .Version }} 61 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }} 62 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 63 | - --label=org.opencontainers.image.licenses=Apache-2.0 64 | docker_manifests: 65 | - name_template: ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }} 66 | image_templates: 67 | - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64 68 | - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8 69 | - name_template: ghcr.io/fivexl/{{ .ProjectName }}:latest 70 | image_templates: 71 | - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-amd64 72 | - ghcr.io/fivexl/{{ .ProjectName }}:{{ .Version }}-arm64v8 73 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v0.0.6 2 | 3 | - container images with lprobe inside. ghcr.io as registry. 4 | 5 | ### v0.0.5 6 | 7 | - http mode features: tls-ca-cert, tls-client-cert, tls-client-key, tls-server-name 8 | - new feature: ipv6 [::1] 9 | 10 | ### v0.0.4 11 | 12 | - http mode features: tls-no-verify, connect-timeout, user-agent 13 | 14 | ### v0.0.3 15 | 16 | - init optimization 17 | 18 | ### v0.0.2 19 | 20 | - grpc mode 21 | 22 | ### v0.0.1 23 | 24 | - first version -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY lprobe /lprobe -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![FivexL](https://releases.fivexl.io/fivexlbannergit.jpg)](https://fivexl.io/) 2 | 3 | # Why LProbe? 4 | A command-line tool to perform Local Health Check Probes inside Container Images (ECS, Docker, Kubernetes). When your container gets breached, the intruder/attacker can use tools like wget or curl to download more tools for further exploitation and lateral movement within your system. Thus we developed LProbe as wget/curl replacement for hardened and secure container images. 5 | 6 | ## HOW TO 7 | ### Local run 8 | ```shell 9 | ./lprobe -port=8080 -endpoint=/ 10 | ``` 11 | 12 | ### Local run for gRPC 13 | ```shell 14 | ./lprobe -port=8080 -mode=grpc 15 | ``` 16 | 17 | ### Add to a container image 18 | You can bundle the statically compiled LProbe in your container image. Choose a binary release and download it in your Dockerfile: 19 | ``` 20 | ARG LPROBE_VERSION=v0.0.6 21 | ARG TARGETPLATFORM 22 | RUN case ${TARGETPLATFORM} in \ 23 | "linux/amd64") LPROBE_ARCH=amd64 ;; \ 24 | "linux/arm64") LPROBE_ARCH=arm64 ;; \ 25 | esac \ 26 | && wget -qO/bin/lprobe https://github.com/fivexl/lprobe/releases/download/${LPROBE_VERSION}/lprobe-linux-${LPROBE_ARCH} \ 27 | && chmod +x /bin/lprobe \ 28 | && rm -f /usr/bin/wget 29 | ``` 30 | 31 | ### Add to a container image from lprobe containter 32 | ``` 33 | FROM scratch 34 | COPY --from=ghcr.io/fivexl/lprobe:0.0.6 /lprobe /bin/lprobe 35 | ``` 36 | 37 | ### Docker Healthcheck 38 | ``` 39 | HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 CMD [ "lprobe", "-mode=http", "-port=8080", "-endpoint=/healthz" ] 40 | ``` 41 | 42 | ### ECS Healthcheck 43 | ``` 44 | [ "CMD", "lprobe", "-port=8080", "-endpoint=/healthz"] 45 | ``` 46 | 47 | ### Kubernetes (k8S) Healthcheck 48 | ``` 49 | spec: 50 | containers: 51 | - name: server 52 | image: "[YOUR-DOCKER-IMAGE]" 53 | ports: 54 | - containerPort: 8080 55 | readinessProbe: 56 | exec: 57 | command: ["/bin/lprobe", "-port=8080", "-endpoint=/readiness"] 58 | initialDelaySeconds: 5 59 | livenessProbe: 60 | exec: 61 | command: ["/bin/lprobe", "-port=8080", "-endpoint=/liveness"] 62 | initialDelaySeconds: 10 63 | ``` 64 | 65 | ### It is possible to use Lprobe as mounted volume 66 | 67 | To use Lprobe via volume mounts, you need to download the Lprobe binary and place it in the volume. Here is how you can do it: 68 | 69 | 1. Download the Lprobe binary: 70 | ``` 71 | wget -qO ./Lprobe https://github.com/fivexl/lprobe/releases/download/v0.1.5/lprobe-linux-amd64 72 | chmod +x ./Lprobe 73 | ``` 74 | 75 | 2. Update your Docker Compose file to mount the Lprobe binary: 76 | ``` 77 | services: 78 | nginx: 79 | image: nginx:latest 80 | volumes: 81 | - ./Lprobe:/Lprobe 82 | healthcheck: 83 | test: /Lprobe -mode=http -port=80 -endpoint=/ 84 | interval: 60s 85 | retries: 3 86 | start_period: 10s 87 | timeout: 3s 88 | ``` 89 | 90 | # Dev Guide 91 | ``` 92 | export GO111MODULE=on 93 | go mod init lprobe 94 | go mod tidy 95 | go run . 96 | ``` 97 | 98 | ``` 99 | go mod edit -go=1.21 100 | go get -u -t -x ./... 101 | go mod tidy 102 | ``` 103 | 104 | # Source code used 105 | - https://github.com/grpc-ecosystem/grpc-health-probe 106 | 107 | 108 | 109 | ## Weekly review link 110 | 111 | - [Review](https://github.com/fivexl/lprobe/compare/main@%7B7day%7D...main) 112 | - [Review branch-based review](https://github.com/fivexl/lprobe/compare/review...main) 113 | -------------------------------------------------------------------------------- /examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | ARG LPROBE_VERSION=v0.0.6 3 | ARG TARGETPLATFORM 4 | RUN case ${TARGETPLATFORM} in \ 5 | "linux/amd64") LPROBE_ARCH=amd64 ;; \ 6 | "linux/arm64") LPROBE_ARCH=arm64 ;; \ 7 | esac \ 8 | && wget -qO/bin/lprobe https://github.com/fivexl/lprobe/releases/download/${LPROBE_VERSION}/lprobe-linux-${LPROBE_ARCH} \ 9 | && chmod +x /bin/lprobe \ 10 | && rm -f /usr/bin/wget 11 | HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 CMD [ "lprobe", "-mode=http", "-port=80", "-endpoint=/" ] 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module lprobe 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/spiffe/go-spiffe/v2 v2.5.0 9 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 10 | google.golang.org/grpc v1.72.0 11 | ) 12 | 13 | require ( 14 | github.com/Microsoft/go-winio v0.6.2 // indirect 15 | github.com/go-jose/go-jose/v4 v4.1.0 // indirect 16 | github.com/zeebo/errs v1.4.0 // indirect 17 | golang.org/x/net v0.39.0 // indirect 18 | golang.org/x/sync v0.13.0 // indirect 19 | golang.org/x/sys v0.32.0 // indirect 20 | golang.org/x/text v0.24.0 // indirect 21 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect 22 | google.golang.org/protobuf v1.36.6 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 2 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY= 6 | github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw= 7 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 8 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 9 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 10 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 11 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 12 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 13 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 14 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= 20 | github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 21 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 22 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 23 | github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= 24 | github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= 25 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 26 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 27 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 28 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 29 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 30 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 31 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 32 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 33 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 34 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 35 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 36 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 37 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= 38 | golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= 39 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 40 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 41 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 42 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 43 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 44 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 45 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 46 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f h1:N/PrbTw4kdkqNRzVfWPrBekzLuarFREcbFOiOLkXon4= 48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 49 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 50 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 51 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 52 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /grpc_healthcheck.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "strconv" 19 | "context" 20 | "log" 21 | "time" 22 | 23 | "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" 24 | "github.com/spiffe/go-spiffe/v2/workloadapi" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/codes" 27 | "google.golang.org/grpc/credentials" 28 | "google.golang.org/grpc/credentials/alts" 29 | "google.golang.org/grpc/credentials/insecure" 30 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 31 | "google.golang.org/grpc/metadata" 32 | "google.golang.org/grpc/status" 33 | ) 34 | 35 | //nolint:all 36 | func grpcHealthCheck() (string) { 37 | status, code := grpchealthprobe(getAddr() + ":" + strconv.Itoa(flPort)) 38 | if code != 0 { 39 | // Error status 40 | return status 41 | } 42 | return "" 43 | } 44 | 45 | func grpchealthprobe(flAddr string) (string, int) { 46 | 47 | ctx, cancel := context.WithCancel(context.Background()) 48 | defer cancel() 49 | 50 | 51 | opts := []grpc.DialOption{ 52 | grpc.WithUserAgent(flUserAgent), 53 | grpc.WithConnectParams(grpc.ConnectParams{ 54 | MinConnectTimeout: flConnTimeout, 55 | }), 56 | } 57 | if flTLS && flSPIFFE { 58 | log.Printf("-tls and -spiffe are mutually incompatible") 59 | return "ERR", StatusInvalidArguments 60 | } 61 | if flTLS { 62 | creds, _ , err := buildCredentials(flTLSNoVerify, flTLSCACert, flTLSClientCert, flTLSClientKey, flTLSServerName) 63 | if err != nil { 64 | log.Printf("failed to initialize tls credentials. error=%v", err) 65 | return "ERR", StatusInvalidArguments 66 | } 67 | opts = append(opts, grpc.WithTransportCredentials(creds)) 68 | } else if flALTS { 69 | creds := alts.NewServerCreds(alts.DefaultServerOptions()) 70 | opts = append(opts, grpc.WithTransportCredentials(creds)) 71 | } else if flSPIFFE { 72 | spiffeCtx, cancel := context.WithTimeout(ctx, flRPCTimeout) 73 | defer cancel() 74 | source, err := workloadapi.NewX509Source(spiffeCtx) 75 | if err != nil { 76 | log.Printf("failed to initialize tls credentials with spiffe. error=%v", err) 77 | return "ERR", StatusSpiffeFailed 78 | } 79 | if flVerbose { 80 | svid, err := source.GetX509SVID() 81 | if err != nil { 82 | log.Fatalf("error getting x509 svid: %+v", err) 83 | } 84 | log.Printf("SPIFFE Verifiable Identity Document (SVID): %q", svid.ID) 85 | } 86 | creds := credentials.NewTLS(tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeAny())) 87 | opts = append(opts, grpc.WithTransportCredentials(creds)) 88 | } else { 89 | opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 90 | } 91 | 92 | if flGZIP { 93 | opts = append(opts, 94 | grpc.WithCompressor(grpc.NewGZIPCompressor()), //nolint:all 95 | grpc.WithDecompressor(grpc.NewGZIPDecompressor()), //nolint:all 96 | ) 97 | } 98 | 99 | if flVerbose { 100 | log.Print("establishing connection") 101 | } 102 | connStart := time.Now() 103 | conn, err := grpc.NewClient( 104 | flAddr, 105 | opts..., 106 | ) 107 | if err != nil { 108 | if err == context.DeadlineExceeded { 109 | log.Printf("timeout: failed to connect service %q within %v", flAddr, flConnTimeout) 110 | } else { 111 | log.Printf("error: failed to connect service at %q: %+v", flAddr, err) 112 | } 113 | return "ERR", StatusConnectionFailure 114 | } 115 | connDuration := time.Since(connStart) 116 | defer conn.Close() 117 | if flVerbose { 118 | log.Printf("connection established (took %v)", connDuration) 119 | } 120 | 121 | rpcStart := time.Now() 122 | rpcCtx, rpcCancel := context.WithTimeout(ctx, flRPCTimeout) 123 | defer rpcCancel() 124 | rpcCtx = metadata.NewOutgoingContext(rpcCtx, flRPCHeaders.MD) 125 | resp, err := healthpb.NewHealthClient(conn).Check(rpcCtx, 126 | &healthpb.HealthCheckRequest{ 127 | Service: flService}) 128 | if err != nil { 129 | if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented { 130 | log.Printf("error: this server does not implement the grpc health protocol (grpc.health.v1.Health): %s", stat.Message()) 131 | } else if stat, ok := status.FromError(err); ok && stat.Code() == codes.DeadlineExceeded { 132 | log.Printf("timeout: health rpc did not complete within %v", flRPCTimeout) 133 | } else { 134 | log.Printf("error: health rpc failed: %+v", err) 135 | } 136 | return "ERR", StatusRPCFailure 137 | } 138 | rpcDuration := time.Since(rpcStart) 139 | 140 | if resp.GetStatus() != healthpb.HealthCheckResponse_SERVING { 141 | return resp.GetStatus().String(), StatusUnhealthy 142 | } 143 | if flVerbose { 144 | log.Printf("time elapsed: connect=%v rpc=%v", connDuration, rpcDuration) 145 | } 146 | return resp.GetStatus().String(), 0 147 | } 148 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | 9 | "google.golang.org/grpc/credentials" 10 | ) 11 | 12 | func buildCredentials(skipVerify bool, caCerts, clientCert, clientKey, serverName string) (credentials.TransportCredentials, *tls.Config, error) { 13 | var cfg tls.Config 14 | 15 | if clientCert != "" && clientKey != "" { 16 | keyPair, err := tls.LoadX509KeyPair(clientCert, clientKey) 17 | if err != nil { 18 | return nil, &cfg, fmt.Errorf("failed to load tls client cert/key pair. error=%v", err) 19 | } 20 | cfg.Certificates = []tls.Certificate{keyPair} 21 | } 22 | 23 | if skipVerify { 24 | cfg.InsecureSkipVerify = true 25 | } else if caCerts != "" { 26 | // override system roots 27 | rootCAs := x509.NewCertPool() 28 | pem, err := os.ReadFile(caCerts) 29 | if err != nil { 30 | return nil, &cfg, fmt.Errorf("failed to load root CA certificates from file (%s) error=%v", caCerts, err) 31 | } 32 | if !rootCAs.AppendCertsFromPEM(pem) { 33 | return nil, &cfg, fmt.Errorf("no root CA certs parsed from file %s", caCerts) 34 | } 35 | cfg.RootCAs = rootCAs 36 | } 37 | if serverName != "" { 38 | cfg.ServerName = serverName 39 | } 40 | return credentials.NewTLS(&cfg), &cfg, nil 41 | } 42 | 43 | -------------------------------------------------------------------------------- /http_healthcheck.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "log" 7 | "strings" 8 | "fmt" 9 | "sort" 10 | ) 11 | 12 | func httpHealthCheck() (error) { 13 | 14 | var endpoint string 15 | var protocol string 16 | var validHTTPCodes map[int]bool 17 | var httpTransport *http.Transport 18 | 19 | if strings.HasPrefix(flEndpoint, "/") { 20 | endpoint = flEndpoint 21 | } else { 22 | endpoint = "/" + flEndpoint 23 | } 24 | 25 | validHTTPCodes = make(map[int]bool) 26 | ranges := strings.Split(flHTTPCodes, ",") 27 | for _, r := range ranges { 28 | if strings.Contains(r, "-") { 29 | parts := strings.Split(r, "-") 30 | start, _ := strconv.Atoi(parts[0]) 31 | end, _ := strconv.Atoi(parts[1]) 32 | for i := start; i <= end; i++ { 33 | validHTTPCodes[i] = true 34 | } 35 | } else { 36 | code, _ := strconv.Atoi(r) 37 | validHTTPCodes[code] = true 38 | } 39 | } 40 | 41 | // print validHTTPCodes map for debug 42 | if flVerbose { 43 | validCodes := make([]int, 0, len(validHTTPCodes)) 44 | for k := range validHTTPCodes { 45 | validCodes = append(validCodes, k) 46 | } 47 | sort.Ints(validCodes) // Sorting the valid codes for readability 48 | log.Printf("Valid HTTP Codes: %v\n", validCodes) 49 | } 50 | 51 | if flTLS { 52 | protocol = "https" 53 | _, creds, err := buildCredentials(flTLSNoVerify, flTLSCACert, flTLSClientCert, flTLSClientKey, flTLSServerName) 54 | if err != nil { 55 | log.Printf("failed to initialize tls credentials. error=%v", err) 56 | return err 57 | } 58 | httpTransport = &http.Transport{ 59 | TLSClientConfig: creds, 60 | } 61 | } else { 62 | protocol = "http" 63 | httpTransport = &http.Transport{} 64 | } 65 | 66 | httpClient := &http.Client{ 67 | Transport: httpTransport, 68 | Timeout: flConnTimeout, 69 | } 70 | 71 | // initialize http request 72 | url := protocol + "://" + getAddr() + ":" + strconv.Itoa(flPort) + endpoint 73 | req, reqErr := http.NewRequest("GET", url, nil) 74 | if reqErr != nil { 75 | log.Printf("failed to initialize http request. error=%v", reqErr) 76 | return reqErr 77 | } 78 | req.Header.Set("User-Agent", flUserAgent) 79 | 80 | // execute http request 81 | httpResponse, respErr := httpClient.Do(req) 82 | if respErr != nil { 83 | log.Printf("failed to execute http request. error=%v", respErr) 84 | return respErr 85 | } 86 | defer httpResponse.Body.Close() 87 | 88 | // check http response code 89 | if !validHTTPCodes[httpResponse.StatusCode] { 90 | log.Printf("HTTP request returned status %v", httpResponse.StatusCode) 91 | return fmt.Errorf("unexpected HTTP status code: %v", httpResponse.StatusCode) 92 | } 93 | 94 | return nil 95 | 96 | } 97 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log" 21 | "os" 22 | "strings" 23 | "time" 24 | "unicode" 25 | 26 | "golang.org/x/exp/slices" 27 | "google.golang.org/grpc/metadata" 28 | ) 29 | 30 | var ( 31 | flMode string 32 | flIPv6 bool 33 | flPort int 34 | flUserAgent string 35 | flEndpoint string 36 | flHTTPCodes string 37 | flService string 38 | flConnTimeout time.Duration 39 | flRPCHeaders = rpcHeaders{MD: make(metadata.MD)} 40 | flRPCTimeout time.Duration 41 | flTLS bool 42 | flTLSNoVerify bool 43 | flTLSCACert string 44 | flTLSClientCert string 45 | flTLSClientKey string 46 | flTLSServerName string 47 | flALTS bool 48 | flVerbose bool 49 | flGZIP bool 50 | flSPIFFE bool 51 | ) 52 | 53 | const ( 54 | // LocalAddress to call 55 | LocalAddress = "127.0.0.1" 56 | // LocalAddress6 IPv6 to call 57 | LocalAddress6 = "[::1]" 58 | // StatusInvalidArguments indicates specified invalid arguments. 59 | StatusInvalidArguments = 1 60 | // StatusConnectionFailure indicates connection failed. 61 | StatusConnectionFailure = 2 62 | // StatusRPCFailure indicates rpc failed. 63 | StatusRPCFailure = 3 64 | // StatusUnhealthy indicates rpc succeeded but indicates unhealthy service. 65 | StatusUnhealthy = 4 66 | // StatusSpiffeFailed indicates failure to retrieve credentials using spiffe workload API 67 | StatusSpiffeFailed = 20 68 | ) 69 | 70 | func getSupportedModes() []string { 71 | return []string{"http", "grpc"} 72 | } 73 | 74 | func getAddr() string { 75 | if flIPv6 { 76 | return LocalAddress6 77 | } 78 | return LocalAddress 79 | } 80 | 81 | func init() { 82 | flagSet := flag.NewFlagSet("", flag.ContinueOnError) 83 | log.SetFlags(0) 84 | // core settings 85 | flagSet.StringVar(&flMode, "mode", "http", "Select mode: http, grpc (default: http)") 86 | flagSet.BoolVar(&flIPv6, "ipv6", false, "Use IPv6 ::1 address (default: false)") 87 | flagSet.IntVar(&flPort, "port", 8080, "port number to check (defaut 8080)") 88 | flagSet.StringVar(&flUserAgent, "user-agent", "lprobe", "user-agent header value of health check requests") 89 | // HTTP settings 90 | flagSet.StringVar(&flEndpoint, "endpoint", "/", "HTTP endpoint (default: /)") 91 | flagSet.StringVar(&flHTTPCodes, "http-codes", "200-299", "comma-separated list of expected HTTP status codes (default: 200-299) with optional ranges (e.g. 200,201,202-204,301-303") 92 | // gRPC settings 93 | flagSet.StringVar(&flService, "service", "", "service name to check (default: \"\")") 94 | // timeouts 95 | flagSet.DurationVar(&flConnTimeout, "connect-timeout", time.Second, "timeout for establishing connection") 96 | // headers 97 | flagSet.Var(&flRPCHeaders, "rpc-header", "additional RPC headers in 'name: value' format. May specify more than one via multiple flags.") 98 | flagSet.DurationVar(&flRPCTimeout, "rpc-timeout", time.Second, "timeout for health check rpc") 99 | // tls settings 100 | flagSet.BoolVar(&flTLS, "tls", false, "use TLS (default: false, INSECURE plaintext transport)") 101 | flagSet.BoolVar(&flTLSNoVerify, "tls-no-verify", false, "(with -tls) don't verify the certificate (INSECURE) presented by the server (default: false)") 102 | flagSet.StringVar(&flTLSCACert, "tls-ca-cert", "", "(with -tls, optional) file containing trusted certificates for verifying server") 103 | flagSet.StringVar(&flTLSClientCert, "tls-client-cert", "", "(with -tls, optional) client certificate for authenticating to the server (requires -tls-client-key)") 104 | flagSet.StringVar(&flTLSClientKey, "tls-client-key", "", "(with -tls) client private key for authenticating to the server (requires -tls-client-cert)") 105 | flagSet.StringVar(&flTLSServerName, "tls-server-name", "", "(with -tls) override the hostname used to verify the server certificate") 106 | flagSet.BoolVar(&flALTS, "alts", false, "use ALTS (default: false, INSECURE plaintext transport)") 107 | flagSet.BoolVar(&flVerbose, "v", false, "verbose logs") 108 | flagSet.BoolVar(&flGZIP, "gzip", false, "use GZIPCompressor for requests and GZIPDecompressor for response (default: false)") 109 | flagSet.BoolVar(&flSPIFFE, "spiffe", false, "use SPIFFE to obtain mTLS credentials") 110 | 111 | err := flagSet.Parse(os.Args[1:]) 112 | if err != nil { 113 | os.Exit(StatusInvalidArguments) 114 | } 115 | 116 | argError := func(s string, v ...interface{}) { 117 | log.Printf("error: "+s, v...) 118 | os.Exit(StatusInvalidArguments) 119 | } 120 | 121 | if !slices.Contains(getSupportedModes(), flMode) { 122 | argError("Unsupported -mode. Please use one of %v", getSupportedModes()) 123 | } 124 | if flConnTimeout <= 0 { 125 | argError("-connect-timeout must be greater than zero (specified: %v)", flConnTimeout) 126 | } 127 | if flRPCTimeout <= 0 { 128 | argError("-rpc-timeout must be greater than zero (specified: %v)", flRPCTimeout) 129 | } 130 | if flALTS && flSPIFFE { 131 | argError("-alts and -spiffe are mutually incompatible") 132 | } 133 | if flTLS && flALTS { 134 | argError("cannot specify -tls with -alts") 135 | } 136 | if !flTLS && flTLSNoVerify { 137 | argError("specified -tls-no-verify without specifying -tls") 138 | } 139 | if !flTLS && flTLSCACert != "" { 140 | argError("specified -tls-ca-cert without specifying -tls") 141 | } 142 | if !flTLS && flTLSClientCert != "" { 143 | argError("specified -tls-client-cert without specifying -tls") 144 | } 145 | if !flTLS && flTLSServerName != "" { 146 | argError("specified -tls-server-name without specifying -tls") 147 | } 148 | if flTLSClientCert != "" && flTLSClientKey == "" { 149 | argError("specified -tls-client-cert without specifying -tls-client-key") 150 | } 151 | if flTLSClientCert == "" && flTLSClientKey != "" { 152 | argError("specified -tls-client-key without specifying -tls-client-cert") 153 | } 154 | if flTLSNoVerify && flTLSCACert != "" { 155 | argError("cannot specify -tls-ca-cert with -tls-no-verify (CA cert would not be used)") 156 | } 157 | if flTLSNoVerify && flTLSServerName != "" { 158 | argError("cannot specify -tls-server-name with -tls-no-verify (server name would not be used)") 159 | } 160 | 161 | if flVerbose { 162 | log.Printf("parsed options:") 163 | log.Printf("> conn_timeout=%v rpc_timeout=%v", flConnTimeout, flRPCTimeout) 164 | if flRPCHeaders.Len() > 0 { 165 | log.Printf("> headers: %s", flRPCHeaders) 166 | } 167 | log.Printf("> tls=%v", flTLS) 168 | if flTLS { 169 | log.Printf(" > no-verify=%v ", flTLSNoVerify) 170 | log.Printf(" > ca-cert=%s", flTLSCACert) 171 | log.Printf(" > client-cert=%s", flTLSClientCert) 172 | log.Printf(" > client-key=%s", flTLSClientKey) 173 | log.Printf(" > server-name=%s", flTLSServerName) 174 | } 175 | log.Printf("> alts=%v", flALTS) 176 | log.Printf("> spiffe=%v", flSPIFFE) 177 | } 178 | } 179 | 180 | type rpcHeaders struct{ metadata.MD } 181 | 182 | func (s *rpcHeaders) String() string { return fmt.Sprintf("%v", s.MD) } 183 | 184 | func (s *rpcHeaders) Set(value string) error { 185 | parts := strings.SplitN(value, ":", 2) 186 | if len(parts) != 2 { 187 | return fmt.Errorf("invalid RPC header, expected 'key: value', got %q", value) 188 | } 189 | trimmed := strings.TrimLeftFunc(parts[1], unicode.IsSpace) 190 | s.Append(parts[0], trimmed) 191 | return nil 192 | } 193 | 194 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | 10 | if flMode == "http" { 11 | // HTTP check 12 | err := httpHealthCheck() 13 | if err != nil { 14 | fmt.Printf("Error: %v", err) 15 | os.Exit(1) 16 | } 17 | } else if flMode == "grpc" { 18 | // gRPC check 19 | status := grpcHealthCheck() 20 | if status != "" { 21 | fmt.Printf("Error: %v", status) 22 | os.Exit(1) 23 | } 24 | os.Exit(0) 25 | } else { 26 | // unkown check 27 | fmt.Printf("Error: Unsupported -mode. Please use one of %v", getSupportedModes()) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # set -e 4 | rm -rf ./lprobe 5 | go build 6 | 7 | ## Prepull latest images 8 | docker pull nginx 9 | docker pull grpc/java-example-hostname 10 | 11 | ## Run docker containers 12 | docker run --rm -p 8080:80 -d --name nginx-lprobe-test nginx 13 | docker run --rm -p 8081:50051 -d --name grpc-lprobe-test grpc/java-example-hostname 14 | echo "Wait 5s" 15 | sleep 5 16 | 17 | ### HTTP Check Test 18 | ./lprobe -mode=http -port=8080 -endpoint=/ -http-codes=200-298,299 -v 19 | if [ "$?" != 0 ]; then 20 | echo "HTTP test failed" 21 | docker stop nginx-lprobe-test 22 | exit 1 23 | fi 24 | 25 | ### HTTP IPv6 Check Test 26 | ./lprobe -mode=http -port=8080 -endpoint=/ -ipv6 -v 27 | if [ "$?" != 0 ]; then 28 | echo "HTTP IPv6 test failed" 29 | docker stop nginx-lprobe-test 30 | exit 1 31 | fi 32 | 33 | ### gRPC Check Test 34 | ./lprobe -mode=grpc -port=8081 -v 35 | if [ "$?" != 0 ]; then 36 | echo "gRPC test failed" 37 | docker stop grpc-lprobe-test 38 | exit 1 39 | fi 40 | 41 | ### gRPC IPv6 Check Test 42 | ./lprobe -mode=grpc -port=8081 -ipv6 -v 43 | if [ "$?" != 0 ]; then 44 | echo "gRPC IPv6 test failed" 45 | docker stop grpc-lprobe-test 46 | exit 1 47 | fi 48 | 49 | 50 | ### FAIL HTTP Check Test 51 | ./lprobe -mode=http -port=7777 -endpoint=/ -v 52 | if [ "$?" != 1 ]; then 53 | echo "FAIL HTTP test failed" 54 | docker stop nginx-lprobe-test 55 | exit 1 56 | fi 57 | 58 | ### FAIL gRPC Check Test 59 | ./lprobe -mode=grpc -port=7777 -v 60 | if [ "$?" != 1 ]; then 61 | echo "FAIL gRPC test failed" 62 | docker stop grpc-lprobe-test 63 | exit 1 64 | fi 65 | 66 | ## Stop docker containers 67 | docker stop nginx-lprobe-test 68 | docker stop grpc-lprobe-test 69 | 70 | echo "All good" 71 | exit 0 --------------------------------------------------------------------------------