├── .earthlyignore ├── .github └── workflows │ ├── go.yml │ └── release-please.yml ├── .gitignore ├── CHANGELOG.md ├── Earthfile ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── go.mod ├── go.sum ├── internal └── tools │ ├── go.mod │ ├── go.sum │ ├── tools.go │ └── vet │ └── main.go ├── peers ├── README.md ├── constants.go ├── constants_string.go ├── e2e_test.go ├── example │ ├── dump │ │ ├── go.mod │ │ └── main.go │ └── prometheus-exporter │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go ├── go.sum ├── handler.go ├── messages.go ├── peers.go ├── protocol.go └── sticktable │ ├── constants.go │ ├── constants_string.go │ ├── sticktables.go │ ├── sticktables_test.go │ ├── values.go │ └── values_test.go ├── pkg ├── buffer │ └── slicebuf.go ├── encoding │ ├── actionwriter.go │ ├── actionwriter_test.go │ ├── encoding.go │ ├── kvscanner.go │ ├── kvwriter.go │ ├── kvwriter_test.go │ ├── messagescanner.go │ ├── varint.go │ └── varint_test.go └── testutil │ ├── allocs.go │ ├── allocs_test.go │ ├── haproxy.go │ ├── net.go │ ├── pipeconn.go │ ├── pipeconn_test.go │ ├── reader.go │ ├── reader_test.go │ ├── subprocess.go │ └── testfile.go ├── spop ├── README.md ├── agent.go ├── benchmarks │ ├── README.md │ ├── benchmarks_test.go │ ├── go.mod │ └── go.sum ├── e2e_test.go ├── error.go ├── example │ └── header-to-body │ │ ├── engine.cfg │ │ ├── go.mod │ │ ├── go.sum │ │ ├── haproxy.cfg │ │ └── main.go ├── frame.go ├── frames.go ├── go.sum ├── handler.go ├── protocol.go ├── scheduler.go └── server_test.go └── staticcheck.conf /.earthlyignore: -------------------------------------------------------------------------------- 1 | go.work 2 | go.work.sum -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: '1.21' 15 | 16 | - uses: dominikh/staticcheck-action@v1.3.0 17 | with: 18 | install-go: false 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.21' 29 | 30 | - name: Build 31 | run: go build -v ./... 32 | 33 | generate: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v3 37 | 38 | - name: Set up Go 39 | uses: actions/setup-go@v4 40 | with: 41 | go-version: '1.21' 42 | 43 | - name: Run go generate 44 | run: | 45 | cd internal/tools 46 | go install golang.org/x/tools/cmd/stringer 47 | cd ../.. 48 | 49 | go generate ./... 50 | git diff --exit-code || (echo "Generated files have changed. Please run 'go generate' and commit the changes." && exit 1) 51 | 52 | - name: Validate 53 | run: | 54 | go mod tidy 55 | git diff --exit-code || (echo "go.mod or go.sum have changed. Please run 'go mod tidy' and commit the changes." && exit 1) 56 | 57 | test: 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: earthly/actions-setup@v1 61 | with: 62 | github-token: ${{ secrets.GITHUB_TOKEN }} 63 | version: "latest" 64 | 65 | - uses: actions/checkout@v3 66 | 67 | - name: Test E2E 68 | run: earthly +test -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | issues: write 10 | 11 | name: release-please 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: googleapis/release-please-action@v4 18 | with: 19 | # this is not recommended, but we don't run any actions on release 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | release-type: simple -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,direnv 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,direnv 3 | 4 | ### direnv ### 5 | .direnv 6 | .envrc 7 | 8 | ### Go ### 9 | # If you prefer the allow list template instead of the deny list, see community template: 10 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 11 | # 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Dependency directories (remove the comment below to include it) 26 | # vendor/ 27 | 28 | # Go workspace file 29 | go.work 30 | 31 | ### Go Patch ### 32 | /vendor/ 33 | /Godeps/ 34 | 35 | # End of https://www.toptal.com/developers/gitignore/api/go,direnv 36 | 37 | ### Intellij ### 38 | 39 | .idea 40 | *.iml 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.0.7](https://github.com/DropMorePackets/haproxy-go/compare/v0.0.6...v0.0.7) (2025-06-05) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **spop:** Disable pipeline and async support ([c12e722](https://github.com/DropMorePackets/haproxy-go/commit/c12e722bc2171bd585d6613d08dcecfb4accbda7)) 9 | 10 | ## [0.0.6](https://github.com/DropMorePackets/haproxy-go/compare/v0.0.5...v0.0.6) (2025-05-30) 11 | 12 | ### CI 13 | * update staticcheck config 14 | * add release-please 15 | * allow release-please to access issues 16 | 17 | ### Bug Fixes 18 | * **spop:** set write and read buffer to 64K 19 | * **spop:** don't let panics take the library or workers out 20 | * **spop:** remove unused field lf 21 | * **peers:** update struct alignment to be more efficient ([5a12fb3](https://github.com/DropMorePackets/haproxy-go/commit/5a12fb36a131076baf277deee278f0a8a5894a3b)) 22 | -------------------------------------------------------------------------------- /Earthfile: -------------------------------------------------------------------------------- 1 | VERSION 0.8 2 | 3 | deps: 4 | FROM golang:1.23.1-alpine 5 | ENV CGO_ENABLED 0 6 | WORKDIR /src 7 | COPY . . 8 | RUN go work init 9 | RUN go work use . 10 | RUN go work use ./internal/tools 11 | RUN go work use ./peers 12 | RUN go work use ./spop 13 | RUN go mod download 14 | 15 | tools: 16 | FROM +deps 17 | 18 | RUN go install honnef.co/go/tools/cmd/staticcheck 19 | RUN go install golang.org/x/tools/cmd/stringer 20 | RUN go install ./internal/tools/vet 21 | 22 | validate: 23 | FROM +tools 24 | RUN $GOPATH/bin/staticcheck ./... 25 | RUN $GOPATH/bin/vet ./... 26 | 27 | go-test: 28 | FROM +deps 29 | RUN mkdir e2e 30 | FOR target IN './spop' './peers' './...' 31 | RUN go test $target 32 | RUN go test -tags e2e -o e2e -c $target 33 | END 34 | SAVE ARTIFACT e2e 35 | 36 | e2e: 37 | FROM haproxy:2.9-alpine 38 | COPY +go-test/e2e e2e 39 | FOR test IN $(ls ./e2e) 40 | RUN ./e2e/$test 41 | END 42 | 43 | test: 44 | BUILD +validate 45 | BUILD +e2e -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | Tim Windelschmidt ( / @fionera) is the main/default maintainer, some parts of the codebase have other maintainers. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HAProxy Protocols in Go 2 | 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go 2 | 3 | go 1.21 4 | 5 | require github.com/google/go-cmp v0.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | -------------------------------------------------------------------------------- /internal/tools/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go/internal/tools 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/dkorunic/betteralign v0.5.2 9 | golang.org/x/tools v0.25.0 10 | honnef.co/go/tools v0.5.1 11 | ) 12 | 13 | require ( 14 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 15 | github.com/google/renameio/v2 v2.0.0 // indirect 16 | github.com/sirkon/dst v0.26.4 // indirect 17 | golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 // indirect 18 | golang.org/x/mod v0.21.0 // indirect 19 | golang.org/x/sync v0.8.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /internal/tools/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 2 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/dkorunic/betteralign v0.5.2 h1:UhGUmh/ato5bWVSC7iKYWgiIDRnJy/JAqK/TzQRBpvY= 4 | github.com/dkorunic/betteralign v0.5.2/go.mod h1:hChMY9U+bWZKK3ZlWCQxP5hBwdKPbmuzSfVjeqo6VBM= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= 8 | github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= 9 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 10 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 11 | github.com/sirkon/dst v0.26.4 h1:ETxfjyp5JKE8OCpdybyyhzTyQqq/MwbIIcs7kxcUAcA= 12 | github.com/sirkon/dst v0.26.4/go.mod h1:e6HRc56jU5F2XT6GB8Cyci1Jb5cjX6gLqrm5+T/P7Zo= 13 | golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0 h1:bVwtbF629Xlyxk6xLQq2TDYmqP0uiWaet5LwRebuY0k= 14 | golang.org/x/exp/typeparams v0.0.0-20240909161429-701f63a606c0/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 15 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 16 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 17 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 18 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 19 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 20 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 21 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 22 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 23 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 24 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 25 | -------------------------------------------------------------------------------- /internal/tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "golang.org/x/tools/cmd/stringer" 7 | _ "honnef.co/go/tools/cmd/staticcheck" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/tools/vet/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/dkorunic/betteralign" 5 | "golang.org/x/tools/go/analysis/multichecker" 6 | "golang.org/x/tools/go/analysis/passes/appends" 7 | "golang.org/x/tools/go/analysis/passes/asmdecl" 8 | "golang.org/x/tools/go/analysis/passes/assign" 9 | "golang.org/x/tools/go/analysis/passes/atomic" 10 | "golang.org/x/tools/go/analysis/passes/atomicalign" 11 | "golang.org/x/tools/go/analysis/passes/bools" 12 | "golang.org/x/tools/go/analysis/passes/buildssa" 13 | "golang.org/x/tools/go/analysis/passes/buildtag" 14 | "golang.org/x/tools/go/analysis/passes/cgocall" 15 | "golang.org/x/tools/go/analysis/passes/composite" 16 | "golang.org/x/tools/go/analysis/passes/copylock" 17 | "golang.org/x/tools/go/analysis/passes/deepequalerrors" 18 | "golang.org/x/tools/go/analysis/passes/defers" 19 | "golang.org/x/tools/go/analysis/passes/directive" 20 | "golang.org/x/tools/go/analysis/passes/errorsas" 21 | "golang.org/x/tools/go/analysis/passes/framepointer" 22 | "golang.org/x/tools/go/analysis/passes/httpmux" 23 | "golang.org/x/tools/go/analysis/passes/httpresponse" 24 | "golang.org/x/tools/go/analysis/passes/ifaceassert" 25 | "golang.org/x/tools/go/analysis/passes/lostcancel" 26 | "golang.org/x/tools/go/analysis/passes/nilfunc" 27 | "golang.org/x/tools/go/analysis/passes/nilness" 28 | "golang.org/x/tools/go/analysis/passes/printf" 29 | "golang.org/x/tools/go/analysis/passes/reflectvaluecompare" 30 | "golang.org/x/tools/go/analysis/passes/shadow" 31 | "golang.org/x/tools/go/analysis/passes/shift" 32 | "golang.org/x/tools/go/analysis/passes/sigchanyzer" 33 | "golang.org/x/tools/go/analysis/passes/slog" 34 | "golang.org/x/tools/go/analysis/passes/sortslice" 35 | "golang.org/x/tools/go/analysis/passes/stdmethods" 36 | "golang.org/x/tools/go/analysis/passes/stringintconv" 37 | "golang.org/x/tools/go/analysis/passes/structtag" 38 | "golang.org/x/tools/go/analysis/passes/testinggoroutine" 39 | "golang.org/x/tools/go/analysis/passes/tests" 40 | "golang.org/x/tools/go/analysis/passes/timeformat" 41 | "golang.org/x/tools/go/analysis/passes/unmarshal" 42 | "golang.org/x/tools/go/analysis/passes/unreachable" 43 | "golang.org/x/tools/go/analysis/passes/unsafeptr" 44 | "golang.org/x/tools/go/analysis/passes/unusedresult" 45 | "golang.org/x/tools/go/analysis/passes/unusedwrite" 46 | ) 47 | 48 | func main() { 49 | multichecker.Main( 50 | appends.Analyzer, 51 | asmdecl.Analyzer, 52 | assign.Analyzer, 53 | atomic.Analyzer, 54 | atomicalign.Analyzer, 55 | bools.Analyzer, 56 | buildssa.Analyzer, 57 | buildtag.Analyzer, 58 | cgocall.Analyzer, 59 | composite.Analyzer, 60 | copylock.Analyzer, 61 | deepequalerrors.Analyzer, 62 | defers.Analyzer, 63 | directive.Analyzer, 64 | errorsas.Analyzer, 65 | framepointer.Analyzer, 66 | httpmux.Analyzer, 67 | httpresponse.Analyzer, 68 | ifaceassert.Analyzer, 69 | lostcancel.Analyzer, 70 | nilfunc.Analyzer, 71 | nilness.Analyzer, 72 | printf.Analyzer, 73 | reflectvaluecompare.Analyzer, 74 | shadow.Analyzer, 75 | shift.Analyzer, 76 | sigchanyzer.Analyzer, 77 | slog.Analyzer, 78 | sortslice.Analyzer, 79 | stdmethods.Analyzer, 80 | stringintconv.Analyzer, 81 | structtag.Analyzer, 82 | testinggoroutine.Analyzer, 83 | tests.Analyzer, 84 | timeformat.Analyzer, 85 | unmarshal.Analyzer, 86 | unreachable.Analyzer, 87 | unsafeptr.Analyzer, 88 | unusedresult.Analyzer, 89 | unusedwrite.Analyzer, 90 | 91 | betteralign.Analyzer, 92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /peers/README.md: -------------------------------------------------------------------------------- 1 | # A HAProxy peer protocol implementation in Go 2 | 3 | | ⚠️ This is still work in progress | 4 | |-----------------------------------------| 5 | 6 | # Applications 7 | 8 | - Prometheus Exporter 9 | - Reflector / Aggregator 10 | 11 | # References 12 | 13 | https://github.com/haproxy/haproxy/blob/master/doc/peers.txt 14 | https://github.com/haproxy/haproxy/blob/master/doc/peers-v2.0.txt 15 | https://github.com/haproxy/haproxy/blob/master/admin/wireshark-dissectors/peers/packet-happp.c 16 | https://github.com/haproxy/haproxy/blob/master/include/haproxy/stick_table-t.h 17 | https://github.com/haproxy/haproxy/blob/master/src/peers.c 18 | 19 | # Alternative implementations 20 | 21 | https://github.com/WoltLab/node-haproxy-peers 22 | -------------------------------------------------------------------------------- /peers/constants.go: -------------------------------------------------------------------------------- 1 | package peers 2 | 3 | //go:generate stringer -type HandshakeStatus,MessageClass,ControlMessageType,ErrorMessageType,StickTableUpdateMessageType -output=constants_string.go 4 | 5 | // HandshakeStatus represents the Handshake States 6 | // +-------------+---------------------------------+ 7 | // | status code | signification | 8 | // +-------------+---------------------------------+ 9 | // | 200 | Handshake succeeded | 10 | // +-------------+---------------------------------+ 11 | // | 300 | Try again later | 12 | // +-------------+---------------------------------+ 13 | // | 501 | Protocol error | 14 | // +-------------+---------------------------------+ 15 | // | 502 | Bad version | 16 | // +-------------+---------------------------------+ 17 | // | 503 | Local peer identifier mismatch | 18 | // +-------------+---------------------------------+ 19 | // | 504 | Remote peer identifier mismatch | 20 | // +-------------+---------------------------------+ 21 | type HandshakeStatus int 22 | 23 | const ( 24 | HandshakeStatusHandshakeSucceeded HandshakeStatus = 200 25 | HandshakeStatusTryAgainLater HandshakeStatus = 300 26 | HandshakeStatusProtocolError HandshakeStatus = 501 27 | HandshakeStatusBadVersion HandshakeStatus = 502 28 | HandshakeStatusLocalPeerIdentifierMismatch HandshakeStatus = 503 29 | HandshakeStatusRemotePeerIdentifierMismatch HandshakeStatus = 504 30 | ) 31 | 32 | // MessageClass represents the message classes. 33 | // There exist four classes of messages: 34 | // +------------+---------------------+--------------+ 35 | // | class byte | signification | message size | 36 | // +------------+---------------------+--------------+ 37 | // | 0 | control | fixed (2) | 38 | // +------------+---------------------+--------------| 39 | // | 1 | error | fixed (2) | 40 | // +------------+---------------------+--------------| 41 | // | 10 | stick-table updates | variable | 42 | // +------------+---------------------+--------------| 43 | // | 255 | reserved | | 44 | // +------------+---------------------+--------------+ 45 | type MessageClass byte 46 | 47 | // HAPPP message classes 48 | const ( 49 | // PEER_MSG_CLASS_CONTROL = 0, 50 | MessageClassControl MessageClass = iota 51 | // PEER_MSG_CLASS_ERROR, 52 | MessageClassError 53 | // PEER_MSG_CLASS_STICKTABLE = 0x0a, 54 | MessageClassStickTableUpdates MessageClass = 10 55 | // PEER_MSG_CLASS_RESERVED = 0xff, 56 | MessageClassReserved MessageClass = 255 57 | ) 58 | 59 | // ControlMessageType represents the control message types. 60 | // There exists five types of such control messages: 61 | // +------------+--------------------------------------------------------+ 62 | // | type byte | signification | 63 | // +------------+--------------------------------------------------------+ 64 | // | 0 | synchronisation request: ask a remote peer for a full | 65 | // | | synchronization | 66 | // +------------+--------------------------------------------------------+ 67 | // | 1 | synchronization finished: signal a remote peer that | 68 | // | | local updates have been pushed and local is considered | 69 | // | | up to date. | 70 | // +------------+--------------------------------------------------------+ 71 | // | 2 | synchronization partial: signal a remote peer that | 72 | // | | local updates have been pushed and local is not | 73 | // | | considered up to date. | 74 | // +------------+--------------------------------------------------------+ 75 | // | 3 | synchronization confirmed: acknowledge a finished or | 76 | // | | partial synchronization message. | 77 | // +------------+--------------------------------------------------------+ 78 | // | 4 | Heartbeat message. | 79 | // +------------+--------------------------------------------------------+ 80 | type ControlMessageType byte 81 | 82 | // Control messages 83 | const ( 84 | // PEER_MSG_CTRL_RESYNCREQ = 0, 85 | ControlMessageSyncRequest ControlMessageType = iota 86 | // PEER_MSG_CTRL_RESYNCFINISHED, 87 | ControlMessageSyncFinished 88 | // PEER_MSG_CTRL_RESYNCPARTIAL, 89 | ControlMessageSyncPartial 90 | // PEER_MSG_CTRL_RESYNCCONFIRM, 91 | ControlMessageSyncConfirmed 92 | // PEER_MSG_CTRL_HEARTBEAT, 93 | ControlMessageHeartbeat 94 | ) 95 | 96 | // ErrorMessageType represents the error message types. 97 | // There exits two types of such error messages: 98 | // +-----------+------------------+ 99 | // | type byte | signification | 100 | // +-----------+------------------+ 101 | // | 0 | protocol error | 102 | // +-----------+------------------+ 103 | // | 1 | size limit error | 104 | // +-----------+------------------+ 105 | type ErrorMessageType byte 106 | 107 | // Error messages 108 | const ( 109 | // PEER_MSG_ERR_PROTOCOL = 0, 110 | ErrorMessageProtocol ErrorMessageType = iota 111 | // PEER_MSG_ERR_SIZELIMIT, 112 | ErrorMessageSizeLimit 113 | ) 114 | 115 | // StickTableUpdateMessageType represents the stick-table update message types. 116 | // There exist five types of such stick-table update messages: 117 | // +-----------+--------------------------------+ 118 | // | type byte | signification | 119 | // +-----------+--------------------------------+ 120 | // | 128 | Entry update | 121 | // +-----------+--------------------------------+ 122 | // | 129 | Incremental entry update | 123 | // +-----------+--------------------------------+ 124 | // | 130 | Stick-table definition | 125 | // +-----------+--------------------------------+ 126 | // | 131 | Stick-table switch (unused) | 127 | // +-----------+--------------------------------+ 128 | // | 132 | Update message acknowledgement | 129 | // +-----------+--------------------------------+ 130 | type StickTableUpdateMessageType byte 131 | 132 | // Stick table messages 133 | const ( 134 | StickTableUpdateMessageTypeEntryUpdate StickTableUpdateMessageType = iota + 0x80 135 | StickTableUpdateMessageTypeIncrementalEntryUpdate 136 | StickTableUpdateMessageTypeStickTableDefinition 137 | StickTableUpdateMessageTypeStickTableSwitch 138 | StickTableUpdateMessageTypeUpdateAcknowledge 139 | StickTableUpdateMessageTypeUpdateTimed 140 | StickTableUpdateMessageTypeIncrementalEntryUpdateTimed 141 | ) 142 | -------------------------------------------------------------------------------- /peers/constants_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type HandshakeStatus,MessageClass,ControlMessageType,ErrorMessageType,StickTableUpdateMessageType -output=constants_string.go"; DO NOT EDIT. 2 | 3 | package peers 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[HandshakeStatusHandshakeSucceeded-200] 12 | _ = x[HandshakeStatusTryAgainLater-300] 13 | _ = x[HandshakeStatusProtocolError-501] 14 | _ = x[HandshakeStatusBadVersion-502] 15 | _ = x[HandshakeStatusLocalPeerIdentifierMismatch-503] 16 | _ = x[HandshakeStatusRemotePeerIdentifierMismatch-504] 17 | } 18 | 19 | const ( 20 | _HandshakeStatus_name_0 = "HandshakeStatusHandshakeSucceeded" 21 | _HandshakeStatus_name_1 = "HandshakeStatusTryAgainLater" 22 | _HandshakeStatus_name_2 = "HandshakeStatusProtocolErrorHandshakeStatusBadVersionHandshakeStatusLocalPeerIdentifierMismatchHandshakeStatusRemotePeerIdentifierMismatch" 23 | ) 24 | 25 | var ( 26 | _HandshakeStatus_index_2 = [...]uint8{0, 28, 53, 95, 138} 27 | ) 28 | 29 | func (i HandshakeStatus) String() string { 30 | switch { 31 | case i == 200: 32 | return _HandshakeStatus_name_0 33 | case i == 300: 34 | return _HandshakeStatus_name_1 35 | case 501 <= i && i <= 504: 36 | i -= 501 37 | return _HandshakeStatus_name_2[_HandshakeStatus_index_2[i]:_HandshakeStatus_index_2[i+1]] 38 | default: 39 | return "HandshakeStatus(" + strconv.FormatInt(int64(i), 10) + ")" 40 | } 41 | } 42 | func _() { 43 | // An "invalid array index" compiler error signifies that the constant values have changed. 44 | // Re-run the stringer command to generate them again. 45 | var x [1]struct{} 46 | _ = x[MessageClassControl-0] 47 | _ = x[MessageClassError-1] 48 | _ = x[MessageClassStickTableUpdates-10] 49 | _ = x[MessageClassReserved-255] 50 | } 51 | 52 | const ( 53 | _MessageClass_name_0 = "MessageClassControlMessageClassError" 54 | _MessageClass_name_1 = "MessageClassStickTableUpdates" 55 | _MessageClass_name_2 = "MessageClassReserved" 56 | ) 57 | 58 | var ( 59 | _MessageClass_index_0 = [...]uint8{0, 19, 36} 60 | ) 61 | 62 | func (i MessageClass) String() string { 63 | switch { 64 | case i <= 1: 65 | return _MessageClass_name_0[_MessageClass_index_0[i]:_MessageClass_index_0[i+1]] 66 | case i == 10: 67 | return _MessageClass_name_1 68 | case i == 255: 69 | return _MessageClass_name_2 70 | default: 71 | return "MessageClass(" + strconv.FormatInt(int64(i), 10) + ")" 72 | } 73 | } 74 | func _() { 75 | // An "invalid array index" compiler error signifies that the constant values have changed. 76 | // Re-run the stringer command to generate them again. 77 | var x [1]struct{} 78 | _ = x[ControlMessageSyncRequest-0] 79 | _ = x[ControlMessageSyncFinished-1] 80 | _ = x[ControlMessageSyncPartial-2] 81 | _ = x[ControlMessageSyncConfirmed-3] 82 | _ = x[ControlMessageHeartbeat-4] 83 | } 84 | 85 | const _ControlMessageType_name = "ControlMessageSyncRequestControlMessageSyncFinishedControlMessageSyncPartialControlMessageSyncConfirmedControlMessageHeartbeat" 86 | 87 | var _ControlMessageType_index = [...]uint8{0, 25, 51, 76, 103, 126} 88 | 89 | func (i ControlMessageType) String() string { 90 | if i >= ControlMessageType(len(_ControlMessageType_index)-1) { 91 | return "ControlMessageType(" + strconv.FormatInt(int64(i), 10) + ")" 92 | } 93 | return _ControlMessageType_name[_ControlMessageType_index[i]:_ControlMessageType_index[i+1]] 94 | } 95 | func _() { 96 | // An "invalid array index" compiler error signifies that the constant values have changed. 97 | // Re-run the stringer command to generate them again. 98 | var x [1]struct{} 99 | _ = x[ErrorMessageProtocol-0] 100 | _ = x[ErrorMessageSizeLimit-1] 101 | } 102 | 103 | const _ErrorMessageType_name = "ErrorMessageProtocolErrorMessageSizeLimit" 104 | 105 | var _ErrorMessageType_index = [...]uint8{0, 20, 41} 106 | 107 | func (i ErrorMessageType) String() string { 108 | if i >= ErrorMessageType(len(_ErrorMessageType_index)-1) { 109 | return "ErrorMessageType(" + strconv.FormatInt(int64(i), 10) + ")" 110 | } 111 | return _ErrorMessageType_name[_ErrorMessageType_index[i]:_ErrorMessageType_index[i+1]] 112 | } 113 | func _() { 114 | // An "invalid array index" compiler error signifies that the constant values have changed. 115 | // Re-run the stringer command to generate them again. 116 | var x [1]struct{} 117 | _ = x[StickTableUpdateMessageTypeEntryUpdate-128] 118 | _ = x[StickTableUpdateMessageTypeIncrementalEntryUpdate-129] 119 | _ = x[StickTableUpdateMessageTypeStickTableDefinition-130] 120 | _ = x[StickTableUpdateMessageTypeStickTableSwitch-131] 121 | _ = x[StickTableUpdateMessageTypeUpdateAcknowledge-132] 122 | _ = x[StickTableUpdateMessageTypeUpdateTimed-133] 123 | _ = x[StickTableUpdateMessageTypeIncrementalEntryUpdateTimed-134] 124 | } 125 | 126 | const _StickTableUpdateMessageType_name = "StickTableUpdateMessageTypeEntryUpdateStickTableUpdateMessageTypeIncrementalEntryUpdateStickTableUpdateMessageTypeStickTableDefinitionStickTableUpdateMessageTypeStickTableSwitchStickTableUpdateMessageTypeUpdateAcknowledgeStickTableUpdateMessageTypeUpdateTimedStickTableUpdateMessageTypeIncrementalEntryUpdateTimed" 127 | 128 | var _StickTableUpdateMessageType_index = [...]uint16{0, 38, 87, 134, 177, 221, 259, 313} 129 | 130 | func (i StickTableUpdateMessageType) String() string { 131 | i -= 128 132 | if i >= StickTableUpdateMessageType(len(_StickTableUpdateMessageType_index)-1) { 133 | return "StickTableUpdateMessageType(" + strconv.FormatInt(int64(i+128), 10) + ")" 134 | } 135 | return _StickTableUpdateMessageType_name[_StickTableUpdateMessageType_index[i]:_StickTableUpdateMessageType_index[i+1]] 136 | } 137 | -------------------------------------------------------------------------------- /peers/e2e_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package peers 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "testing" 11 | "time" 12 | 13 | "github.com/dropmorepackets/haproxy-go/peers/sticktable" 14 | "github.com/dropmorepackets/haproxy-go/pkg/testutil" 15 | ) 16 | 17 | func TestE2E(t *testing.T) { 18 | success := make(chan bool) 19 | a := Peer{Handler: HandlerFunc(func(_ context.Context, u *sticktable.EntryUpdate) { 20 | log.Println(u) 21 | success <- true 22 | })} 23 | 24 | // create the listener synchronously to prevent a race 25 | l := testutil.TCPListener(t) 26 | // ignore errors as the listener will be closed by t.Cleanup 27 | go a.Serve(l) 28 | 29 | cfg := testutil.HAProxyConfig{ 30 | FrontendPort: fmt.Sprintf("%d", testutil.TCPPort(t)), 31 | CustomFrontendConfig: ` 32 | http-request track-sc0 src table st_src_global 33 | http-request track-sc2 req.hdr(Host) table st_be_name 34 | `, 35 | CustomConfig: ` 36 | backend st_be_name 37 | stick-table type string size 1m expire 10m store http_req_rate(10s) peers mypeers 38 | 39 | backend st_src_global 40 | stick-table type ip size 1m expire 10m store http_req_rate(10s),conn_rate(10s),bytes_in_rate(10s) peers mypeers 41 | `, 42 | PeerAddr: l.Addr().String(), 43 | } 44 | 45 | t.Run("receive update", func(t *testing.T) { 46 | cfg.Run(t) 47 | 48 | for i := 0; i < 10; i++ { 49 | _, err := http.Get("http://127.0.0.1:" + cfg.FrontendPort) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | } 54 | 55 | tm := time.NewTimer(1 * time.Second) 56 | defer tm.Stop() 57 | select { 58 | case v := <-success: 59 | if !v { 60 | t.Fail() 61 | } 62 | case <-tm.C: 63 | t.Error("timeout") 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /peers/example/dump/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go/peers/example/printer 2 | 3 | go 1.21 4 | 5 | replace github.com/dropmorepackets/haproxy-go => ../../../ 6 | 7 | require github.com/dropmorepackets/haproxy-go v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /peers/example/dump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/dropmorepackets/haproxy-go/peers" 8 | "github.com/dropmorepackets/haproxy-go/peers/sticktable" 9 | ) 10 | 11 | func main() { 12 | log.SetFlags(log.LstdFlags | log.Lshortfile) 13 | 14 | err := peers.ListenAndServe(":21000", peers.HandlerFunc(func(_ context.Context, u *sticktable.EntryUpdate) { 15 | log.Println(u.String()) 16 | })) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /peers/example/prometheus-exporter/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go/peers/example/prometheus-exporter 2 | 3 | go 1.21 4 | 5 | replace github.com/dropmorepackets/haproxy-go => ../../../ 6 | 7 | require ( 8 | github.com/dropmorepackets/haproxy-go v0.0.2 9 | github.com/prometheus/client_golang v1.17.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 15 | github.com/golang/protobuf v1.5.3 // indirect 16 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 17 | github.com/prometheus/client_model v0.5.0 // indirect 18 | github.com/prometheus/common v0.44.0 // indirect 19 | github.com/prometheus/procfs v0.12.0 // indirect 20 | golang.org/x/sys v0.13.0 // indirect 21 | google.golang.org/protobuf v1.31.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /peers/example/prometheus-exporter/go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 4 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 9 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 10 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 11 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 15 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 16 | github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 17 | github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 18 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= 19 | github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 20 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 21 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 22 | github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= 23 | github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 24 | github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= 25 | github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= 26 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 27 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 28 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 30 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 32 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 35 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 36 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 37 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 38 | -------------------------------------------------------------------------------- /peers/example/prometheus-exporter/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/promauto" 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | 12 | "github.com/dropmorepackets/haproxy-go/peers" 13 | "github.com/dropmorepackets/haproxy-go/peers/sticktable" 14 | ) 15 | 16 | var metric = promauto.NewGaugeVec(prometheus.GaugeOpts{ 17 | Namespace: "haproxy", 18 | Subsystem: "stick_table", 19 | Name: "data", 20 | Help: "", 21 | }, []string{"table", "type", "key"}) 22 | 23 | func main() { 24 | log.SetFlags(log.LstdFlags | log.Lshortfile) 25 | 26 | go http.ListenAndServe(":8081", promhttp.Handler()) 27 | 28 | err := peers.ListenAndServe(":21000", peers.HandlerFunc(func(_ context.Context, update *sticktable.EntryUpdate) { 29 | for i, d := range update.Data { 30 | dt := update.StickTable.DataTypes[i].DataType 31 | switch d := d.(type) { 32 | case *sticktable.FreqData: 33 | metric.WithLabelValues(update.StickTable.Name, dt.String(), update.Key.String()).Set(float64(d.LastPeriod)) 34 | case *sticktable.SignedIntegerData: 35 | metric.WithLabelValues(update.StickTable.Name, dt.String(), update.Key.String()).Set(float64(*d)) 36 | case *sticktable.UnsignedIntegerData: 37 | metric.WithLabelValues(update.StickTable.Name, dt.String(), update.Key.String()).Set(float64(*d)) 38 | case *sticktable.UnsignedLongLongData: 39 | metric.WithLabelValues(update.StickTable.Name, dt.String(), update.Key.String()).Set(float64(*d)) 40 | } 41 | } 42 | })) 43 | 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /peers/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dropmorepackets/haproxy-go v0.0.4 h1:bZQ8KHu00s/AyVNXW2kp9TKwpIWso2xTA3UfZmyxMGM= 2 | github.com/dropmorepackets/haproxy-go v0.0.4/go.mod h1:OGwftKhVqRvI1QtonOPCvPHKgDQLLaZpT2aF25ReQ2Q= 3 | -------------------------------------------------------------------------------- /peers/handler.go: -------------------------------------------------------------------------------- 1 | package peers 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dropmorepackets/haproxy-go/peers/sticktable" 7 | ) 8 | 9 | type Handler interface { 10 | HandleUpdate(context.Context, *sticktable.EntryUpdate) 11 | HandleHandshake(context.Context, *Handshake) 12 | Close() error 13 | } 14 | 15 | type HandlerFunc func(context.Context, *sticktable.EntryUpdate) 16 | 17 | func (HandlerFunc) Close() error { return nil } 18 | 19 | func (HandlerFunc) HandleHandshake(context.Context, *Handshake) {} 20 | 21 | func (h HandlerFunc) HandleUpdate(ctx context.Context, u *sticktable.EntryUpdate) { 22 | h(ctx, u) 23 | } 24 | 25 | var ( 26 | _ Handler = (HandlerFunc)(nil) 27 | ) 28 | -------------------------------------------------------------------------------- /peers/messages.go: -------------------------------------------------------------------------------- 1 | package peers 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | // Handshake is composed by these fields: 11 | // 12 | // protocol identifier : HAProxyS 13 | // version : 2.1 14 | // remote peer identifier: the peer name this "hello" message is sent to. 15 | // local peer identifier : the name of the peer which sends this "hello" message. 16 | // process ID : the ID of the process handling this peer session. 17 | // relative process ID : the haproxy's relative process ID (0 if nbproc == 1). 18 | type Handshake struct { 19 | ProtocolIdentifier string 20 | Version string 21 | RemotePeer string 22 | LocalPeerIdentifier string 23 | ProcessID int 24 | RelativeProcessID int 25 | } 26 | 27 | // NewHandshake returns a basic handshake to be used for connecting to 28 | // haproxy peers. It is filled with all necessary information except the remote 29 | // peer hostname. 30 | func NewHandshake(remotePeer string) *Handshake { 31 | return &Handshake{ 32 | ProtocolIdentifier: "HAProxyS", 33 | Version: "2.1", 34 | RemotePeer: remotePeer, 35 | LocalPeerIdentifier: func() string { 36 | s, _ := os.Hostname() 37 | return s 38 | }(), 39 | ProcessID: os.Getpid(), 40 | RelativeProcessID: 0, 41 | } 42 | } 43 | 44 | func (h *Handshake) ReadFrom(r io.Reader) (n int64, err error) { 45 | scanner := bufio.NewScanner(r) 46 | 47 | scanner.Scan() 48 | _, err = fmt.Sscanf(scanner.Text(), "%s %s", &h.ProtocolIdentifier, &h.Version) 49 | if err != nil { 50 | return -1, err 51 | } 52 | 53 | scanner.Scan() 54 | h.RemotePeer = scanner.Text() 55 | 56 | scanner.Scan() 57 | _, err = fmt.Sscanf(scanner.Text(), "%s %d %d", &h.LocalPeerIdentifier, &h.ProcessID, &h.RelativeProcessID) 58 | if err != nil { 59 | return -1, err 60 | } 61 | 62 | //TODO: find out how many bytes where read. 63 | return -1, scanner.Err() 64 | } 65 | 66 | func (h *Handshake) WriteTo(w io.Writer) (nw int64, err error) { 67 | n, err := fmt.Fprintf(w, "%s %s\n", h.ProtocolIdentifier, h.Version) 68 | nw += int64(n) 69 | if err != nil { 70 | return nw, err 71 | } 72 | 73 | n, err = fmt.Fprintf(w, "%s\n", h.RemotePeer) 74 | nw += int64(n) 75 | if err != nil { 76 | return nw, err 77 | } 78 | 79 | n, err = fmt.Fprintf(w, "%s %d %d\n", h.LocalPeerIdentifier, h.ProcessID, h.RelativeProcessID) 80 | nw += int64(n) 81 | 82 | return nw, err 83 | } 84 | -------------------------------------------------------------------------------- /peers/peers.go: -------------------------------------------------------------------------------- 1 | package peers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | ) 9 | 10 | type Peer struct { 11 | Handler Handler 12 | HandlerSource func() Handler 13 | BaseContext context.Context 14 | Addr string 15 | } 16 | 17 | func ListenAndServe(addr string, handler Handler) error { 18 | a := Peer{Addr: addr, Handler: handler} 19 | return a.ListenAndServe() 20 | } 21 | 22 | func (a *Peer) ListenAndServe() error { 23 | l, err := net.Listen("tcp", a.Addr) 24 | if err != nil { 25 | return fmt.Errorf("opening listener: %w", err) 26 | } 27 | defer l.Close() 28 | 29 | return a.Serve(l) 30 | } 31 | 32 | func (a *Peer) Serve(l net.Listener) error { 33 | a.Addr = l.Addr().String() 34 | if a.BaseContext == nil { 35 | a.BaseContext = context.Background() 36 | } 37 | 38 | go func() { 39 | <-a.BaseContext.Done() 40 | l.Close() 41 | }() 42 | 43 | if a.Handler != nil && a.HandlerSource != nil { 44 | return fmt.Errorf("cannot set Handler and HandlerSource at the same time") 45 | } 46 | 47 | if a.Handler != nil { 48 | a.HandlerSource = func() Handler { 49 | return a.Handler 50 | } 51 | } 52 | 53 | for { 54 | nc, err := l.Accept() 55 | if err != nil { 56 | return fmt.Errorf("accepting conn: %w", err) 57 | } 58 | 59 | // Wrap the context to provide access to the underlying connection. 60 | // TODO(tim): Do we really want this? 61 | ctx := context.WithValue(a.BaseContext, connectionKey, nc) 62 | p := newProtocolClient(ctx, nc, a.HandlerSource()) 63 | go func() { 64 | defer nc.Close() 65 | defer p.Close() 66 | 67 | if err := p.Serve(); err != nil && err != p.ctx.Err() { 68 | log.Println(err) 69 | } 70 | }() 71 | } 72 | } 73 | 74 | type contextKey string 75 | 76 | const ( 77 | connectionKey = contextKey("connection") 78 | ) 79 | 80 | // Connection returns the underlying connection used in calls 81 | // to function in a Handler. 82 | func Connection(ctx context.Context) net.Conn { 83 | return ctx.Value(connectionKey).(net.Conn) 84 | } 85 | -------------------------------------------------------------------------------- /peers/protocol.go: -------------------------------------------------------------------------------- 1 | package peers 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net" 11 | "time" 12 | 13 | "github.com/dropmorepackets/haproxy-go/peers/sticktable" 14 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 15 | ) 16 | 17 | type protocolClient struct { 18 | ctx context.Context 19 | ctxCancel context.CancelFunc 20 | rw io.ReadWriter 21 | br *bufio.Reader 22 | 23 | nextHeartbeat *time.Ticker 24 | lastMessageTimer *time.Timer 25 | lastTableDefinition *sticktable.Definition 26 | lastEntryUpdate *sticktable.EntryUpdate 27 | 28 | handler Handler 29 | } 30 | 31 | func newProtocolClient(ctx context.Context, rw io.ReadWriter, handler Handler) *protocolClient { 32 | var c protocolClient 33 | c.rw = rw 34 | c.br = bufio.NewReader(rw) 35 | c.handler = handler 36 | c.ctx, c.ctxCancel = context.WithCancel(ctx) 37 | return &c 38 | } 39 | 40 | func (c *protocolClient) Close() error { 41 | defer c.ctxCancel() 42 | if c.ctx.Err() != nil { 43 | return c.ctx.Err() 44 | } 45 | 46 | return c.handler.Close() 47 | } 48 | 49 | func (c *protocolClient) peerHandshake() error { 50 | var h Handshake 51 | if _, err := h.ReadFrom(c.br); err != nil { 52 | return err 53 | } 54 | 55 | c.handler.HandleHandshake(c.ctx, &h) 56 | 57 | if _, err := c.rw.Write([]byte(fmt.Sprintf("%d\n", HandshakeStatusHandshakeSucceeded))); err != nil { 58 | return fmt.Errorf("handshake failed: %v", err) 59 | } 60 | 61 | return nil 62 | } 63 | 64 | func (c *protocolClient) resetHeartbeat() { 65 | // a peer sends heartbeat messages to peers it is 66 | // connected to after periods of 3s of inactivity (i.e. when there is no 67 | // stick-table to synchronize for 3s). 68 | if c.nextHeartbeat == nil { 69 | c.nextHeartbeat = time.NewTicker(time.Second * 3) 70 | return 71 | } 72 | 73 | c.nextHeartbeat.Reset(time.Second * 3) 74 | } 75 | 76 | func (c *protocolClient) resetLastMessage() { 77 | // After a successful peer protocol handshake between two peers, 78 | // if one of them does not send any other peer 79 | // protocol messages (i.e. no heartbeat and no stick-table update messages) 80 | // during a 5s period, it is considered as no more alive by its remote peer 81 | // which closes the session and then tries to reconnect to the peer which 82 | // has just disappeared. 83 | if c.lastMessageTimer == nil { 84 | c.lastMessageTimer = time.NewTimer(time.Second * 5) 85 | return 86 | } 87 | 88 | c.lastMessageTimer.Reset(time.Second * 5) 89 | } 90 | 91 | func (c *protocolClient) heartbeat() { 92 | for range c.nextHeartbeat.C { 93 | _, err := c.rw.Write([]byte{byte(MessageClassControl), byte(ControlMessageHeartbeat)}) 94 | if err != nil { 95 | _ = c.Close() 96 | return 97 | } 98 | } 99 | } 100 | 101 | func (c *protocolClient) lastMessage() { 102 | <-c.lastMessageTimer.C 103 | log.Println("last message timer expired: closing connection") 104 | _ = c.Close() 105 | } 106 | 107 | func (c *protocolClient) Serve() error { 108 | if err := c.peerHandshake(); err != nil { 109 | return fmt.Errorf("handshake: %v", err) 110 | } 111 | 112 | c.resetHeartbeat() 113 | c.resetLastMessage() 114 | go c.heartbeat() 115 | go c.lastMessage() 116 | 117 | for { 118 | var m rawMessage 119 | 120 | if _, err := m.ReadFrom(c.br); err != nil { 121 | if c.ctx.Err() != nil { 122 | return c.ctx.Err() 123 | } 124 | 125 | if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { 126 | return nil 127 | } 128 | 129 | return fmt.Errorf("reading message: %v", err) 130 | } 131 | 132 | c.resetLastMessage() 133 | if err := c.messageHandler(&m); err != nil { 134 | return fmt.Errorf("message handler: %v", err) 135 | } 136 | } 137 | } 138 | 139 | func (c *protocolClient) messageHandler(m *rawMessage) error { 140 | switch m.MessageClass { 141 | case MessageClassControl: 142 | return ControlMessageType(m.MessageType).OnMessage(m, c) 143 | case MessageClassError: 144 | return ErrorMessageType(m.MessageType).OnMessage(m, c) 145 | case MessageClassStickTableUpdates: 146 | return StickTableUpdateMessageType(m.MessageType).OnMessage(m, c) 147 | default: 148 | return fmt.Errorf("unknown message class: %s", m.MessageClass) 149 | } 150 | } 151 | 152 | type byteReader interface { 153 | io.ByteReader 154 | io.Reader 155 | } 156 | 157 | type rawMessage struct { 158 | Data []byte 159 | 160 | MessageClass MessageClass 161 | MessageType byte 162 | } 163 | 164 | func (m *rawMessage) ReadFrom(r byteReader) (int64, error) { 165 | // All the messages are made at least of a two bytes length header. 166 | header := make([]byte, 2) 167 | n, err := io.ReadFull(r, header) 168 | if err != nil { 169 | return int64(n), err 170 | } 171 | 172 | m.MessageClass = MessageClass(header[0]) 173 | m.MessageType = header[1] 174 | 175 | var readData int 176 | // All messages with type >= 128 have a payload 177 | if m.MessageType >= 128 { 178 | dataLength, err := encoding.ReadVarint(r) 179 | if err != nil { 180 | return int64(n), fmt.Errorf("failed decoding data length: %v", err) 181 | } 182 | 183 | m.Data = make([]byte, dataLength) 184 | readData, err = io.ReadFull(r, m.Data) 185 | if err != nil { 186 | return int64(n + readData), fmt.Errorf("failed reading message data: %v", err) 187 | } 188 | if uint64(readData) != dataLength { 189 | return int64(n + readData), fmt.Errorf("invalid amount read: %d != %d", dataLength, readData) 190 | } 191 | } 192 | 193 | return int64(n + readData), nil 194 | } 195 | 196 | func (t ErrorMessageType) OnMessage(m *rawMessage, c *protocolClient) error { 197 | switch t { 198 | case ErrorMessageProtocol: 199 | return fmt.Errorf("protocol error") 200 | case ErrorMessageSizeLimit: 201 | return fmt.Errorf("message size limit") 202 | default: 203 | return fmt.Errorf("unknown error message type: %s", t) 204 | } 205 | } 206 | 207 | func (t ControlMessageType) OnMessage(m *rawMessage, c *protocolClient) error { 208 | switch t { 209 | case ControlMessageSyncRequest: 210 | _, _ = c.rw.Write([]byte{byte(MessageClassControl), byte(ControlMessageSyncPartial)}) 211 | return nil 212 | case ControlMessageSyncFinished: 213 | return nil 214 | case ControlMessageSyncPartial: 215 | return nil 216 | case ControlMessageSyncConfirmed: 217 | return nil 218 | case ControlMessageHeartbeat: 219 | return nil 220 | default: 221 | return fmt.Errorf("unknown control message type: %s", t) 222 | } 223 | } 224 | 225 | func (t StickTableUpdateMessageType) OnMessage(m *rawMessage, c *protocolClient) error { 226 | switch t { 227 | case StickTableUpdateMessageTypeStickTableDefinition: 228 | var std sticktable.Definition 229 | if _, err := std.Unmarshal(m.Data); err != nil { 230 | return err 231 | } 232 | c.lastTableDefinition = &std 233 | 234 | return nil 235 | case StickTableUpdateMessageTypeStickTableSwitch: 236 | log.Printf("not implemented: %s", t) 237 | return nil 238 | case StickTableUpdateMessageTypeUpdateAcknowledge: 239 | log.Printf("not implemented: %s", t) 240 | return nil 241 | case StickTableUpdateMessageTypeEntryUpdate, 242 | StickTableUpdateMessageTypeUpdateTimed, 243 | StickTableUpdateMessageTypeIncrementalEntryUpdate, 244 | StickTableUpdateMessageTypeIncrementalEntryUpdateTimed: 245 | // All entry update messages are handled in a separate switch case 246 | // following this one. 247 | break 248 | default: 249 | return fmt.Errorf("unknown stick-table update message type: %s", t) 250 | } 251 | 252 | if c.lastTableDefinition == nil { 253 | return fmt.Errorf("cannot process entry update without table definition") 254 | } 255 | 256 | e := sticktable.EntryUpdate{ 257 | StickTable: c.lastTableDefinition, 258 | } 259 | 260 | if c.lastEntryUpdate != nil { 261 | e.LocalUpdateID = c.lastEntryUpdate.LocalUpdateID + 1 262 | } 263 | 264 | switch t { 265 | case StickTableUpdateMessageTypeEntryUpdate: 266 | e.WithLocalUpdateID = true 267 | case StickTableUpdateMessageTypeUpdateTimed: 268 | e.WithLocalUpdateID = true 269 | e.WithExpiry = true 270 | case StickTableUpdateMessageTypeIncrementalEntryUpdate: 271 | case StickTableUpdateMessageTypeIncrementalEntryUpdateTimed: 272 | e.WithExpiry = true 273 | } 274 | 275 | if _, err := e.Unmarshal(m.Data); err != nil { 276 | return err 277 | } 278 | 279 | c.lastEntryUpdate = &e 280 | 281 | c.handler.HandleUpdate(c.ctx, &e) 282 | 283 | return nil 284 | } 285 | -------------------------------------------------------------------------------- /peers/sticktable/constants.go: -------------------------------------------------------------------------------- 1 | package sticktable 2 | 3 | import "strconv" 4 | 5 | //go:generate stringer -type KeyType -output constants_string.go 6 | 7 | type KeyType int 8 | 9 | // This is the different key types of the stick tables. 10 | // Same definitions as in HAProxy sources. 11 | const ( 12 | KeyTypeAny KeyType = iota 13 | KeyTypeBoolean 14 | KeyTypeSignedInteger 15 | KeyTypeAddress 16 | KeyTypeIPv4Address 17 | KeyTypeIPv6Address 18 | KeyTypeString 19 | KeyTypeBinary 20 | KeyTypeMethod 21 | ) 22 | 23 | func (t KeyType) New() MapKey { 24 | switch t { 25 | case KeyTypeSignedInteger: 26 | return new(SignedIntegerKey) 27 | case KeyTypeIPv4Address: 28 | return new(IPv4AddressKey) 29 | case KeyTypeIPv6Address: 30 | return new(IPv6AddressKey) 31 | case KeyTypeString: 32 | return new(StringKey) 33 | case KeyTypeBinary: 34 | return new(BinaryKey) 35 | default: 36 | panic("unknown key type: " + t.String()) 37 | } 38 | } 39 | 40 | type DataType int 41 | 42 | func (d DataType) String() string { 43 | switch d { 44 | case DataTypeServerId: 45 | return "server_id" 46 | case DataTypeGPT0: 47 | return "gpt0" 48 | case DataTypeGPC0: 49 | return "gpc0" 50 | case DataTypeGPC0Rate: 51 | return "gpc0_rate" 52 | case DataTypeConnectionsCounter: 53 | return "conn_cnt" 54 | case DataTypeConnectionRate: 55 | return "conn_rate" 56 | case DataTypeNumberOfCurrentConnections: 57 | return "conn_cur" 58 | case DataTypeSessionsCounter: 59 | return "sess_cnt" 60 | case DataTypeSessionRate: 61 | return "sess_rate" 62 | case DataTypeHttpRequestsCounter: 63 | return "http_req_cnt" 64 | case DataTypeHttpRequestsRate: 65 | return "http_req_rate" 66 | case DataTypeErrorsCounter: 67 | return "http_err_cnt" 68 | case DataTypeErrorsRate: 69 | return "http_err_rate" 70 | case DataTypeBytesInCounter: 71 | return "bytes_in_cnt" 72 | case DataTypeBytesInRate: 73 | return "bytes_in_rate" 74 | case DataTypeBytesOutCounter: 75 | return "bytes_out_cnt" 76 | case DataTypeBytesOutRate: 77 | return "bytes_out_rate" 78 | case DataTypeGPC1: 79 | return "gpc1" 80 | case DataTypeGPC1Rate: 81 | return "gpc1_rate" 82 | case DataTypeServerKey: 83 | return "server_key" 84 | case DataTypeHttpFailCounter: 85 | return "http_fail_cnt" 86 | case DataTypeHttpFailRate: 87 | return "http_fail_rate" 88 | case DataTypeGPTArray: 89 | return "gpt" 90 | case DataTypeGPCArray: 91 | return "gpc" 92 | case DataTypeGPCRateArray: 93 | return "gpc_rate" 94 | case DataTypeGlitchCounter: 95 | return "glitch_cnt" 96 | case DataTypeGlitchRate: 97 | return "glitch_rate" 98 | default: 99 | return "StickTableUpdateMessageType(" + strconv.FormatInt(int64(d), 10) + ")" 100 | } 101 | } 102 | 103 | func (d DataType) IsDelay() bool { 104 | switch d { 105 | case DataTypeGPC0Rate, 106 | DataTypeConnectionRate, 107 | DataTypeSessionRate, 108 | DataTypeHttpRequestsRate, 109 | DataTypeErrorsRate, 110 | DataTypeBytesInRate, 111 | DataTypeBytesOutRate, 112 | DataTypeGPC1Rate, 113 | DataTypeHttpFailRate: 114 | return true 115 | default: 116 | return false 117 | } 118 | } 119 | 120 | // The types of extra data we can store in a stick table 121 | const ( 122 | // DataTypeServerId represents the server ID to use with this 123 | // represents a stream if > 0 124 | DataTypeServerId DataType = iota 125 | // DataTypeGPT0 represents a General Purpose Flag 0. 126 | DataTypeGPT0 127 | // DataTypeGPC0 represents a General Purpose Counter 0 (unsigned 32-bit integer) 128 | DataTypeGPC0 129 | // DataTypeGPC0Rate represents a General Purpose Counter 0's event rate 130 | DataTypeGPC0Rate 131 | // DataTypeConnectionsCounter represents a cumulated number of connections 132 | DataTypeConnectionsCounter 133 | // DataTypeConnectionRate represents an incoming connection rate 134 | DataTypeConnectionRate 135 | // DataTypeNumberOfCurrentConnections represents a concurrent number of connections 136 | DataTypeNumberOfCurrentConnections 137 | // DataTypeSessionsCounter represents a cumulated number of sessions (accepted connections) 138 | DataTypeSessionsCounter 139 | // DataTypeSessionRate represents an accepted sessions rate 140 | DataTypeSessionRate 141 | // DataTypeHttpRequestsCounter represents a cumulated number of incoming HTTP requests 142 | DataTypeHttpRequestsCounter 143 | // DataTypeHttpRequestsRate represents an incoming HTTP request rate 144 | DataTypeHttpRequestsRate 145 | // DataTypeErrorsCounter represents a cumulated number of HTTP requests errors (4xx) 146 | DataTypeErrorsCounter 147 | // DataTypeErrorsRate represents an HTTP request error rate 148 | DataTypeErrorsRate 149 | // DataTypeBytesInCounter represents a cumulated bytes count from client to servers 150 | DataTypeBytesInCounter 151 | // DataTypeBytesInRate represents a bytes rate from client to servers 152 | DataTypeBytesInRate 153 | // DataTypeBytesOutCounter represents a cumulated bytes count from servers to client 154 | DataTypeBytesOutCounter 155 | // DataTypeBytesOutRate represents a bytes rate from servers to client 156 | DataTypeBytesOutRate 157 | // DataTypeGPC1 represents a General Purpose Counter 1 (unsigned 32-bit integer) 158 | DataTypeGPC1 159 | // DataTypeGPC1Rate represents a General Purpose Counter 1's event rate 160 | DataTypeGPC1Rate 161 | // DataTypeServerKey represents the server key 162 | DataTypeServerKey 163 | // DataTypeHttpFailCounter represents a cumulated number of HTTP server failures 164 | DataTypeHttpFailCounter 165 | // DataTypeHttpFailRate represents an HTTP server failures rate 166 | DataTypeHttpFailRate 167 | // DataTypeGPTArray represents an array of gpt 168 | DataTypeGPTArray 169 | // DataTypeGPCArray represents an array of gpc 170 | DataTypeGPCArray 171 | // DataTypeGPCRateArray represents an array of gpc_rate 172 | DataTypeGPCRateArray 173 | // DataTypeGlitchCounter represents a cumulated number of front glitches 174 | DataTypeGlitchCounter 175 | // DataTypeGlitchRate represents a rate of front glitches 176 | DataTypeGlitchRate 177 | ) 178 | 179 | func (d DataType) New() MapData { 180 | switch d { 181 | case DataTypeServerId: 182 | return new(SignedIntegerData) 183 | case DataTypeGPT0: 184 | return new(UnsignedIntegerData) 185 | case DataTypeGPC0: 186 | return new(UnsignedIntegerData) 187 | case DataTypeGPC0Rate: 188 | return new(FreqData) 189 | case DataTypeConnectionsCounter: 190 | return new(UnsignedIntegerData) 191 | case DataTypeConnectionRate: 192 | return new(FreqData) 193 | case DataTypeNumberOfCurrentConnections: 194 | return new(UnsignedIntegerData) 195 | case DataTypeSessionsCounter: 196 | return new(UnsignedIntegerData) 197 | case DataTypeSessionRate: 198 | return new(FreqData) 199 | case DataTypeHttpRequestsCounter: 200 | return new(UnsignedIntegerData) 201 | case DataTypeHttpRequestsRate: 202 | return new(FreqData) 203 | case DataTypeErrorsCounter: 204 | return new(UnsignedIntegerData) 205 | case DataTypeErrorsRate: 206 | return new(FreqData) 207 | case DataTypeBytesInCounter: 208 | return new(UnsignedLongLongData) 209 | case DataTypeBytesInRate: 210 | return new(FreqData) 211 | case DataTypeBytesOutCounter: 212 | return new(UnsignedLongLongData) 213 | case DataTypeBytesOutRate: 214 | return new(FreqData) 215 | case DataTypeGPC1: 216 | return new(UnsignedIntegerData) 217 | case DataTypeGPC1Rate: 218 | return new(FreqData) 219 | case DataTypeServerKey: 220 | return new(DictData) 221 | case DataTypeHttpFailCounter: 222 | return new(UnsignedIntegerData) 223 | case DataTypeHttpFailRate: 224 | return new(FreqData) 225 | case DataTypeGPTArray: 226 | return new(DictData) 227 | case DataTypeGPCArray: 228 | return new(DictData) 229 | case DataTypeGPCRateArray: 230 | return new(DictData) 231 | case DataTypeGlitchCounter: 232 | return new(UnsignedIntegerData) 233 | case DataTypeGlitchRate: 234 | return new(FreqData) 235 | default: 236 | return nil 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /peers/sticktable/constants_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type KeyType -output constants_string.go"; DO NOT EDIT. 2 | 3 | package sticktable 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[KeyTypeAny-0] 12 | _ = x[KeyTypeBoolean-1] 13 | _ = x[KeyTypeSignedInteger-2] 14 | _ = x[KeyTypeAddress-3] 15 | _ = x[KeyTypeIPv4Address-4] 16 | _ = x[KeyTypeIPv6Address-5] 17 | _ = x[KeyTypeString-6] 18 | _ = x[KeyTypeBinary-7] 19 | _ = x[KeyTypeMethod-8] 20 | } 21 | 22 | const _KeyType_name = "KeyTypeAnyKeyTypeBooleanKeyTypeSignedIntegerKeyTypeAddressKeyTypeIPv4AddressKeyTypeIPv6AddressKeyTypeStringKeyTypeBinaryKeyTypeMethod" 23 | 24 | var _KeyType_index = [...]uint8{0, 10, 24, 44, 58, 76, 94, 107, 120, 133} 25 | 26 | func (i KeyType) String() string { 27 | if i < 0 || i >= KeyType(len(_KeyType_index)-1) { 28 | return "KeyType(" + strconv.FormatInt(int64(i), 10) + ")" 29 | } 30 | return _KeyType_name[_KeyType_index[i]:_KeyType_index[i+1]] 31 | } 32 | -------------------------------------------------------------------------------- /peers/sticktable/sticktables.go: -------------------------------------------------------------------------------- 1 | package sticktable 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 9 | ) 10 | 11 | type DataTypeDefinition struct { 12 | DataType DataType 13 | Counter uint64 14 | Period uint64 15 | } 16 | 17 | type Definition struct { 18 | Name string 19 | DataTypes []DataTypeDefinition 20 | KeyType KeyType 21 | StickTableID uint64 22 | KeyLength uint64 23 | Expiry uint64 24 | } 25 | 26 | func (s *Definition) Unmarshal(b []byte) (int, error) { 27 | var offset int 28 | stickTableID, n, err := encoding.Varint(b[offset:]) 29 | offset += n 30 | if err != nil { 31 | return offset, err 32 | } 33 | s.StickTableID = stickTableID 34 | 35 | nameLength, n, err := encoding.Varint(b[offset:]) 36 | offset += n 37 | if err != nil { 38 | return offset, err 39 | } 40 | 41 | name := make([]byte, nameLength) 42 | offset += copy(name, b[offset:]) 43 | s.Name = string(name) 44 | 45 | keyType, n, err := encoding.Varint(b[offset:]) 46 | offset += n 47 | if err != nil { 48 | return offset, err 49 | } 50 | s.KeyType = KeyType(keyType) 51 | 52 | keyLength, n, err := encoding.Varint(b[offset:]) 53 | offset += n 54 | if err != nil { 55 | return offset, err 56 | } 57 | s.KeyLength = keyLength 58 | 59 | dataTypes, n, err := encoding.Varint(b[offset:]) 60 | offset += n 61 | if err != nil { 62 | return offset, err 63 | } 64 | 65 | expiry, n, err := encoding.Varint(b[offset:]) 66 | offset += n 67 | if err != nil { 68 | return offset, err 69 | } 70 | s.Expiry = expiry 71 | 72 | // The data types are values from 0 to 64. Currently only 24 are implemented, 73 | // but we iterate over all possible values to capture potentially missing ones. 74 | for i := 0; i < 64; i++ { 75 | if (dataTypes>>i)&1 == 1 { 76 | 77 | d := DataTypeDefinition{ 78 | DataType: DataType(i), 79 | } 80 | 81 | info := d.DataType.New() 82 | if info == nil { 83 | return offset, fmt.Errorf("unknown data type: %v", d.DataType) 84 | } 85 | 86 | if d.DataType.IsDelay() { 87 | counter, n, err := encoding.Varint(b[offset:]) 88 | offset += n 89 | if err != nil { 90 | return offset, err 91 | } 92 | d.Counter = counter 93 | 94 | period, n, err := encoding.Varint(b[offset:]) 95 | offset += n 96 | if err != nil { 97 | return offset, err 98 | } 99 | d.Period = period 100 | } 101 | 102 | s.DataTypes = append(s.DataTypes, d) 103 | } 104 | } 105 | return offset, nil 106 | } 107 | 108 | func (s *Definition) Marshal(b []byte) (int, error) { 109 | var offset int 110 | n, err := encoding.PutVarint(b[offset:], s.StickTableID) 111 | offset += n 112 | if err != nil { 113 | return offset, err 114 | } 115 | 116 | n, err = encoding.PutVarint(b[offset:], uint64(len(s.Name))) 117 | offset += n 118 | if err != nil { 119 | return offset, err 120 | } 121 | 122 | offset += copy(b[offset:], s.Name) 123 | 124 | n, err = encoding.PutVarint(b[offset:], uint64(s.KeyType)) 125 | offset += n 126 | if err != nil { 127 | return offset, err 128 | } 129 | 130 | n, err = encoding.PutVarint(b[offset:], s.KeyLength) 131 | offset += n 132 | if err != nil { 133 | return offset, err 134 | } 135 | 136 | var dataTypes uint64 137 | for _, dataType := range s.DataTypes { 138 | dataTypes |= 1 << dataType.DataType 139 | } 140 | 141 | n, err = encoding.PutVarint(b[offset:], dataTypes) 142 | offset += n 143 | if err != nil { 144 | return offset, err 145 | } 146 | 147 | n, err = encoding.PutVarint(b[offset:], s.Expiry) 148 | offset += n 149 | if err != nil { 150 | return offset, err 151 | } 152 | 153 | for _, dataType := range s.DataTypes { 154 | if dataType.DataType.IsDelay() { 155 | n, err = encoding.PutVarint(b[offset:], dataType.Counter) 156 | offset += n 157 | if err != nil { 158 | return offset, err 159 | } 160 | 161 | n, err = encoding.PutVarint(b[offset:], dataType.Period) 162 | offset += n 163 | if err != nil { 164 | return offset, err 165 | } 166 | } 167 | } 168 | 169 | return offset, nil 170 | } 171 | 172 | type EntryUpdate struct { 173 | StickTable *Definition 174 | Key MapKey 175 | Data []MapData 176 | 177 | WithLocalUpdateID bool 178 | LocalUpdateID uint32 179 | 180 | WithExpiry bool 181 | Expiry uint32 182 | } 183 | 184 | func (e *EntryUpdate) String() string { 185 | var data []string 186 | for i, d := range e.Data { 187 | data = append(data, fmt.Sprintf("%s: %s", e.StickTable.DataTypes[i].DataType.String(), d.String())) 188 | } 189 | 190 | return fmt.Sprintf("EntryUpdate %d: %s - %s", e.LocalUpdateID, e.Key, strings.Join(data, " | ")) 191 | } 192 | 193 | func (e *EntryUpdate) Marshal(b []byte) (int, error) { 194 | var offset int 195 | if e.WithLocalUpdateID { 196 | binary.BigEndian.PutUint32(b[offset:], e.LocalUpdateID) 197 | offset += 4 198 | } 199 | 200 | if e.WithExpiry { 201 | binary.BigEndian.PutUint32(b[offset:], e.Expiry) 202 | offset += 4 203 | } 204 | 205 | n, err := e.Key.Marshal(b[offset:], e.StickTable.KeyLength) 206 | offset += n 207 | if err != nil { 208 | return offset, err 209 | } 210 | 211 | for _, data := range e.Data { 212 | n, err := data.Unmarshal(b[offset:]) 213 | offset += n 214 | if err != nil { 215 | return offset, err 216 | } 217 | } 218 | 219 | return offset, nil 220 | } 221 | 222 | func (e *EntryUpdate) Unmarshal(b []byte) (int, error) { 223 | var offset int 224 | // We already have a correct update ID loaded from the caller, 225 | // so we just override it when the message has its own 226 | if e.WithLocalUpdateID { 227 | e.LocalUpdateID = binary.BigEndian.Uint32(b[offset:]) 228 | offset += 4 229 | } 230 | 231 | if e.WithExpiry { 232 | e.Expiry = binary.BigEndian.Uint32(b[offset:]) 233 | offset += 4 234 | } 235 | 236 | e.Key = e.StickTable.KeyType.New() 237 | if e.Key == nil { 238 | return offset, fmt.Errorf("unknown key type: %v", e.StickTable.KeyType) 239 | } 240 | 241 | n, err := e.Key.Unmarshal(b[offset:], e.StickTable.KeyLength) 242 | if err != nil { 243 | return offset, err 244 | } 245 | offset += n 246 | 247 | for _, dataType := range e.StickTable.DataTypes { 248 | data := dataType.DataType.New() 249 | if data == nil { 250 | return offset, fmt.Errorf("unknown data type: %v", dataType) 251 | } 252 | 253 | n, err := data.Unmarshal(b[offset:]) 254 | if err != nil { 255 | return offset, err 256 | } 257 | offset += n 258 | 259 | e.Data = append(e.Data, data) 260 | } 261 | 262 | return offset, nil 263 | } 264 | -------------------------------------------------------------------------------- /peers/sticktable/sticktables_test.go: -------------------------------------------------------------------------------- 1 | package sticktable 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestMarshalUnmarshalMessages(t *testing.T) { 10 | stickTableDefinition := &Definition{ 11 | StickTableID: 1337, 12 | Name: "foobar", 13 | KeyType: KeyTypeString, 14 | KeyLength: 11, 15 | DataTypes: []DataTypeDefinition{ 16 | { 17 | DataType: DataTypeBytesInRate, 18 | Counter: 12, 19 | Period: 1, 20 | }, 21 | }, 22 | Expiry: 13, 23 | } 24 | 25 | stringKey := StringKey("1234567890a") 26 | entryUpdate := &EntryUpdate{ 27 | StickTable: stickTableDefinition, 28 | WithLocalUpdateID: true, 29 | WithExpiry: true, 30 | LocalUpdateID: 23, 31 | Key: &stringKey, 32 | Data: []MapData{ 33 | &FreqData{ 34 | CurrentTick: 1, 35 | CurrentPeriod: 2, 36 | LastPeriod: 3, 37 | }, 38 | }, 39 | Expiry: 42, 40 | } 41 | 42 | t.Run("sticktable definition", func(t *testing.T) { 43 | b := make([]byte, 256) 44 | n, err := stickTableDefinition.Marshal(b) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | var d Definition 50 | _, err = d.Unmarshal(b[:n]) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | if diff := cmp.Diff(*stickTableDefinition, d); diff != "" { 56 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 57 | } 58 | }) 59 | 60 | t.Run("entry update", func(t *testing.T) { 61 | b := make([]byte, 256) 62 | n, err := entryUpdate.Marshal(b) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | var d EntryUpdate 68 | d.StickTable = entryUpdate.StickTable 69 | d.WithExpiry = entryUpdate.WithExpiry 70 | d.WithLocalUpdateID = entryUpdate.WithLocalUpdateID 71 | _, err = d.Unmarshal(b[:n]) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if diff := cmp.Diff(*entryUpdate, d); diff != "" { 77 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /peers/sticktable/values.go: -------------------------------------------------------------------------------- 1 | package sticktable 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "net/netip" 7 | 8 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 9 | ) 10 | 11 | type MapKey interface { 12 | fmt.Stringer 13 | Unmarshal(b []byte, keySize uint64) (int, error) 14 | Marshal(b []byte, keySize uint64) (int, error) 15 | } 16 | 17 | type SignedIntegerKey int32 18 | 19 | func (v *SignedIntegerKey) Unmarshal(b []byte, keySize uint64) (int, error) { 20 | *v = SignedIntegerKey(binary.BigEndian.Uint32(b)) 21 | return 4, nil 22 | } 23 | 24 | func (v *SignedIntegerKey) String() string { 25 | return fmt.Sprintf("%d", *v) 26 | } 27 | 28 | type IPv4AddressKey netip.Addr 29 | 30 | func (v *IPv4AddressKey) Unmarshal(b []byte, keySize uint64) (int, error) { 31 | if keySize != 4 { 32 | return 0, fmt.Errorf("invalid ipv4 key size: %d", keySize) 33 | } 34 | 35 | *v = IPv4AddressKey(netip.AddrFrom4([4]byte(b))) 36 | return 4, nil 37 | } 38 | 39 | func (v *IPv4AddressKey) String() string { 40 | return (*netip.Addr)(v).String() 41 | } 42 | 43 | type IPv6AddressKey netip.Addr 44 | 45 | func (v *IPv6AddressKey) Unmarshal(b []byte, keySize uint64) (int, error) { 46 | if keySize != 16 { 47 | return 0, fmt.Errorf("invalid ipv6 key size: %d", keySize) 48 | } 49 | 50 | *v = IPv6AddressKey(netip.AddrFrom16([16]byte(b))) 51 | 52 | return 16, nil 53 | } 54 | 55 | func (v *IPv6AddressKey) String() string { 56 | return (*netip.Addr)(v).String() 57 | } 58 | 59 | type StringKey string 60 | 61 | func (v *StringKey) Unmarshal(b []byte, keySize uint64) (int, error) { 62 | valueLength, n, err := encoding.Varint(b) 63 | if err != nil { 64 | return n, err 65 | } 66 | if valueLength == 0 { 67 | return n, nil 68 | } 69 | *v = StringKey(b[n : n+int(valueLength)]) 70 | return n + int(valueLength), nil 71 | } 72 | 73 | func (v *StringKey) String() string { 74 | return string(*v) 75 | } 76 | 77 | type BinaryKey []byte 78 | 79 | func (v *BinaryKey) Unmarshal(b []byte, keySize uint64) (int, error) { 80 | *v = b[:keySize] 81 | return int(keySize), nil 82 | } 83 | 84 | func (v *BinaryKey) String() string { 85 | return fmt.Sprintf("%v", *v) 86 | } 87 | 88 | type MapData interface { 89 | fmt.Stringer 90 | Unmarshal(b []byte) (int, error) 91 | Marshal(b []byte) (int, error) 92 | } 93 | 94 | type FreqData struct { 95 | CurrentTick uint64 96 | CurrentPeriod uint64 97 | LastPeriod uint64 98 | } 99 | 100 | func (f *FreqData) String() string { 101 | return fmt.Sprintf("tick/cur/last: %d/%d/%d", f.CurrentTick, f.CurrentPeriod, f.LastPeriod) 102 | } 103 | 104 | func (f *FreqData) Unmarshal(b []byte) (int, error) { 105 | var offset int 106 | // start date of current period (wrapping ticks) 107 | currentTick, n, err := encoding.Varint(b[offset:]) 108 | if err != nil { 109 | return n, err 110 | } 111 | f.CurrentTick = currentTick 112 | offset += n 113 | 114 | // cumulated value for current period 115 | currentPeriod, n, err := encoding.Varint(b[offset:]) 116 | if err != nil { 117 | return n, err 118 | } 119 | f.CurrentPeriod = currentPeriod 120 | offset += n 121 | 122 | // value for last period 123 | lastPeriod, n, err := encoding.Varint(b[offset:]) 124 | if err != nil { 125 | return n, err 126 | } 127 | f.LastPeriod = lastPeriod 128 | offset += n 129 | 130 | return offset, nil 131 | } 132 | 133 | type SignedIntegerData int32 134 | 135 | func (v *SignedIntegerData) Unmarshal(b []byte) (int, error) { 136 | value, n, err := encoding.Varint(b) 137 | if err != nil { 138 | return n, err 139 | } 140 | 141 | *v = SignedIntegerData(value) 142 | return n, nil 143 | } 144 | 145 | func (v *SignedIntegerData) String() string { 146 | return fmt.Sprintf("%d", *v) 147 | } 148 | 149 | type UnsignedIntegerData uint32 150 | 151 | func (v *UnsignedIntegerData) Unmarshal(b []byte) (int, error) { 152 | value, n, err := encoding.Varint(b) 153 | if err != nil { 154 | return n, err 155 | } 156 | 157 | *v = UnsignedIntegerData(value) 158 | return n, nil 159 | } 160 | 161 | func (v *UnsignedIntegerData) String() string { 162 | return fmt.Sprintf("%d", *v) 163 | } 164 | 165 | type UnsignedLongLongData uint64 166 | 167 | func (v *UnsignedLongLongData) Unmarshal(b []byte) (int, error) { 168 | value, n, err := encoding.Varint(b) 169 | if err != nil { 170 | return n, err 171 | } 172 | 173 | *v = UnsignedLongLongData(value) 174 | return n, nil 175 | } 176 | 177 | func (v *UnsignedLongLongData) String() string { 178 | return fmt.Sprintf("%d", *v) 179 | } 180 | 181 | type DictData struct { 182 | Value []byte 183 | ID uint64 184 | } 185 | 186 | func (f *DictData) String() string { 187 | if f.ID == 0 { 188 | return "No Entry" 189 | } 190 | 191 | return fmt.Sprintf("%d: %v", f.ID, f.Value) 192 | } 193 | 194 | func (f *DictData) Unmarshal(b []byte) (int, error) { 195 | var offset int 196 | length, n, err := encoding.Varint(b[offset:]) 197 | offset += n 198 | if err != nil { 199 | return offset, err 200 | } 201 | 202 | // No entries 203 | if length == 0 { 204 | return offset, nil 205 | } 206 | 207 | id, n, err := encoding.Varint(b[offset:]) 208 | offset += n 209 | if err != nil { 210 | return offset, err 211 | } 212 | f.ID = id 213 | 214 | if length == 1 { 215 | return offset, nil 216 | } 217 | 218 | valueLength, n, err := encoding.Varint(b[offset:]) 219 | offset += n 220 | if err != nil { 221 | return offset, err 222 | } 223 | 224 | if valueLength == 0 { 225 | return offset, nil 226 | } 227 | 228 | value := make([]byte, valueLength) 229 | offset += copy(value, b[offset:]) 230 | f.Value = value 231 | 232 | return offset, nil 233 | } 234 | 235 | func (v *SignedIntegerKey) Marshal(b []byte, keySize uint64) (int, error) { 236 | binary.BigEndian.PutUint32(b, uint32(*v)) 237 | return 4, nil 238 | } 239 | 240 | func (v *IPv4AddressKey) Marshal(b []byte, keySize uint64) (int, error) { 241 | if keySize != 4 { 242 | return 0, fmt.Errorf("invalid ipv4 key size: %d", keySize) 243 | } 244 | a := (*netip.Addr)(v).As4() 245 | copy(b, a[:]) 246 | return 4, nil 247 | } 248 | 249 | func (v *IPv6AddressKey) Marshal(b []byte, keySize uint64) (int, error) { 250 | if keySize != 16 { 251 | return 0, fmt.Errorf("invalid ipv6 key size: %d", keySize) 252 | } 253 | a := (*netip.Addr)(v).As16() 254 | copy(b, a[:]) 255 | return 16, nil 256 | } 257 | 258 | func (v *StringKey) Marshal(b []byte, keySize uint64) (int, error) { 259 | n, err := encoding.PutVarint(b, uint64(len(*v))) 260 | if err != nil { 261 | return n, err 262 | } 263 | 264 | return n + copy(b[n:], *v), nil 265 | } 266 | 267 | func (v *BinaryKey) Marshal(b []byte, keySize uint64) (int, error) { 268 | return copy(b[:keySize], *v), nil 269 | } 270 | 271 | func (f *FreqData) Marshal(b []byte) (int, error) { 272 | var offset int 273 | 274 | n, err := encoding.PutVarint(b[offset:], f.CurrentTick) 275 | offset += n 276 | if err != nil { 277 | return offset, err 278 | } 279 | 280 | n, err = encoding.PutVarint(b[offset:], f.CurrentPeriod) 281 | offset += n 282 | if err != nil { 283 | return offset, err 284 | } 285 | 286 | n, err = encoding.PutVarint(b[offset:], f.LastPeriod) 287 | offset += n 288 | if err != nil { 289 | return offset, err 290 | } 291 | 292 | return offset, nil 293 | } 294 | 295 | func (v *SignedIntegerData) Marshal(b []byte) (int, error) { 296 | return encoding.PutVarint(b, uint64(*v)) 297 | } 298 | 299 | func (v *UnsignedIntegerData) Marshal(b []byte) (int, error) { 300 | return encoding.PutVarint(b, uint64(*v)) 301 | } 302 | 303 | func (v *UnsignedLongLongData) Marshal(b []byte) (int, error) { 304 | return encoding.PutVarint(b, uint64(*v)) 305 | } 306 | 307 | func (f *DictData) Marshal(b []byte) (int, error) { 308 | var offset int 309 | 310 | n, err := encoding.PutVarint(b[offset:], uint64(len(f.Value))) 311 | offset += n 312 | if err != nil { 313 | return offset, err 314 | } 315 | 316 | n, err = encoding.PutVarint(b[offset:], uint64(f.ID)) 317 | offset += n 318 | if err != nil { 319 | return offset, err 320 | } 321 | 322 | if len(f.Value) > 0 { 323 | n, err := encoding.PutVarint(b[offset:], uint64(len(f.Value))) 324 | offset += n 325 | if err != nil { 326 | return offset, err 327 | } 328 | 329 | copy(b[offset:], f.Value) 330 | offset += len(f.Value) 331 | } 332 | 333 | return offset, nil 334 | } 335 | -------------------------------------------------------------------------------- /peers/sticktable/values_test.go: -------------------------------------------------------------------------------- 1 | package sticktable 2 | 3 | import ( 4 | "net/netip" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestMarshalUnmarshalValues(t *testing.T) { 11 | t.Run("MapKey", func(t *testing.T) { 12 | t.Run("SignedIntegerKey", func(t *testing.T) { 13 | in := SignedIntegerKey(1337) 14 | var out SignedIntegerKey 15 | 16 | b := make([]byte, 256) 17 | n, err := in.Marshal(b, 1) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | _, err = out.Unmarshal(b[:n], 1) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if diff := cmp.Diff(in, out); diff != "" { 28 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 29 | } 30 | }) 31 | t.Run("IPv4AddressKey", func(t *testing.T) { 32 | in := IPv4AddressKey(netip.MustParseAddr("127.0.0.1")) 33 | var out IPv4AddressKey 34 | 35 | b := make([]byte, 256) 36 | n, err := in.Marshal(b, 4) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | _, err = out.Unmarshal(b[:n], 4) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if in != out { 47 | t.Errorf("Unmarshal() mismatch:\n%v != %v", in, out) 48 | } 49 | }) 50 | t.Run("IPv6AddressKey", func(t *testing.T) { 51 | in := IPv6AddressKey(netip.MustParseAddr("fe80::1")) 52 | var out IPv6AddressKey 53 | 54 | b := make([]byte, 256) 55 | n, err := in.Marshal(b, 16) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | _, err = out.Unmarshal(b[:n], 16) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | if in != out { 66 | t.Errorf("Unmarshal() mismatch:\n%v != %v", in, out) 67 | } 68 | }) 69 | t.Run("StringKey", func(t *testing.T) { 70 | in := StringKey("foobar") 71 | var out StringKey 72 | 73 | b := make([]byte, 256) 74 | n, err := in.Marshal(b, uint64(len(in))) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | _, err = out.Unmarshal(b[:n], uint64(len(in))) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | if diff := cmp.Diff(in, out); diff != "" { 85 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 86 | } 87 | }) 88 | t.Run("BinaryKey", func(t *testing.T) { 89 | in := BinaryKey("foobar") 90 | var out BinaryKey 91 | 92 | b := make([]byte, 256) 93 | n, err := in.Marshal(b, uint64(len(in))) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | _, err = out.Unmarshal(b[:n], uint64(len(in))) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | if diff := cmp.Diff(in, out); diff != "" { 104 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 105 | } 106 | }) 107 | 108 | }) 109 | 110 | t.Run("MapData", func(t *testing.T) { 111 | t.Run("FreqData", func(t *testing.T) { 112 | in := FreqData{ 113 | CurrentTick: 1, 114 | CurrentPeriod: 2, 115 | LastPeriod: 3, 116 | } 117 | var out FreqData 118 | 119 | b := make([]byte, 256) 120 | n, err := in.Marshal(b) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | _, err = out.Unmarshal(b[:n]) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | if diff := cmp.Diff(in, out); diff != "" { 131 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 132 | } 133 | }) 134 | t.Run("SignedIntegerData", func(t *testing.T) { 135 | in := SignedIntegerData(1337) 136 | var out SignedIntegerData 137 | 138 | b := make([]byte, 256) 139 | n, err := in.Marshal(b) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | _, err = out.Unmarshal(b[:n]) 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | 149 | if diff := cmp.Diff(in, out); diff != "" { 150 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 151 | } 152 | }) 153 | 154 | t.Run("UnsignedIntegerData", func(t *testing.T) { 155 | in := UnsignedIntegerData(1337) 156 | var out UnsignedIntegerData 157 | 158 | b := make([]byte, 256) 159 | n, err := in.Marshal(b) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | 164 | _, err = out.Unmarshal(b[:n]) 165 | if err != nil { 166 | t.Fatal(err) 167 | } 168 | 169 | if diff := cmp.Diff(in, out); diff != "" { 170 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 171 | } 172 | }) 173 | 174 | t.Run("UnsignedLongLongData", func(t *testing.T) { 175 | in := UnsignedLongLongData(1337) 176 | var out UnsignedLongLongData 177 | 178 | b := make([]byte, 256) 179 | n, err := in.Marshal(b) 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | 184 | _, err = out.Unmarshal(b[:n]) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | if diff := cmp.Diff(in, out); diff != "" { 190 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 191 | } 192 | }) 193 | 194 | t.Run("DictData", func(t *testing.T) { 195 | in := DictData{ 196 | ID: 1, 197 | Value: []byte("foobar"), 198 | } 199 | var out DictData 200 | 201 | b := make([]byte, 256) 202 | n, err := in.Marshal(b) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | 207 | _, err = out.Unmarshal(b[:n]) 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | 212 | if diff := cmp.Diff(in, out); diff != "" { 213 | t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) 214 | } 215 | }) 216 | }) 217 | } 218 | -------------------------------------------------------------------------------- /pkg/buffer/slicebuf.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | type SliceBuffer struct { 4 | buf []byte 5 | readOffset int 6 | writeOffset int 7 | } 8 | 9 | func NewSliceBuffer(size int) *SliceBuffer { 10 | return &SliceBuffer{ 11 | buf: make([]byte, size), 12 | } 13 | } 14 | 15 | func NewSliceBufferWithSlice(b []byte) *SliceBuffer { 16 | return &SliceBuffer{ 17 | buf: b, 18 | writeOffset: len(b), 19 | } 20 | } 21 | 22 | func (s *SliceBuffer) Reset() { 23 | s.readOffset = 0 24 | s.writeOffset = 0 25 | } 26 | 27 | func (s *SliceBuffer) ReadBytes() []byte { 28 | return s.buf[s.readOffset:s.writeOffset] 29 | } 30 | 31 | func (s *SliceBuffer) WriteBytes() []byte { 32 | return s.buf[s.writeOffset:] 33 | } 34 | 35 | func (s *SliceBuffer) AdvanceR(n int) { 36 | s.readOffset += n 37 | } 38 | 39 | func (s *SliceBuffer) AdvanceW(n int) { 40 | s.writeOffset += n 41 | } 42 | 43 | func (s *SliceBuffer) WriteNBytes(n int) []byte { 44 | s.writeOffset += n 45 | return s.buf[s.writeOffset-n : s.writeOffset] 46 | } 47 | 48 | func (s *SliceBuffer) ReadNBytes(n int) []byte { 49 | s.readOffset += n 50 | return s.buf[s.readOffset-n : s.readOffset] 51 | } 52 | 53 | func (s *SliceBuffer) Len() int { 54 | return s.writeOffset - s.readOffset 55 | } 56 | -------------------------------------------------------------------------------- /pkg/encoding/actionwriter.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "sync" 7 | ) 8 | 9 | type actionType byte 10 | 11 | const ( 12 | ActionTypeSetVar actionType = 1 13 | ActionTypeUnsetVar actionType = 2 14 | ) 15 | 16 | type varScope byte 17 | 18 | const ( 19 | VarScopeProcess varScope = 0 20 | VarScopeSession varScope = 1 21 | VarScopeTransaction varScope = 2 22 | VarScopeRequest varScope = 3 23 | VarScopeResponse varScope = 4 24 | ) 25 | 26 | var actionWriterPool = sync.Pool{ 27 | New: func() any { 28 | return NewActionWriter(nil, 0) 29 | }, 30 | } 31 | 32 | func AcquireActionWriter(buf []byte, off int) *ActionWriter { 33 | w := actionWriterPool.Get().(*ActionWriter) 34 | w.data = buf 35 | w.off = off 36 | return w 37 | } 38 | 39 | func ReleaseActionWriter(w *ActionWriter) { 40 | w.data = nil 41 | w.off = 0 42 | actionWriterPool.Put(w) 43 | } 44 | 45 | type ActionWriter struct { 46 | data []byte 47 | off int 48 | } 49 | 50 | func NewActionWriter(buf []byte, off int) *ActionWriter { 51 | return &ActionWriter{ 52 | data: buf, 53 | off: off, 54 | } 55 | } 56 | 57 | func (aw *ActionWriter) Off() int { 58 | return aw.off 59 | } 60 | 61 | func (aw *ActionWriter) Bytes() []byte { 62 | return aw.data[:aw.off] 63 | } 64 | 65 | func (aw *ActionWriter) actionHeader(t actionType, s varScope, name []byte) error { 66 | aw.data[aw.off] = byte(t) 67 | aw.off++ 68 | 69 | // NB-Args 70 | var nbArgs byte 71 | switch t { 72 | case ActionTypeSetVar: 73 | nbArgs = 3 74 | case ActionTypeUnsetVar: 75 | nbArgs = 2 76 | default: 77 | panic("unknown action type") 78 | } 79 | 80 | aw.data[aw.off] = nbArgs 81 | aw.off++ 82 | 83 | aw.data[aw.off] = byte(s) 84 | aw.off++ 85 | 86 | n, err := PutBytes(aw.data[aw.off:], name) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | aw.off += n 92 | return nil 93 | } 94 | 95 | func (aw *ActionWriter) Unset(s varScope, name string) error { 96 | return aw.actionHeader(ActionTypeUnsetVar, s, []byte(name)) 97 | } 98 | 99 | func (aw *ActionWriter) SetStringBytes(s varScope, name string, v []byte) error { 100 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 101 | return err 102 | } 103 | 104 | aw.data[aw.off] = byte(DataTypeString) 105 | aw.off++ 106 | 107 | n, err := PutBytes(aw.data[aw.off:], v) 108 | if err != nil { 109 | return err 110 | } 111 | aw.off += n 112 | 113 | return nil 114 | } 115 | func (aw *ActionWriter) SetString(s varScope, name string, v string) error { 116 | return aw.SetStringBytes(s, name, []byte(v)) 117 | } 118 | 119 | func (aw *ActionWriter) SetBinary(s varScope, name string, v []byte) error { 120 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 121 | return err 122 | } 123 | 124 | aw.data[aw.off] = byte(DataTypeBinary) 125 | aw.off++ 126 | 127 | n, err := PutBytes(aw.data[aw.off:], v) 128 | if err != nil { 129 | return err 130 | } 131 | aw.off += n 132 | 133 | return nil 134 | } 135 | 136 | func (aw *ActionWriter) SetNull(s varScope, name string) error { 137 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 138 | return err 139 | } 140 | 141 | aw.data[aw.off] = byte(DataTypeNull) 142 | aw.off++ 143 | 144 | return nil 145 | } 146 | func (aw *ActionWriter) SetBool(s varScope, name string, v bool) error { 147 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 148 | return err 149 | } 150 | 151 | aw.data[aw.off] = byte(DataTypeBool) 152 | if v { 153 | aw.data[aw.off] |= dataFlagTrue 154 | } 155 | aw.off++ 156 | 157 | return nil 158 | } 159 | 160 | func (aw *ActionWriter) SetUInt32(s varScope, name string, v uint32) error { 161 | return aw.SetInt64(s, name, int64(v)) 162 | } 163 | 164 | func (aw *ActionWriter) SetInt32(s varScope, name string, v int32) error { 165 | return aw.SetInt64(s, name, int64(v)) 166 | } 167 | 168 | func (aw *ActionWriter) SetInt64(s varScope, name string, v int64) error { 169 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 170 | return err 171 | } 172 | 173 | aw.data[aw.off] = byte(DataTypeInt64) 174 | aw.off++ 175 | 176 | n, err := PutVarint(aw.data[aw.off:], uint64(v)) 177 | if err != nil { 178 | return err 179 | } 180 | aw.off += n 181 | 182 | return nil 183 | } 184 | func (aw *ActionWriter) SetUInt64(s varScope, name string, v uint64) error { 185 | return aw.SetInt64(s, name, int64(v)) 186 | } 187 | 188 | func (aw *ActionWriter) SetAddr(s varScope, name string, v netip.Addr) error { 189 | if err := aw.actionHeader(ActionTypeSetVar, s, []byte(name)); err != nil { 190 | return err 191 | } 192 | 193 | switch { 194 | case v.Is6(): 195 | aw.data[aw.off] = byte(DataTypeIPV6) 196 | case v.Is4(): 197 | aw.data[aw.off] = byte(DataTypeIPV4) 198 | default: 199 | return fmt.Errorf("invalid address") 200 | } 201 | aw.off++ 202 | 203 | n, err := PutAddr(aw.data[aw.off:], v) 204 | if err != nil { 205 | return err 206 | } 207 | aw.off += n 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /pkg/encoding/actionwriter_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dropmorepackets/haproxy-go/pkg/testutil" 8 | ) 9 | 10 | func TestActionWriter(t *testing.T) { 11 | buf := make([]byte, 16386) 12 | 13 | const exampleKey, exampleValue = "key", "value" 14 | testutil.WithoutAllocations(t, func() { 15 | aw := NewActionWriter(buf, 0) 16 | 17 | if err := aw.SetString(VarScopeTransaction, exampleKey, exampleValue); err != nil { 18 | t.Error(err) 19 | } 20 | 21 | buf = aw.Bytes() 22 | }) 23 | 24 | const expectedValue = "010302036b6579080576616c7565" 25 | if s := fmt.Sprintf("%x", buf); s != expectedValue { 26 | t.Errorf("result doesnt match golden string: %s != %s", expectedValue, s) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "net/netip" 5 | ) 6 | 7 | type DataType byte 8 | 9 | const ( 10 | DataTypeNull DataType = 0 11 | DataTypeBool DataType = 1 12 | DataTypeInt32 DataType = 2 13 | DataTypeUInt32 DataType = 3 14 | DataTypeInt64 DataType = 4 15 | DataTypeUInt64 DataType = 5 16 | DataTypeIPV4 DataType = 6 17 | DataTypeIPV6 DataType = 7 18 | DataTypeString DataType = 8 19 | DataTypeBinary DataType = 9 20 | 21 | dataTypeMask byte = 0x0F 22 | dataFlagTrue byte = 0x10 23 | ) 24 | 25 | func PutBytes(b []byte, v []byte) (int, error) { 26 | l := len(v) 27 | n, err := PutVarint(b, uint64(l)) 28 | if err != nil { 29 | return 0, err 30 | } 31 | 32 | if l+n > len(b) { 33 | return 0, ErrInsufficientSpace 34 | } 35 | 36 | copy(b[n:], v) 37 | return n + l, nil 38 | } 39 | 40 | func PutAddr(b []byte, ip netip.Addr) (int, error) { 41 | s := ip.AsSlice() 42 | if len(b) < len(s) { 43 | return 0, ErrInsufficientSpace 44 | } 45 | 46 | copy(b, s) 47 | return len(s), nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/encoding/kvscanner.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/netip" 7 | "sync" 8 | ) 9 | 10 | var kvEntryPool = sync.Pool{ 11 | New: func() any { 12 | return &KVEntry{} 13 | }, 14 | } 15 | 16 | var kvScannerPool = sync.Pool{ 17 | New: func() any { 18 | return NewKVScanner(nil, 0) 19 | }, 20 | } 21 | 22 | func AcquireKVScanner(b []byte, count int) *KVScanner { 23 | s := kvScannerPool.Get().(*KVScanner) 24 | s.buf = b 25 | s.left = count 26 | return s 27 | } 28 | 29 | func ReleaseKVScanner(s *KVScanner) { 30 | s.lastErr = nil 31 | s.buf = nil 32 | s.left = 0 33 | kvScannerPool.Put(s) 34 | } 35 | 36 | func AcquireKVEntry() *KVEntry { 37 | return kvEntryPool.Get().(*KVEntry) 38 | } 39 | 40 | func ReleaseKVEntry(k *KVEntry) { 41 | k.Reset() 42 | kvEntryPool.Put(k) 43 | } 44 | 45 | type KVScanner struct { 46 | lastErr error 47 | buf []byte 48 | left int 49 | } 50 | 51 | // RemainingBuf returns the remaining length of the buffer 52 | func (k *KVScanner) RemainingBuf() int { 53 | return len(k.buf) 54 | } 55 | 56 | func NewKVScanner(b []byte, count int) *KVScanner { 57 | return &KVScanner{buf: b, left: count} 58 | } 59 | 60 | func (k *KVScanner) Error() error { 61 | return k.lastErr 62 | } 63 | 64 | type KVEntry struct { 65 | name []byte 66 | 67 | // if the content is a varint, we directly decode it. 68 | // else its decoded on the fly 69 | byteVal []byte 70 | intVal int64 71 | 72 | dataType DataType 73 | 74 | boolVar bool 75 | } 76 | 77 | func (k *KVEntry) NameBytes() []byte { 78 | return k.name 79 | } 80 | 81 | func (k *KVEntry) ValueBytes() []byte { 82 | return k.byteVal 83 | } 84 | 85 | func (k *KVEntry) ValueInt() int64 { 86 | return k.intVal 87 | } 88 | 89 | func (k *KVEntry) ValueBool() bool { 90 | return k.boolVar 91 | } 92 | 93 | func (k *KVEntry) ValueAddr() netip.Addr { 94 | addr, ok := netip.AddrFromSlice(k.byteVal) 95 | if !ok { 96 | panic("invalid addr decode: " + fmt.Sprintf("%x", k.byteVal)) 97 | } 98 | return addr 99 | } 100 | 101 | func (k *KVEntry) NameEquals(s string) bool { 102 | // bytes.Equal describes this operation as alloc free 103 | return string(k.name) == s 104 | } 105 | 106 | func (k *KVEntry) Type() DataType { 107 | return k.dataType 108 | } 109 | 110 | // Value returns the typed value for the KVEntry. It can allocate memory 111 | // which is why assertions and direct type access is recommended. 112 | func (k *KVEntry) Value() any { 113 | switch k.dataType { 114 | case DataTypeNull: 115 | return nil 116 | case DataTypeBool: 117 | return k.boolVar 118 | case DataTypeInt32: 119 | return int32(k.intVal) 120 | case DataTypeInt64: 121 | return k.intVal 122 | case DataTypeUInt32: 123 | return uint32(k.intVal) 124 | case DataTypeUInt64: 125 | return uint64(k.intVal) 126 | case DataTypeIPV4, DataTypeIPV6: 127 | addr, ok := netip.AddrFromSlice(k.byteVal) 128 | if !ok { 129 | panic("invalid addr decode") 130 | } 131 | return addr 132 | case DataTypeString: 133 | return string(k.byteVal) 134 | case DataTypeBinary: 135 | return k.byteVal 136 | default: 137 | panic("unknown datatype") 138 | } 139 | } 140 | 141 | func (k *KVEntry) Reset() { 142 | k.name = nil 143 | k.dataType = 0 144 | k.byteVal = nil 145 | k.boolVar = false 146 | k.intVal = 0 147 | } 148 | 149 | func (k *KVScanner) Next(e *KVEntry) bool { 150 | if len(k.buf) == 0 { 151 | return false 152 | } 153 | 154 | if e == nil { 155 | panic("KVEntry cant be nil") 156 | } 157 | e.Reset() 158 | k.left-- 159 | 160 | nameLen, n, err := Varint(k.buf) 161 | if err != nil { 162 | k.lastErr = err 163 | return false 164 | } 165 | k.buf = k.buf[n:] 166 | 167 | e.name = k.buf[:nameLen] 168 | k.buf = k.buf[nameLen:] 169 | 170 | e.dataType = DataType(k.buf[0] & dataTypeMask) 171 | // just always decode the boolVar even tho its wrong. 172 | e.boolVar = k.buf[0]&dataFlagTrue > 0 173 | k.buf = k.buf[1:] 174 | 175 | switch e.dataType { 176 | case DataTypeNull, DataTypeBool: 177 | // noop 178 | 179 | case DataTypeInt32, DataTypeInt64, 180 | DataTypeUInt32, DataTypeUInt64: 181 | var v uint64 182 | v, n, k.lastErr = Varint(k.buf) 183 | if k.lastErr != nil { 184 | return false 185 | } 186 | e.intVal = int64(v) 187 | 188 | k.buf = k.buf[n:] 189 | 190 | case DataTypeIPV4: 191 | e.byteVal = k.buf[:net.IPv4len] 192 | k.buf = k.buf[net.IPv4len:] 193 | 194 | case DataTypeIPV6: 195 | e.byteVal = k.buf[:net.IPv6len] 196 | k.buf = k.buf[net.IPv6len:] 197 | 198 | case DataTypeString: 199 | nameLen, n, err := Varint(k.buf) 200 | if err != nil { 201 | k.lastErr = err 202 | return false 203 | } 204 | k.buf = k.buf[n:] 205 | 206 | e.byteVal = k.buf[:nameLen] 207 | k.buf = k.buf[nameLen:] 208 | 209 | case DataTypeBinary: 210 | valLen, n, err := Varint(k.buf) 211 | if err != nil { 212 | k.lastErr = err 213 | return false 214 | } 215 | k.buf = k.buf[n:] 216 | 217 | e.byteVal = k.buf[:valLen] 218 | k.buf = k.buf[valLen:] 219 | 220 | default: 221 | k.lastErr = fmt.Errorf("unknown data type: %x", e.dataType) 222 | return false 223 | } 224 | 225 | return true 226 | } 227 | 228 | func (k *KVScanner) Discard() error { 229 | if k.RemainingBuf() == 0 { 230 | return nil 231 | } 232 | 233 | e := AcquireKVEntry() 234 | defer ReleaseKVEntry(e) 235 | for k.Next(e) { 236 | } 237 | 238 | return k.Error() 239 | } 240 | -------------------------------------------------------------------------------- /pkg/encoding/kvwriter.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | "sync" 7 | ) 8 | 9 | var kvWriterPool = sync.Pool{ 10 | New: func() any { 11 | return NewKVWriter(nil, 0) 12 | }, 13 | } 14 | 15 | func AcquireKVWriter(buf []byte, off int) *KVWriter { 16 | w := kvWriterPool.Get().(*KVWriter) 17 | w.data = buf 18 | w.off = off 19 | return w 20 | } 21 | 22 | func ReleaseKVWriter(w *KVWriter) { 23 | w.data = nil 24 | w.off = 0 25 | kvWriterPool.Put(w) 26 | } 27 | 28 | type KVWriter struct { 29 | data []byte 30 | off int 31 | } 32 | 33 | func NewKVWriter(buf []byte, off int) *KVWriter { 34 | return &KVWriter{ 35 | data: buf, 36 | off: off, 37 | } 38 | } 39 | 40 | func (aw *KVWriter) Off() int { 41 | return aw.off 42 | } 43 | 44 | func (aw *KVWriter) Bytes() []byte { 45 | return aw.data[:aw.off] 46 | } 47 | 48 | func (aw *KVWriter) writeKey(name []byte) error { 49 | n, err := PutBytes(aw.data[aw.off:], name) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | aw.off += n 55 | return nil 56 | } 57 | 58 | func (aw *KVWriter) SetString(name string, v string) error { 59 | if err := aw.writeKey([]byte(name)); err != nil { 60 | return err 61 | } 62 | 63 | aw.data[aw.off] = byte(DataTypeString) 64 | aw.off++ 65 | 66 | n, err := PutBytes(aw.data[aw.off:], []byte(v)) 67 | if err != nil { 68 | return err 69 | } 70 | aw.off += n 71 | 72 | return nil 73 | } 74 | 75 | func (aw *KVWriter) SetBinary(name string, v []byte) error { 76 | if err := aw.writeKey([]byte(name)); err != nil { 77 | return err 78 | } 79 | 80 | aw.data[aw.off] = byte(DataTypeBinary) 81 | aw.off++ 82 | 83 | n, err := PutBytes(aw.data[aw.off:], v) 84 | if err != nil { 85 | return err 86 | } 87 | aw.off += n 88 | 89 | return nil 90 | } 91 | 92 | func (aw *KVWriter) SetNull(name string) error { 93 | if err := aw.writeKey([]byte(name)); err != nil { 94 | return err 95 | } 96 | 97 | aw.data[aw.off] = byte(DataTypeNull) 98 | aw.off++ 99 | 100 | return nil 101 | } 102 | func (aw *KVWriter) SetBool(name string, v bool) error { 103 | if err := aw.writeKey([]byte(name)); err != nil { 104 | return err 105 | } 106 | 107 | aw.data[aw.off] = byte(DataTypeBool) 108 | if v { 109 | aw.data[aw.off] |= dataFlagTrue 110 | } 111 | aw.off++ 112 | 113 | return nil 114 | } 115 | 116 | func (aw *KVWriter) SetUInt32(name string, v uint32) error { 117 | return aw.setInt(name, DataTypeUInt32, int64(v)) 118 | } 119 | 120 | func (aw *KVWriter) SetInt32(name string, v int32) error { 121 | return aw.setInt(name, DataTypeInt32, int64(v)) 122 | } 123 | 124 | func (aw *KVWriter) setInt(name string, d DataType, v int64) error { 125 | if err := aw.writeKey([]byte(name)); err != nil { 126 | return err 127 | } 128 | 129 | aw.data[aw.off] = byte(d) 130 | aw.off++ 131 | 132 | n, err := PutVarint(aw.data[aw.off:], uint64(v)) 133 | if err != nil { 134 | return err 135 | } 136 | aw.off += n 137 | 138 | return nil 139 | } 140 | 141 | func (aw *KVWriter) SetInt64(name string, v int64) error { 142 | return aw.setInt(name, DataTypeInt64, v) 143 | } 144 | func (aw *KVWriter) SetUInt64(name string, v uint64) error { 145 | return aw.setInt(name, DataTypeUInt64, int64(v)) 146 | } 147 | 148 | func (aw *KVWriter) SetAddr(name string, v netip.Addr) error { 149 | if err := aw.writeKey([]byte(name)); err != nil { 150 | return err 151 | } 152 | 153 | switch { 154 | case v.Is6(): 155 | aw.data[aw.off] = byte(DataTypeIPV6) 156 | case v.Is4(): 157 | aw.data[aw.off] = byte(DataTypeIPV4) 158 | default: 159 | return fmt.Errorf("invalid address") 160 | } 161 | aw.off++ 162 | 163 | n, err := PutAddr(aw.data[aw.off:], v) 164 | if err != nil { 165 | return err 166 | } 167 | aw.off += n 168 | 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /pkg/encoding/kvwriter_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/dropmorepackets/haproxy-go/pkg/testutil" 8 | ) 9 | 10 | func TestKVWriter(t *testing.T) { 11 | buf := make([]byte, 16386) 12 | 13 | const exampleKey, exampleValue = "key", "value" 14 | testutil.WithoutAllocations(t, func() { 15 | aw := NewKVWriter(buf, 0) 16 | 17 | if err := aw.SetString(exampleKey, exampleValue); err != nil { 18 | t.Error(err) 19 | } 20 | 21 | buf = aw.Bytes() 22 | }) 23 | 24 | const expectedValue = "036b6579080576616c7565" 25 | if s := fmt.Sprintf("%x", buf); s != expectedValue { 26 | t.Errorf("result doesnt match golden string: %s != %s", expectedValue, s) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/encoding/messagescanner.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var messagePool = sync.Pool{ 8 | New: func() any { 9 | return &Message{} 10 | }, 11 | } 12 | 13 | var messageScannerPool = sync.Pool{ 14 | New: func() any { 15 | return NewMessageScanner(nil) 16 | }, 17 | } 18 | 19 | func AcquireMessageScanner(buf []byte) *MessageScanner { 20 | s := messageScannerPool.Get().(*MessageScanner) 21 | s.buf = buf 22 | s.lastErr = nil 23 | return s 24 | } 25 | 26 | func ReleaseMessageScanner(s *MessageScanner) { 27 | s.buf = nil 28 | s.lastErr = nil 29 | messageScannerPool.Put(s) 30 | } 31 | 32 | func AcquireMessage() *Message { 33 | return messagePool.Get().(*Message) 34 | } 35 | 36 | func ReleaseMessage(m *Message) { 37 | m.name = nil 38 | m.KV = nil 39 | m.kvEntryCount = 0 40 | 41 | messagePool.Put(m) 42 | } 43 | 44 | type Message struct { 45 | KV *KVScanner 46 | name []byte 47 | 48 | kvEntryCount byte 49 | } 50 | 51 | func (m *Message) NameBytes() []byte { 52 | return m.name 53 | } 54 | 55 | type MessageScanner struct { 56 | lastErr error 57 | buf []byte 58 | } 59 | 60 | func NewMessageScanner(b []byte) *MessageScanner { 61 | return &MessageScanner{buf: b} 62 | } 63 | 64 | func (s *MessageScanner) Error() error { 65 | return s.lastErr 66 | } 67 | 68 | func (s *MessageScanner) Next(m *Message) bool { 69 | if m.KV != nil { 70 | // if the scanner is still existing from a previous read 71 | // forward the current slice to the correct position 72 | s.buf = s.buf[len(s.buf)-m.KV.RemainingBuf():] 73 | ReleaseKVScanner(m.KV) 74 | m.KV = nil 75 | } 76 | 77 | if len(s.buf) == 0 { 78 | return false 79 | } 80 | 81 | nameLen, n, err := Varint(s.buf) 82 | if err != nil { 83 | s.lastErr = err 84 | return false 85 | } 86 | s.buf = s.buf[n:] 87 | 88 | m.name = s.buf[:nameLen] 89 | s.buf = s.buf[nameLen:] 90 | 91 | m.kvEntryCount = s.buf[0] 92 | s.buf = s.buf[1:] 93 | 94 | m.KV = AcquireKVScanner(s.buf, int(m.kvEntryCount)) 95 | 96 | return true 97 | } 98 | -------------------------------------------------------------------------------- /pkg/encoding/varint.go: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 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 | // This code is copied from https://github.com/criteo/haproxy-spoe-go/blob/master/encoding.go 16 | 17 | package encoding 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | ) 23 | 24 | var ( 25 | ErrUnterminatedSequence = fmt.Errorf("unterminated sequence") 26 | ErrInsufficientSpace = fmt.Errorf("insufficient space in buffer") 27 | ) 28 | 29 | func ReadVarint(rd io.ByteReader) (uint64, error) { 30 | b, err := rd.ReadByte() 31 | if err != nil { 32 | return 0, ErrUnterminatedSequence 33 | } 34 | 35 | val := uint64(b) 36 | off := 1 37 | 38 | if val < 240 { 39 | return val, nil 40 | } 41 | 42 | r := uint(4) 43 | for { 44 | b, err := rd.ReadByte() 45 | if err != nil { 46 | return 0, ErrUnterminatedSequence 47 | } 48 | 49 | v := uint64(b) 50 | val += v << r 51 | off++ 52 | r += 7 53 | 54 | if v < 128 { 55 | break 56 | } 57 | } 58 | 59 | return val, nil 60 | } 61 | 62 | func PutVarint(b []byte, i uint64) (int, error) { 63 | if len(b) == 0 { 64 | return 0, ErrInsufficientSpace 65 | } 66 | 67 | if i < 240 { 68 | b[0] = byte(i) 69 | return 1, nil 70 | } 71 | 72 | n := 0 73 | b[n] = byte(i) | 240 74 | n++ 75 | i = (i - 240) >> 4 76 | for i >= 128 { 77 | if n > len(b)-1 { 78 | return 0, ErrInsufficientSpace 79 | } 80 | 81 | b[n] = byte(i) | 128 82 | n++ 83 | i = (i - 128) >> 7 84 | } 85 | 86 | if n > len(b)-1 { 87 | return 0, ErrInsufficientSpace 88 | } 89 | 90 | b[n] = byte(i) 91 | n++ 92 | 93 | return n, nil 94 | } 95 | 96 | func Varint(b []byte) (uint64, int, error) { 97 | if len(b) == 0 { 98 | return 0, 0, ErrUnterminatedSequence 99 | } 100 | val := uint64(b[0]) 101 | off := 1 102 | 103 | if val < 240 { 104 | return val, 1, nil 105 | } 106 | 107 | r := uint(4) 108 | for { 109 | if off > len(b)-1 { 110 | return 0, 0, ErrUnterminatedSequence 111 | } 112 | 113 | v := uint64(b[off]) 114 | val += v << r 115 | off++ 116 | r += 7 117 | 118 | if v < 128 { 119 | break 120 | } 121 | } 122 | 123 | return val, off, nil 124 | } 125 | -------------------------------------------------------------------------------- /pkg/encoding/varint_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_encode(t *testing.T) { 9 | type args struct { 10 | val uint64 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want []byte 16 | }{ 17 | { 18 | name: "", 19 | args: args{ 20 | val: 0x1234, 21 | }, 22 | want: []byte{0xf4, 0x94, 0x01}, 23 | }, 24 | } 25 | for _, tt := range tests { 26 | t.Run(tt.name, func(t *testing.T) { 27 | b := make([]byte, 4) 28 | if n, _ := PutVarint(b, tt.args.val); !reflect.DeepEqual(b[:n], tt.want) { 29 | t.Errorf("encode() = %v, want %v", b[:n], tt.want) 30 | } 31 | 32 | if got, _, _ := Varint(tt.want); !reflect.DeepEqual(got, tt.args.val) { 33 | t.Errorf("decode() = %v, want %v", got, tt.args.val) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/testutil/allocs.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const runsPerTest = 10 8 | 9 | func WithoutAllocations(tb testing.TB, fn func()) { 10 | WithNAllocations(tb, 0, fn) 11 | } 12 | 13 | func WithNAllocations(tb testing.TB, n uint64, fn func()) { 14 | avg := testing.AllocsPerRun(runsPerTest, fn) 15 | 16 | // early exit when failed 17 | if tb.Failed() { 18 | return 19 | } 20 | 21 | if uint64(avg) != n { 22 | tb.Errorf("got %v allocs, want %d allocs", avg, n) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/testutil/allocs_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAllocTests(t *testing.T) { 8 | t.Run("without allocations", func(t *testing.T) { 9 | WithoutAllocations(t, func() { 10 | v := make([]byte, 10) 11 | copy(v, []byte{1, 2, 3, 4}) 12 | }) 13 | }) 14 | 15 | t.Run("with allocation", func(t *testing.T) { 16 | WithNAllocations(t, 1, func() { 17 | v := make([]byte, 10) 18 | v = append(v, 1) 19 | _ = v 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /pkg/testutil/haproxy.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net" 7 | "os" 8 | "testing" 9 | "text/template" 10 | "time" 11 | ) 12 | 13 | const haproxyConfigTemplate = ` 14 | global 15 | log stdout format short daemon 16 | stats socket {{ .StatsSocket }} mode 660 level admin expose-fd listeners 17 | stats timeout 30s 18 | 19 | defaults 20 | log global 21 | option httplog 22 | timeout connect 1s 23 | timeout server 5s 24 | timeout client 5s 25 | 26 | {{ .CustomConfig }} 27 | 28 | frontend test 29 | mode http 30 | bind 127.0.0.1:{{ .FrontendPort }} 31 | bind unix@{{ .FrontendSocket }} accept-proxy 32 | 33 | {{- if .EngineConfigFile }} 34 | filter spoe engine e2e config {{ .EngineConfigFile }} 35 | {{ end -}} 36 | 37 | {{ .CustomFrontendConfig }} 38 | 39 | use_backend backend 40 | 41 | backend backend 42 | mode http 43 | 44 | {{ .CustomBackendConfig }} 45 | 46 | {{ .BackendConfig }} 47 | 48 | {{ if .EngineAddr -}} 49 | backend e2e-spoa 50 | mode tcp 51 | server e2e {{ .EngineAddr }} 52 | {{ end }} 53 | 54 | {{ if .PeerAddr -}} 55 | peers mypeers 56 | peer {{ .InstanceID }} {{ .LocalPeerAddr }} 57 | peer go_client {{ .PeerAddr }} 58 | {{ end }} 59 | 60 | ` 61 | 62 | const haproxyEngineConfig = ` 63 | [e2e] 64 | spoe-agent e2e-agent 65 | messages e2e-req e2e-res 66 | option var-prefix e2e 67 | option set-on-error error 68 | timeout hello 100ms 69 | timeout idle 10s 70 | timeout processing 500ms 71 | use-backend e2e-spoa 72 | log global 73 | 74 | spoe-message e2e-req 75 | args id=unique-id src-ip=src method=method path=path query=query version=req.ver headers=req.hdrs body=req.body 76 | event on-frontend-http-request 77 | 78 | spoe-message e2e-res 79 | args id=unique-id version=res.ver status=status headers=res.hdrs body=res.body 80 | event on-http-response 81 | ` 82 | 83 | type HAProxyConfig struct { 84 | EngineAddr string 85 | PeerAddr string 86 | EngineConfig string 87 | FrontendPort string 88 | CustomFrontendConfig string 89 | BackendConfig string 90 | CustomBackendConfig string 91 | CustomConfig string 92 | } 93 | 94 | func (cfg HAProxyConfig) Run(tb testing.TB) string { 95 | tb.Helper() 96 | 97 | if cfg.EngineConfig == "" { 98 | cfg.EngineConfig = haproxyEngineConfig 99 | } 100 | 101 | if cfg.BackendConfig == "" { 102 | cfg.BackendConfig = ` 103 | http-request return status 200 content-type "text/plain" string "Hello World!\n" 104 | ` 105 | } 106 | 107 | tmpDir, err := os.MkdirTemp("", fmt.Sprintf("haproxy_%s", cfg.FrontendPort)) 108 | if err != nil { 109 | tb.Fatal(err) 110 | } 111 | tb.Cleanup(func() { 112 | os.RemoveAll(tmpDir) 113 | }) 114 | 115 | type tmplCfg struct { 116 | HAProxyConfig 117 | 118 | StatsSocket string 119 | FrontendSocket string 120 | InstanceID string 121 | LocalPeerAddr string 122 | EngineConfigFile string 123 | } 124 | var tcfg tmplCfg 125 | tcfg.HAProxyConfig = cfg 126 | tcfg.InstanceID = fmt.Sprintf("instance_%s", cfg.FrontendPort) 127 | tcfg.LocalPeerAddr = fmt.Sprintf("127.0.0.1:%d", TCPPort(tb)) 128 | tcfg.StatsSocket = fmt.Sprintf("%s/stats.sock", tmpDir) 129 | tcfg.FrontendSocket = fmt.Sprintf("%s/frontend.sock", tmpDir) 130 | 131 | if cfg.EngineAddr != "" { 132 | engineConfigFile := TempFile(tb, "e2e.cfg", cfg.EngineConfig) 133 | tcfg.EngineConfigFile = engineConfigFile 134 | } 135 | 136 | haproxyConfig := mustExecuteTemplate(tb, haproxyConfigTemplate, tcfg) 137 | haproxyConfigFile := TempFile(tb, "haproxy.cfg", haproxyConfig) 138 | 139 | tb.Cleanup(func() { 140 | if tb.Failed() { 141 | tb.Logf("HAProxy Config: \n%s", haproxyConfig) 142 | } 143 | }) 144 | 145 | RunProcess(tb, "haproxy", []string{"-f", haproxyConfigFile, "-L", tcfg.InstanceID}) 146 | 147 | c := make(chan bool) 148 | defer close(c) 149 | 150 | go func() { 151 | for { 152 | l, err := net.Dial("unix", tcfg.StatsSocket) 153 | if err != nil { 154 | continue 155 | } 156 | l.Close() 157 | 158 | l, err = net.Dial("unix", tcfg.FrontendSocket) 159 | if err != nil { 160 | continue 161 | } 162 | l.Close() 163 | 164 | // if we were able to connect, exit and let the test run 165 | break 166 | } 167 | c <- true 168 | }() 169 | 170 | select { 171 | case <-time.After(3 * time.Second): 172 | tb.Fatal("timeout while waiting for haproxy") 173 | case <-c: 174 | } 175 | 176 | return tcfg.FrontendSocket 177 | } 178 | 179 | func mustExecuteTemplate(tb testing.TB, text string, data any) string { 180 | tb.Helper() 181 | tmpl, err := template.New("").Parse(text) 182 | if err != nil { 183 | tb.Fatal(err) 184 | } 185 | 186 | var tmplBuf bytes.Buffer 187 | if err := tmpl.Execute(&tmplBuf, data); err != nil { 188 | tb.Fatal(err) 189 | } 190 | 191 | return tmplBuf.String() 192 | } 193 | -------------------------------------------------------------------------------- /pkg/testutil/net.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func TCPListener(tb testing.TB) net.Listener { 9 | tb.Helper() 10 | l, err := net.Listen("tcp", ":0") 11 | if err != nil { 12 | tb.Fatal(err) 13 | } 14 | tb.Cleanup(func() { 15 | _ = l.Close() 16 | }) 17 | return l 18 | } 19 | 20 | func TCPPort(tb testing.TB) int { 21 | tb.Helper() 22 | l, err := net.Listen("tcp", ":0") 23 | if err != nil { 24 | tb.Fatal(err) 25 | } 26 | defer l.Close() 27 | return l.Addr().(*net.TCPAddr).Port 28 | } 29 | -------------------------------------------------------------------------------- /pkg/testutil/pipeconn.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | type rwPipeConn struct { 12 | rw *rwPipe 13 | closedMtx sync.Mutex 14 | closed bool 15 | } 16 | 17 | func (c *rwPipeConn) Network() string { 18 | return "pipe" 19 | } 20 | 21 | func (c *rwPipeConn) String() string { 22 | return "pipe" 23 | } 24 | 25 | func (c *rwPipeConn) Read(b []byte) (n int, err error) { 26 | c.closedMtx.Lock() 27 | if c.closed { 28 | c.closedMtx.Unlock() 29 | return 0, net.ErrClosed 30 | } 31 | c.closedMtx.Unlock() 32 | 33 | return c.rw.Read(b) 34 | } 35 | 36 | func (c *rwPipeConn) Write(b []byte) (n int, err error) { 37 | c.closedMtx.Lock() 38 | if c.closed { 39 | c.closedMtx.Unlock() 40 | return 0, net.ErrClosed 41 | } 42 | c.closedMtx.Unlock() 43 | 44 | return c.rw.Write(b) 45 | } 46 | 47 | func (c *rwPipeConn) Close() error { 48 | c.closedMtx.Lock() 49 | defer c.closedMtx.Unlock() 50 | 51 | c.closed = true 52 | return c.rw.Close() 53 | } 54 | 55 | func (c *rwPipeConn) LocalAddr() net.Addr { 56 | return c 57 | } 58 | 59 | func (c *rwPipeConn) RemoteAddr() net.Addr { 60 | return c 61 | } 62 | 63 | func (c *rwPipeConn) SetDeadline(t time.Time) error { 64 | return errors.ErrUnsupported 65 | } 66 | 67 | func (c *rwPipeConn) SetReadDeadline(t time.Time) error { 68 | return errors.ErrUnsupported 69 | } 70 | 71 | func (c *rwPipeConn) SetWriteDeadline(t time.Time) error { 72 | return errors.ErrUnsupported 73 | } 74 | 75 | func PipeConn() (io.ReadWriteCloser, net.Conn) { 76 | a, b := newRWPipe() 77 | return a, &rwPipeConn{rw: b} 78 | } 79 | 80 | func newRWPipe() (a *rwPipe, b *rwPipe) { 81 | rr, rw := io.Pipe() 82 | wr, ww := io.Pipe() 83 | 84 | return &rwPipe{rr, ww}, &rwPipe{wr, rw} 85 | } 86 | 87 | type rwPipe struct { 88 | r *io.PipeReader 89 | w *io.PipeWriter 90 | } 91 | 92 | func (r *rwPipe) Close() error { 93 | return errors.Join(r.r.Close(), r.w.Close()) 94 | } 95 | 96 | func (r *rwPipe) Read(p []byte) (n int, err error) { 97 | return r.r.Read(p) 98 | } 99 | 100 | func (r *rwPipe) Write(p []byte) (n int, err error) { 101 | return r.w.Write(p) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/testutil/pipeconn_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestPipeConn(t *testing.T) { 9 | rw, netConn := PipeConn() 10 | 11 | var a, b = []byte("abc"), []byte("def") 12 | go func() { 13 | if _, err := rw.Write(a); err != nil { 14 | t.Error(err) 15 | } 16 | 17 | if _, err := netConn.Write(b); err != nil { 18 | t.Error(err) 19 | } 20 | }() 21 | 22 | expectA := make([]byte, len(a)) 23 | if _, err := netConn.Read(expectA); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if !bytes.Equal(a, expectA) { 28 | t.Fatal("data doesnt match") 29 | } 30 | 31 | expectB := make([]byte, len(b)) 32 | if _, err := rw.Read(expectB); err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | if !bytes.Equal(b, expectB) { 37 | t.Fatal("data doesnt match") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/testutil/reader.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import "io" 4 | 5 | func NewRepeatReader(data []byte) *RepeatReader { 6 | return &RepeatReader{data: data} 7 | } 8 | 9 | type RepeatReader struct { 10 | data []byte 11 | offset int 12 | } 13 | 14 | func (r *RepeatReader) Read(p []byte) (n int, err error) { 15 | if len(r.data) == 0 { 16 | return 0, io.EOF 17 | } 18 | 19 | for n < len(p) { 20 | remaining := len(r.data) - r.offset 21 | if remaining == 0 { 22 | r.offset = 0 23 | continue 24 | } 25 | 26 | // Calculate how many bytes to copy 27 | toCopy := min(len(p)-n, remaining) 28 | copy(p[n:], r.data[r.offset:r.offset+toCopy]) 29 | n += toCopy 30 | r.offset += toCopy 31 | } 32 | 33 | return n, nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/testutil/reader_test.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestRepeatReader_Read(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | data []byte 12 | out []byte 13 | }{ 14 | {name: "same size", data: []byte{1, 2, 3}, out: []byte{1, 2, 3}}, 15 | {name: "double size", data: []byte{1, 2, 3}, out: []byte{1, 2, 3, 1, 2, 3}}, 16 | {name: "huge size", data: []byte{1, 2, 3}, out: bytes.Repeat([]byte{1, 2, 3}, 123)}, 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | r := &RepeatReader{ 21 | data: tt.data, 22 | } 23 | buf := make([]byte, len(tt.out)) 24 | _, err := r.Read(buf) 25 | if err != nil { 26 | t.Errorf("Read() error = %v", err) 27 | return 28 | } 29 | if !bytes.Equal(buf, tt.out) { 30 | t.Errorf("Equal(): %v != %v", buf, tt.out) 31 | return 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/testutil/subprocess.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func RunProcess(tb testing.TB, cmd string, args []string) { 11 | tb.Helper() 12 | 13 | cmdString := cmd 14 | if len(args) != 0 { 15 | cmdString += " " + strings.Join(args, " ") 16 | } 17 | 18 | var stdout, stderr bytes.Buffer 19 | c := exec.Command(cmd, args...) 20 | c.Stdout = &stdout 21 | c.Stderr = &stderr 22 | 23 | tb.Cleanup(func() { 24 | if c.Process == nil { 25 | return 26 | } 27 | 28 | if err := c.Process.Kill(); err != nil { 29 | tb.Errorf("while killing: %q: %v", cmdString, err) 30 | } 31 | 32 | // ignore the exit result 33 | _ = c.Wait() 34 | 35 | if tb.Failed() { 36 | tb.Logf("Subprocess %q stdout: \n%s", cmdString, stdout.String()) 37 | tb.Logf("Subprocess %q stderr: \n%s", cmdString, stderr.String()) 38 | } 39 | }) 40 | 41 | if err := c.Start(); err != nil { 42 | tb.Fatalf("while running subprocess: %q: %v", cmdString, err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/testutil/testfile.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // TempFile creates a temporary file that just needs to be deleted with 9 | // os.Remove(f) 10 | func TempFile(tb testing.TB, name, content string) string { 11 | tb.Helper() 12 | 13 | f, err := os.CreateTemp("", name) 14 | if err != nil { 15 | tb.Fatal(err) 16 | } 17 | 18 | if _, err := f.WriteString(content); err != nil { 19 | tb.Fatal(err) 20 | } 21 | 22 | if err := f.Close(); err != nil { 23 | tb.Fatal(err) 24 | } 25 | 26 | tb.Cleanup(func() { 27 | os.Remove(f.Name()) 28 | }) 29 | 30 | return f.Name() 31 | } 32 | -------------------------------------------------------------------------------- /spop/README.md: -------------------------------------------------------------------------------- 1 | # A HAProxy SPOE implementation in Go 2 | 3 | 4 | # References 5 | https://www.haproxy.org/download/2.0/doc/SPOE.txt 6 | 7 | # Alternative implementations 8 | https://github.com/criteo/haproxy-spoe-go 9 | https://github.com/negasus/haproxy-spoe-go 10 | -------------------------------------------------------------------------------- /spop/agent.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "math" 9 | "net" 10 | "runtime" 11 | ) 12 | 13 | type Agent struct { 14 | Handler Handler 15 | BaseContext context.Context 16 | Addr string 17 | } 18 | 19 | func ListenAndServe(addr string, handler Handler) error { 20 | a := Agent{Addr: addr, Handler: handler} 21 | return a.ListenAndServe() 22 | } 23 | 24 | func (a *Agent) ListenAndServe() error { 25 | l, err := net.Listen("tcp", a.Addr) 26 | if err != nil { 27 | return fmt.Errorf("opening listener: %w", err) 28 | } 29 | defer l.Close() 30 | 31 | return a.Serve(l) 32 | } 33 | 34 | func (a *Agent) Serve(l net.Listener) error { 35 | a.Addr = l.Addr().String() 36 | if a.BaseContext == nil { 37 | a.BaseContext = context.Background() 38 | } 39 | 40 | go func() { 41 | <-a.BaseContext.Done() 42 | l.Close() 43 | }() 44 | 45 | as := newAsyncScheduler() 46 | for { 47 | nc, err := l.Accept() 48 | if err != nil { 49 | return fmt.Errorf("accepting conn: %w", err) 50 | } 51 | 52 | if tcp, ok := nc.(*net.TCPConn); ok { 53 | err = tcp.SetWriteBuffer(math.MaxUint16) // 64KB seems like a fair buffer size 54 | if err != nil { 55 | return err 56 | } 57 | err = tcp.SetReadBuffer(math.MaxUint16) // 64KB seems like a fair buffer size 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | p := newProtocolClient(a.BaseContext, nc, as, a.Handler) 64 | go func() { 65 | defer nc.Close() 66 | defer p.Close() 67 | 68 | // don't let panics inside the protocol kill the entire library 69 | if err := wrapPanic(p.Serve); err != nil && !errors.Is(err, p.ctx.Err()) { 70 | log.Println(err) 71 | } 72 | }() 73 | } 74 | } 75 | 76 | func wrapPanic(fn func() error) (err error) { 77 | didPanic := true 78 | defer func() { 79 | if didPanic { 80 | if e := recover(); e != nil { 81 | const size = 64 << 10 82 | buf := make([]byte, size) 83 | buf = buf[:runtime.Stack(buf, false)] 84 | err = fmt.Errorf("spop: panic: %v\n%s", e, buf) 85 | } 86 | } 87 | }() 88 | err = fn() 89 | didPanic = false 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /spop/benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Benchmarks comparing the different SPOP libraries. Currently only raw decoding and no proper e2e flow. 4 | 5 | Criteo Library is replaced with a slightly modified fork by Babiel 6 | https://github.com/babiel/haproxy-spoe-go 7 | 8 | --- 9 | 10 | Tested and compared via benchstat (thx to https://www.rodolfocarvalho.net/blog/go-test-bench-pipe-to-benchstat/) 11 | 12 | go test -run='^$' -bench=. -benchmem -count=5 | tee >(benchstat /dev/stdin) 13 | 14 | --- 15 | 16 | ``` 17 | goos: linux 18 | goarch: amd64 19 | pkg: github.com/dropmorepackets/haproxy-go/spop/benchmarks 20 | cpu: AMD EPYC 7502P 32-Core Processor 21 | BenchmarkCriteo-48 5874009 245.8 ns/op 263 B/op 12 allocs/op 22 | BenchmarkCriteo-48 4504574 266.3 ns/op 263 B/op 12 allocs/op 23 | BenchmarkCriteo-48 4511300 272.8 ns/op 263 B/op 12 allocs/op 24 | BenchmarkCriteo-48 4336767 279.2 ns/op 263 B/op 12 allocs/op 25 | BenchmarkCriteo-48 4241575 267.2 ns/op 263 B/op 12 allocs/op 26 | BenchmarkCriteo-48 4719711 274.6 ns/op 263 B/op 12 allocs/op 27 | BenchmarkCriteo-48 4419110 255.4 ns/op 263 B/op 12 allocs/op 28 | BenchmarkCriteo-48 5013790 270.8 ns/op 263 B/op 12 allocs/op 29 | BenchmarkCriteo-48 4283295 267.7 ns/op 263 B/op 12 allocs/op 30 | BenchmarkCriteo-48 4446008 270.4 ns/op 263 B/op 12 allocs/op 31 | BenchmarkNegasus-48 1668440 725.3 ns/op 755 B/op 18 allocs/op 32 | BenchmarkNegasus-48 1583863 763.6 ns/op 755 B/op 18 allocs/op 33 | BenchmarkNegasus-48 1592184 730.7 ns/op 755 B/op 18 allocs/op 34 | BenchmarkNegasus-48 1579813 755.2 ns/op 755 B/op 18 allocs/op 35 | BenchmarkNegasus-48 1626435 731.5 ns/op 755 B/op 18 allocs/op 36 | BenchmarkNegasus-48 1656385 751.8 ns/op 755 B/op 18 allocs/op 37 | BenchmarkNegasus-48 1610750 735.7 ns/op 755 B/op 18 allocs/op 38 | BenchmarkNegasus-48 1632219 750.6 ns/op 755 B/op 18 allocs/op 39 | BenchmarkNegasus-48 1685029 709.3 ns/op 755 B/op 18 allocs/op 40 | BenchmarkNegasus-48 1649761 730.2 ns/op 755 B/op 18 allocs/op 41 | BenchmarkDropMorePackets-48 120675940 10.50 ns/op 0 B/op 0 allocs/op 42 | BenchmarkDropMorePackets-48 93222517 16.45 ns/op 0 B/op 0 allocs/op 43 | BenchmarkDropMorePackets-48 100000000 14.19 ns/op 0 B/op 0 allocs/op 44 | BenchmarkDropMorePackets-48 93988230 11.41 ns/op 0 B/op 0 allocs/op 45 | BenchmarkDropMorePackets-48 97593783 13.23 ns/op 0 B/op 0 allocs/op 46 | BenchmarkDropMorePackets-48 79098175 16.19 ns/op 0 B/op 0 allocs/op 47 | BenchmarkDropMorePackets-48 95429886 13.68 ns/op 0 B/op 0 allocs/op 48 | BenchmarkDropMorePackets-48 119408089 13.65 ns/op 0 B/op 0 allocs/op 49 | BenchmarkDropMorePackets-48 80430522 17.22 ns/op 0 B/op 0 allocs/op 50 | BenchmarkDropMorePackets-48 111808652 11.97 ns/op 0 B/op 0 allocs/op 51 | PASS 52 | ok github.com/dropmorepackets/haproxy-go/spop/benchmarks 51.642s 53 | goos: linux 54 | goarch: amd64 55 | pkg: github.com/dropmorepackets/haproxy-go/spop/benchmarks 56 | cpu: AMD EPYC 7502P 32-Core Processor 57 | │ /dev/stdin │ 58 | │ sec/op │ 59 | Criteo-48 269.0n ± 5% 60 | Negasus-48 733.6n ± 3% 61 | DropMorePackets-48 13.66n ± 20% 62 | geomean 139.2n 63 | 64 | │ /dev/stdin │ 65 | │ B/op │ 66 | Criteo-48 263.0 ± 0% 67 | Negasus-48 755.0 ± 0% 68 | DropMorePackets-48 0.000 ± 0% 69 | geomean ¹ 70 | ¹ summaries must be >0 to compute geomean 71 | 72 | │ /dev/stdin │ 73 | │ allocs/op │ 74 | Criteo-48 12.00 ± 0% 75 | Negasus-48 18.00 ± 0% 76 | DropMorePackets-48 0.000 ± 0% 77 | geomean ¹ 78 | ¹ summaries must be >0 to compute geomean 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /spop/benchmarks/benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | criteo "github.com/criteo/haproxy-spoe-go" 8 | "github.com/negasus/haproxy-spoe-go/message" 9 | 10 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 11 | ) 12 | 13 | var ( 14 | // a raw message from the test environment at @babiel 15 | msgInput, _ = hex.DecodeString("06766572696679050c636f6f6b69655f76616c7565001064657374696e6174696f6e5f686f7374080d69702e62616269656c2e636f6d0b71756572795f76616c75650009736f757263655f6970060a0900010866726f6e74656e64082067656e66726f6e74656e645f36303031302d64646f735f62616269656c5f6970") 16 | dis Dispatcher 17 | ) 18 | 19 | type Dispatcher struct { 20 | } 21 | 22 | func (d *Dispatcher) ServeCriteo(messages *criteo.MessageIterator) ([]criteo.Action, error) { 23 | for messages.Next() { 24 | for messages.Message.Args.Next() { 25 | } 26 | } 27 | 28 | if err := messages.Error(); err != nil { 29 | return nil, err 30 | } 31 | 32 | return nil, nil 33 | } 34 | 35 | func (d *Dispatcher) ServeDropMorePacket(w *encoding.ActionWriter, m *encoding.Message) { 36 | k := encoding.AcquireKVEntry() 37 | defer encoding.ReleaseKVEntry(k) 38 | 39 | for m.KV.Next(k) { 40 | } 41 | 42 | if err := m.KV.Error(); err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | func (d *Dispatcher) ServeNegasus(m *message.Messages) { 48 | for i := 0; i < m.Len(); i++ { 49 | m, err := m.GetByIndex(i) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | for range m.KV.Data() { 55 | } 56 | } 57 | } 58 | 59 | func BenchmarkCriteo(b *testing.B) { 60 | b.RunParallel(func(pb *testing.PB) { 61 | for pb.Next() { 62 | _, _ = dis.ServeCriteo(criteo.NewMessageIterator(msgInput)) 63 | } 64 | }) 65 | } 66 | 67 | func BenchmarkNegasus(b *testing.B) { 68 | b.RunParallel(func(pb *testing.PB) { 69 | m := message.NewMessages() 70 | for pb.Next() { 71 | if err := m.Decode(msgInput); err != nil { 72 | b.Fatal(err) 73 | } 74 | 75 | dis.ServeNegasus(m) 76 | *m = (*m)[:0] // let's be fair and reuse the slice. 77 | } 78 | }) 79 | } 80 | 81 | func BenchmarkDropMorePackets(b *testing.B) { 82 | b.RunParallel(func(pb *testing.PB) { 83 | // I am unfair against myself as these structures aren't always 84 | // reacquired, but let's do it anyway. 85 | for pb.Next() { 86 | m := encoding.AcquireMessage() 87 | // we don't write any actions right now 88 | w := encoding.AcquireActionWriter(nil, 0) 89 | s := encoding.AcquireMessageScanner(msgInput) 90 | 91 | for s.Next(m) { 92 | dis.ServeDropMorePacket(w, m) 93 | } 94 | 95 | if err := s.Error(); err != nil { 96 | b.Fatal(err) 97 | } 98 | 99 | encoding.ReleaseMessageScanner(s) 100 | encoding.ReleaseActionWriter(w) 101 | encoding.ReleaseMessage(m) 102 | } 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /spop/benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go/spop/benchmarks 2 | 3 | go 1.21 4 | 5 | replace github.com/dropmorepackets/haproxy-go => ../../ 6 | 7 | // Replace to allow benchmarking 8 | replace github.com/criteo/haproxy-spoe-go => github.com/babiel/haproxy-spoe-go v1.0.7-0.20220317153857-9119f3323ea8 9 | 10 | require ( 11 | github.com/criteo/haproxy-spoe-go v0.0.0 12 | github.com/dropmorepackets/haproxy-go v0.0.0 13 | github.com/negasus/haproxy-spoe-go v1.0.4 14 | ) 15 | 16 | require ( 17 | github.com/libp2p/go-buffer-pool v0.0.2 // indirect 18 | github.com/pkg/errors v0.9.1 // indirect 19 | github.com/sirupsen/logrus v1.7.0 // indirect 20 | golang.org/x/net v0.16.0 // indirect 21 | golang.org/x/sys v0.13.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /spop/benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/babiel/haproxy-spoe-go v1.0.7-0.20220317153857-9119f3323ea8 h1:7pSyk8QJPlP4M7BbBcSRl35WJwqABDQ5vG4769rLD2U= 2 | github.com/babiel/haproxy-spoe-go v1.0.7-0.20220317153857-9119f3323ea8/go.mod h1:WT3an2m8Hl5koUjhgFJ7WHfa2QCqWZV7UKcP73wfGS4= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 11 | github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= 12 | github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= 13 | github.com/negasus/haproxy-spoe-go v1.0.4 h1:9Azs7eSeCdA5KzsczurQRcxihBfNsQm2zhfwkC0FhFQ= 14 | github.com/negasus/haproxy-spoe-go v1.0.4/go.mod h1:ZjphHzEwXT2qReiRl8oDtYKrBOB33gpkQt1f+MgzM04= 15 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 16 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 20 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 23 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 25 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 26 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 27 | golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= 28 | golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 29 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 33 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 35 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 36 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 39 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 40 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | -------------------------------------------------------------------------------- /spop/e2e_test.go: -------------------------------------------------------------------------------- 1 | //go:build e2e 2 | 3 | package spop 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "net/http" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 14 | "github.com/dropmorepackets/haproxy-go/pkg/testutil" 15 | ) 16 | 17 | func TestE2E(t *testing.T) { 18 | tests := []E2ETest{ 19 | { 20 | name: "default", 21 | hf: func(_ context.Context, w *encoding.ActionWriter, m *encoding.Message) {}, 22 | tf: func(t *testing.T, config testutil.HAProxyConfig) { 23 | resp, err := http.Get("http://127.0.0.1:" + config.FrontendPort) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | if resp.StatusCode != http.StatusOK { 29 | t.Fatalf("expected %d; got %d", http.StatusOK, resp.StatusCode) 30 | } 31 | }, 32 | }, 33 | { 34 | name: "status-code acl", 35 | hf: func(_ context.Context, w *encoding.ActionWriter, m *encoding.Message) { 36 | err := w.SetInt64(encoding.VarScopeTransaction, "statuscode", http.StatusUnauthorized) 37 | if err != nil { 38 | t.Fatalf("writing status-code: %v", err) 39 | } 40 | }, 41 | tf: func(t *testing.T, config testutil.HAProxyConfig) { 42 | resp, err := http.Get("http://127.0.0.1:" + config.FrontendPort) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | if resp.StatusCode != http.StatusUnauthorized { 48 | t.Fatalf("expected %d; got %d", http.StatusUnauthorized, resp.StatusCode) 49 | } 50 | }, 51 | backendCfg: "http-request return status 401 if { var(txn.e2e.statuscode) -m int eq 401 }", 52 | }, 53 | { 54 | name: "ctx cancel on disconnect", 55 | hf: func(ctx context.Context, w *encoding.ActionWriter, m *encoding.Message) { 56 | select { 57 | case <-ctx.Done(): 58 | case <-time.After(5 * time.Second): 59 | panic("ctx not cancelled") 60 | } 61 | }, 62 | tf: func(t *testing.T, config testutil.HAProxyConfig) { 63 | resp, err := http.Get("http://127.0.0.1:" + config.FrontendPort) 64 | if err != nil { 65 | t.Fatal(err) 66 | } 67 | 68 | if resp.StatusCode != http.StatusUnauthorized { 69 | t.Fatalf("expected %d; got %d", http.StatusUnauthorized, resp.StatusCode) 70 | } 71 | }, 72 | backendCfg: "http-request return status 401 if { var(txn.e2e.error) -m found }", 73 | }, 74 | { 75 | name: "recover from panic", 76 | hf: func(ctx context.Context, w *encoding.ActionWriter, m *encoding.Message) { 77 | panic("example panic") 78 | }, 79 | tf: func(t *testing.T, config testutil.HAProxyConfig) { 80 | resp, err := http.Get("http://127.0.0.1:" + config.FrontendPort) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if resp.StatusCode != http.StatusOK { 86 | t.Fatalf("expected %d; got %d", http.StatusOK, resp.StatusCode) 87 | } 88 | }, 89 | }, 90 | { 91 | name: "high request rate", 92 | hf: func(ctx context.Context, w *encoding.ActionWriter, m *encoding.Message) { 93 | k := encoding.AcquireKVEntry() 94 | m.KV.Next(k) 95 | if !k.NameEquals("id") { 96 | t.Errorf("expected %q; got %q", "id", string(k.NameBytes())) 97 | } 98 | }, 99 | tf: func(t *testing.T, config testutil.HAProxyConfig) { 100 | var wg sync.WaitGroup 101 | for i := 0; i < 100; i++ { 102 | wg.Add(1) 103 | go func() { 104 | defer wg.Done() 105 | for i := 0; i < 100; i++ { 106 | resp, err := http.Get("http://127.0.0.1:" + config.FrontendPort) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | 111 | if resp.StatusCode != http.StatusOK { 112 | t.Errorf("expected %d; got %d", http.StatusOK, resp.StatusCode) 113 | } 114 | } 115 | }() 116 | } 117 | wg.Wait() 118 | }, 119 | }, 120 | } 121 | 122 | t.Parallel() 123 | for _, test := range tests { 124 | t.Run(test.name, func(t *testing.T) { 125 | a := Agent{Handler: test.hf} 126 | 127 | // create the listener synchronously to prevent a race 128 | l := testutil.TCPListener(t) 129 | // ignore errors as the listener will be closed by t.Cleanup 130 | go a.Serve(l) 131 | 132 | cfg := testutil.HAProxyConfig{ 133 | EngineAddr: l.Addr().String(), 134 | FrontendPort: fmt.Sprintf("%d", testutil.TCPPort(t)), 135 | CustomFrontendConfig: test.frontendCfg, 136 | CustomBackendConfig: test.backendCfg, 137 | } 138 | 139 | cfg.Run(t) 140 | 141 | test.tf(t, cfg) 142 | }) 143 | } 144 | } 145 | 146 | type E2ETest struct { 147 | name string 148 | hf HandlerFunc 149 | tf func(*testing.T, testutil.HAProxyConfig) 150 | frontendCfg string 151 | backendCfg string 152 | } 153 | -------------------------------------------------------------------------------- /spop/error.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type errorCode int 8 | 9 | func (e errorCode) String() string { 10 | switch e { 11 | case ErrorNone: 12 | return "normal" 13 | case ErrorIO: 14 | return "I/O error" 15 | case ErrorTimeout: 16 | return "a timeout occurred" 17 | case ErrorTooBig: 18 | return "frame is too big" 19 | case ErrorInvalid: 20 | return "invalid frame received" 21 | case ErrorNoVSN: 22 | return "version value not found" 23 | case ErrorNoFrameSize: 24 | return "max-frame-size value not found" 25 | case ErrorNoCap: 26 | return "capabilities value not found" 27 | case ErrorBadVsn: 28 | return "unsupported version" 29 | case ErrorBadFrameSize: 30 | return "max-frame-size too big or too small" 31 | case ErrorFragNotSupported: 32 | return "fragmentation not supported" 33 | case ErrorInterlacedFrames: 34 | return "invalid interlaced frames" 35 | case ErrorFrameIDNotfound: 36 | return "frame-id not found" 37 | case ErrorRes: 38 | return "resource allocation error" 39 | case ErrorUnknown: 40 | return "an unknown error occurred" 41 | default: 42 | return fmt.Sprintf("unknown spoe error code: %d", e) 43 | } 44 | } 45 | 46 | const ( 47 | ErrorNone errorCode = iota 48 | ErrorIO 49 | ErrorTimeout 50 | ErrorTooBig 51 | ErrorInvalid 52 | ErrorNoVSN 53 | ErrorNoFrameSize 54 | ErrorNoCap 55 | ErrorBadVsn 56 | ErrorBadFrameSize 57 | ErrorFragNotSupported 58 | ErrorInterlacedFrames 59 | ErrorFrameIDNotfound 60 | ErrorRes 61 | ErrorUnknown errorCode = 99 62 | ) 63 | -------------------------------------------------------------------------------- /spop/example/header-to-body/engine.cfg: -------------------------------------------------------------------------------- 1 | [engine] 2 | spoe-agent engine-agent 3 | messages engine-req engine-res 4 | option var-prefix engine 5 | option set-on-error error 6 | timeout hello 100ms 7 | timeout idle 10s 8 | timeout processing 500ms 9 | use-backend engine-spoa 10 | log global 11 | 12 | spoe-message engine-req 13 | args id=unique-id src-ip=src method=method path=path query=query version=req.ver headers=req.hdrs body=req.body 14 | event on-frontend-http-request 15 | 16 | spoe-message engine-res 17 | args id=unique-id version=res.ver status=status headers=res.hdrs body=res.body 18 | event on-http-response 19 | -------------------------------------------------------------------------------- /spop/example/header-to-body/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dropmorepackets/haproxy-go/spop/example/header-to-body 2 | 3 | go 1.21 4 | 5 | replace github.com/dropmorepackets/haproxy-go => ../../../ 6 | 7 | require github.com/dropmorepackets/haproxy-go v0.0.0-00010101000000-000000000000 8 | -------------------------------------------------------------------------------- /spop/example/header-to-body/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DropMorePackets/haproxy-go/2d691f6d7c01d9b476fa64112345d3d6b9447191/spop/example/header-to-body/go.sum -------------------------------------------------------------------------------- /spop/example/header-to-body/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 3 | 4 | defaults 5 | mode http 6 | log global 7 | timeout client 5s 8 | timeout server 5s 9 | timeout connect 5s 10 | option httplog 11 | 12 | listen stats 13 | bind 127.0.0.1:8000 14 | stats enable 15 | stats uri / 16 | stats refresh 15s 17 | 18 | frontend test 19 | bind *:8080 20 | log-format "%ci:%cp\ [%t]\ %ft\ %b/%s\ %Th/%Ti/%TR/%Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %CS\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ %hr\ %hs\ %{+Q}r\ %ID spoa-error:\ %[var(txn.engine.error)]" 21 | filter spoe engine engine config engine.cfg 22 | 23 | default_backend test_backend 24 | 25 | backend test_backend 26 | mode http 27 | http-request return status 200 content-type "text/plain" lf-string "%[var(txn.engine.body)]" 28 | 29 | backend engine-spoa 30 | mode tcp 31 | option spop-check 32 | server s1 127.0.0.1:9000 check 33 | -------------------------------------------------------------------------------- /spop/example/header-to-body/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | _ "net/http/pprof" 8 | 9 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 10 | "github.com/dropmorepackets/haproxy-go/spop" 11 | ) 12 | 13 | func main() { 14 | go http.ListenAndServe(":9001", nil) 15 | 16 | log.SetFlags(log.LstdFlags | log.Lshortfile) 17 | log.Fatal(spop.ListenAndServe(":9000", spop.HandlerFunc(HandleSPOE))) 18 | } 19 | 20 | func HandleSPOE(_ context.Context, w *encoding.ActionWriter, m *encoding.Message) { 21 | k := encoding.AcquireKVEntry() 22 | defer encoding.ReleaseKVEntry(k) 23 | 24 | for m.KV.Next(k) { 25 | if k.NameEquals("headers") { 26 | err := w.SetStringBytes(encoding.VarScopeTransaction, "body", k.ValueBytes()) 27 | if err != nil { 28 | log.Printf("err: %v", err) 29 | } 30 | } 31 | } 32 | 33 | if m.KV.Error() != nil { 34 | log.Println(m.KV.Error()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spop/frame.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "sync" 8 | 9 | "github.com/dropmorepackets/haproxy-go/pkg/buffer" 10 | 11 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 12 | ) 13 | 14 | const uint32Len = 4 15 | 16 | var framePool = sync.Pool{ 17 | New: func() any { 18 | return &frame{ 19 | length: make([]byte, uint32Len), 20 | buf: buffer.NewSliceBuffer(maxFrameSize), 21 | } 22 | }, 23 | } 24 | 25 | func acquireFrame() *frame { 26 | return framePool.Get().(*frame) 27 | } 28 | 29 | func releaseFrame(f *frame) { 30 | f.buf.Reset() 31 | f.frameType = 0 32 | f.meta = frameMetadata{} 33 | 34 | framePool.Put(f) 35 | } 36 | 37 | type frameMetadata struct { 38 | Flags frameFlag 39 | StreamID uint64 40 | FrameID uint64 41 | } 42 | 43 | type frame struct { 44 | buf *buffer.SliceBuffer 45 | 46 | length []byte 47 | meta frameMetadata 48 | 49 | frameType frameType 50 | } 51 | 52 | func (f *frame) ReadFrom(r io.Reader) (int64, error) { 53 | if _, err := r.Read(f.length); err != nil { 54 | return 0, fmt.Errorf("reading frame length: %w", err) 55 | } 56 | frameLen := binary.BigEndian.Uint32(f.length) 57 | 58 | f.buf.Reset() 59 | dataBuf := f.buf.WriteNBytes(int(frameLen)) 60 | 61 | // read full frame into buffer 62 | n, err := r.Read(dataBuf) 63 | if err != nil { 64 | return int64(n + len(f.length)), fmt.Errorf("reading frame payload: %w", err) 65 | } 66 | 67 | if n != int(frameLen) { 68 | return int64(n + len(f.length)), io.ErrUnexpectedEOF 69 | } 70 | 71 | return int64(n + len(f.length)), f.decodeHeader() 72 | } 73 | 74 | func (f *frame) WriteTo(w io.Writer) (int64, error) { 75 | binary.BigEndian.PutUint32(f.length, uint32(f.buf.Len())) 76 | 77 | if n, err := w.Write(f.length); err != nil { 78 | return int64(n), err 79 | } 80 | 81 | n, err := w.Write(f.buf.ReadBytes()) 82 | return int64(n + len(f.length)), err 83 | } 84 | 85 | func (f *frame) encodeHeader() error { 86 | f.buf.WriteNBytes(1)[0] = byte(f.frameType) 87 | 88 | binary.BigEndian.PutUint32(f.buf.WriteNBytes(uint32Len), uint32(f.meta.Flags)) 89 | 90 | n, err := encoding.PutVarint(f.buf.WriteBytes(), f.meta.StreamID) 91 | if err != nil { 92 | return err 93 | } 94 | f.buf.AdvanceW(n) 95 | 96 | n, err = encoding.PutVarint(f.buf.WriteBytes(), f.meta.FrameID) 97 | if err != nil { 98 | return err 99 | } 100 | f.buf.AdvanceW(n) 101 | 102 | return nil 103 | } 104 | 105 | func (f *frame) decodeHeader() error { 106 | // We don't need to validate here, 107 | // there is validation further down the chain 108 | f.frameType = frameType(f.buf.ReadNBytes(1)[0]) 109 | 110 | f.meta.Flags = frameFlag(binary.BigEndian.Uint32(f.buf.ReadNBytes(uint32Len))) 111 | 112 | streamID, n, err := encoding.Varint(f.buf.ReadBytes()) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | f.meta.StreamID = streamID 118 | f.buf.AdvanceR(n) 119 | 120 | frameID, n, err := encoding.Varint(f.buf.ReadBytes()) 121 | if err != nil { 122 | return err 123 | } 124 | f.meta.FrameID = frameID 125 | f.buf.AdvanceR(n) 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /spop/frames.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 9 | ) 10 | 11 | type frameFlag uint32 12 | 13 | const ( 14 | frameFlagFin frameFlag = 1 15 | frameFlagAbrt frameFlag = 2 16 | ) 17 | 18 | type frameType byte 19 | 20 | const ( 21 | // Frames sent by HAProxy 22 | frameTypeIDHaproxyHello frameType = 1 23 | frameTypeIDHaproxyDisconnect frameType = 2 24 | frameTypeIDNotify frameType = 3 25 | 26 | // Frames sent by the agents 27 | frameTypeIDAgentHello frameType = 101 28 | frameTypeIDAgentDisconnect frameType = 102 29 | frameTypeIDAck frameType = 103 30 | ) 31 | 32 | type frameWriter interface { 33 | io.WriterTo 34 | } 35 | 36 | var ( 37 | _ frameWriter = (*AgentDisconnectFrame)(nil) 38 | _ frameWriter = (*AgentHelloFrame)(nil) 39 | ) 40 | 41 | type AgentDisconnectFrame struct { 42 | ErrCode errorCode 43 | } 44 | 45 | func (a *AgentDisconnectFrame) WriteTo(w io.Writer) (int64, error) { 46 | f := acquireFrame() 47 | defer releaseFrame(f) 48 | 49 | f.frameType = frameTypeIDAgentDisconnect 50 | f.meta.FrameID = 0 51 | f.meta.StreamID = 0 52 | f.meta.Flags = frameFlagFin 53 | 54 | if err := f.encodeHeader(); err != nil { 55 | return 0, err 56 | } 57 | 58 | kvw := encoding.NewKVWriter(f.buf.WriteBytes(), 0) 59 | if err := kvw.SetUInt32("status-code", uint32(a.ErrCode)); err != nil { 60 | return 0, err 61 | } 62 | 63 | if err := kvw.SetString("message", a.ErrCode.String()); err != nil { 64 | return 0, err 65 | } 66 | 67 | f.buf.AdvanceW(kvw.Off()) 68 | 69 | return f.WriteTo(w) 70 | } 71 | 72 | const ( 73 | helloKeyMaxFrameSize = "max-frame-size" 74 | helloKeySupportedVersions = "supported-versions" 75 | helloKeyVersion = "version" 76 | helloKeyCapabilities = "capabilities" 77 | helloKeyHealthcheck = "healthcheck" 78 | helloKeyEngineID = "engine-id" 79 | 80 | //lint:ignore U1000 These will probably be implemented again 81 | capabilityNameAsync = "async" 82 | capabilityNamePipelining = "pipelining" 83 | ) 84 | 85 | type AgentHelloFrame struct { 86 | Version string 87 | Capabilities []string 88 | MaxFrameSize uint32 89 | } 90 | 91 | func (a *AgentHelloFrame) WriteTo(w io.Writer) (int64, error) { 92 | f := acquireFrame() 93 | defer releaseFrame(f) 94 | 95 | f.frameType = frameTypeIDAgentHello 96 | f.meta.FrameID = 0 97 | f.meta.StreamID = 0 98 | f.meta.Flags = frameFlagFin 99 | 100 | if err := f.encodeHeader(); err != nil { 101 | return 0, err 102 | } 103 | 104 | kvw := encoding.NewKVWriter(f.buf.WriteBytes(), 0) 105 | if err := kvw.SetString(helloKeyVersion, a.Version); err != nil { 106 | return 0, err 107 | } 108 | 109 | if err := kvw.SetUInt32(helloKeyMaxFrameSize, a.MaxFrameSize); err != nil { 110 | return 0, err 111 | } 112 | 113 | err := kvw.SetString(helloKeyCapabilities, strings.Join(a.Capabilities, ",")) 114 | if err != nil { 115 | return 0, err 116 | } 117 | f.buf.AdvanceW(kvw.Off()) 118 | 119 | return f.WriteTo(w) 120 | } 121 | 122 | type AckFrame struct { 123 | ActionWriterCallback func(*encoding.ActionWriter) error 124 | FrameID uint64 125 | StreamID uint64 126 | } 127 | 128 | func (a *AckFrame) WriteTo(w io.Writer) (int64, error) { 129 | f := acquireFrame() 130 | defer releaseFrame(f) 131 | 132 | f.frameType = frameTypeIDAck 133 | f.meta.FrameID = a.FrameID 134 | f.meta.StreamID = a.StreamID 135 | f.meta.Flags = frameFlagFin 136 | 137 | if err := f.encodeHeader(); err != nil { 138 | return 0, fmt.Errorf("encoding header: %w", err) 139 | } 140 | 141 | aw := encoding.AcquireActionWriter(f.buf.WriteBytes(), 0) 142 | defer encoding.ReleaseActionWriter(aw) 143 | 144 | // TODO: errors are not correctly handled and will result in an invalid state. 145 | if err := a.ActionWriterCallback(aw); err != nil { 146 | return 0, err 147 | } 148 | 149 | f.buf.AdvanceW(aw.Off()) 150 | 151 | return f.WriteTo(w) 152 | } 153 | -------------------------------------------------------------------------------- /spop/go.sum: -------------------------------------------------------------------------------- 1 | github.com/dropmorepackets/haproxy-go v0.0.4 h1:bZQ8KHu00s/AyVNXW2kp9TKwpIWso2xTA3UfZmyxMGM= 2 | github.com/dropmorepackets/haproxy-go v0.0.4/go.mod h1:OGwftKhVqRvI1QtonOPCvPHKgDQLLaZpT2aF25ReQ2Q= 3 | -------------------------------------------------------------------------------- /spop/handler.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 7 | ) 8 | 9 | type Handler interface { 10 | HandleSPOE(context.Context, *encoding.ActionWriter, *encoding.Message) 11 | } 12 | 13 | type HandlerFunc func(context.Context, *encoding.ActionWriter, *encoding.Message) 14 | 15 | func (h HandlerFunc) HandleSPOE(ctx context.Context, w *encoding.ActionWriter, m *encoding.Message) { 16 | h(ctx, w, m) 17 | } 18 | -------------------------------------------------------------------------------- /spop/protocol.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "syscall" 9 | 10 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 11 | ) 12 | 13 | func newProtocolClient(ctx context.Context, rw io.ReadWriter, as *asyncScheduler, handler Handler) *protocolClient { 14 | var c protocolClient 15 | c.rw = rw 16 | c.handler = handler 17 | c.ctx, c.ctxCancel = context.WithCancelCause(ctx) 18 | c.as = as 19 | return &c 20 | } 21 | 22 | type protocolClient struct { 23 | rw io.ReadWriter 24 | handler Handler 25 | ctx context.Context 26 | 27 | ctxCancel context.CancelCauseFunc 28 | as *asyncScheduler 29 | 30 | engineID string 31 | maxFrameSize uint32 32 | 33 | gotHello bool 34 | } 35 | 36 | func (c *protocolClient) Close() error { 37 | if c.ctx.Err() != nil { 38 | return c.ctx.Err() 39 | } 40 | 41 | // We ignore any error since the disconnect frame is delivered on 42 | // best effort anyway. 43 | _, _ = (&AgentDisconnectFrame{ 44 | ErrCode: ErrorUnknown, 45 | }).WriteTo(c.rw) 46 | 47 | c.ctxCancel(fmt.Errorf("closing client")) 48 | 49 | return nil 50 | } 51 | 52 | func (c *protocolClient) frameHandler(f *frame) error { 53 | defer releaseFrame(f) 54 | 55 | switch f.frameType { 56 | case frameTypeIDHaproxyHello: 57 | return c.onHAProxyHello(f) 58 | case frameTypeIDNotify: 59 | return c.onNotify(f) 60 | case frameTypeIDHaproxyDisconnect: 61 | return c.onHAProxyDisconnect(f) 62 | default: 63 | return fmt.Errorf("unknown frame type: %d", f.frameType) 64 | } 65 | } 66 | 67 | func (c *protocolClient) Serve() error { 68 | for { 69 | f := acquireFrame() 70 | if _, err := f.ReadFrom(c.rw); err != nil { 71 | if c.ctx.Err() != nil { 72 | return context.Cause(c.ctx) 73 | } 74 | 75 | if errors.Is(err, io.EOF) || errors.Is(err, syscall.ECONNRESET) { 76 | return nil 77 | } 78 | 79 | return err 80 | } 81 | 82 | c.as.schedule(f, c) 83 | } 84 | } 85 | 86 | const ( 87 | version = "2.0" 88 | 89 | // maxFrameSize represents the maximum frame size allowed by this library 90 | // it also represents the maximum slice size that is allowed on stack 91 | maxFrameSize = 64<<10 - 1 92 | ) 93 | 94 | func (c *protocolClient) onHAProxyHello(f *frame) error { 95 | if c.gotHello { 96 | panic("duplicate hello frame") 97 | } 98 | c.gotHello = true 99 | 100 | s := encoding.AcquireKVScanner(f.buf.ReadBytes(), -1) 101 | defer encoding.ReleaseKVScanner(s) 102 | 103 | k := encoding.AcquireKVEntry() 104 | defer encoding.ReleaseKVEntry(k) 105 | for s.Next(k) { 106 | switch { 107 | case k.NameEquals(helloKeyMaxFrameSize): 108 | c.maxFrameSize = uint32(k.ValueInt()) 109 | if c.maxFrameSize > maxFrameSize { 110 | return fmt.Errorf("maxFrameSize bigger than maximum allowed size: %d < %d", maxFrameSize, c.maxFrameSize) 111 | } 112 | 113 | case k.NameEquals(helloKeyEngineID): 114 | //TODO: This does copy the engine id but yolo? 115 | c.engineID = string(k.ValueBytes()) 116 | //case k.NameEquals(helloKeySupportedVersions): 117 | //case k.NameEquals(helloKeyCapabilities): 118 | case k.NameEquals(helloKeyHealthcheck): 119 | // as described in the protocol, close connection after hello 120 | // AGENT-HELLO + close() 121 | defer c.ctxCancel(nil) 122 | } 123 | } 124 | 125 | if err := s.Error(); err != nil { 126 | return err 127 | } 128 | 129 | _, err := (&AgentHelloFrame{ 130 | Version: version, 131 | MaxFrameSize: c.maxFrameSize, 132 | Capabilities: []string{}, 133 | }).WriteTo(c.rw) 134 | return err 135 | } 136 | 137 | func (c *protocolClient) onNotify(f *frame) error { 138 | s := encoding.AcquireMessageScanner(f.buf.ReadBytes()) 139 | defer encoding.ReleaseMessageScanner(s) 140 | 141 | m := encoding.AcquireMessage() 142 | defer encoding.ReleaseMessage(m) 143 | 144 | fn := func(w *encoding.ActionWriter) error { 145 | for s.Next(m) { 146 | err := wrapPanic(func() error { 147 | c.handler.HandleSPOE(c.ctx, w, m) 148 | return nil 149 | }) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | if err := m.KV.Discard(); err != nil { 155 | return err 156 | } 157 | } 158 | 159 | return s.Error() 160 | } 161 | 162 | _, err := (&AckFrame{ 163 | FrameID: f.meta.FrameID, 164 | StreamID: f.meta.StreamID, 165 | ActionWriterCallback: fn, 166 | }).WriteTo(c.rw) 167 | return err 168 | } 169 | 170 | func (c *protocolClient) onHAProxyDisconnect(f *frame) error { 171 | if f.buf.Len() == 0 { 172 | return fmt.Errorf("disconnect frame without content") 173 | } 174 | 175 | s := encoding.AcquireKVScanner(f.buf.ReadBytes(), -1) 176 | defer encoding.ReleaseKVScanner(s) 177 | 178 | k := encoding.AcquireKVEntry() 179 | defer encoding.ReleaseKVEntry(k) 180 | 181 | var ( 182 | code errorCode 183 | ) 184 | 185 | for s.Next(k) { 186 | switch name := string(k.NameBytes()); name { 187 | case "status-code": 188 | code = errorCode(k.ValueInt()) 189 | case "message": 190 | // We don't really care about the message since they should all be 191 | // defined in the errorCode type. 192 | default: 193 | panic("unexpected kv entry: " + name) 194 | } 195 | } 196 | 197 | var err error 198 | switch code { 199 | // HAProxy returns an IO error when it doesn't require a connection 200 | // anymore. 201 | case ErrorIO, ErrorTimeout, ErrorNone: 202 | default: 203 | err = fmt.Errorf("disconnect frame with code %d: %s", code, code) 204 | } 205 | 206 | c.ctxCancel(err) 207 | return err 208 | } 209 | -------------------------------------------------------------------------------- /spop/scheduler.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | type queue struct { 10 | notEmptyCond *sync.Cond 11 | notFullCond *sync.Cond 12 | elems []queueElem 13 | tail int 14 | head int 15 | size int 16 | lock sync.RWMutex 17 | } 18 | 19 | type queueElem struct { 20 | f *frame 21 | pc *protocolClient 22 | } 23 | 24 | func newQueue(cap int) *queue { 25 | q := &queue{ 26 | elems: make([]queueElem, cap), 27 | } 28 | 29 | q.notEmptyCond = sync.NewCond(&q.lock) 30 | q.notFullCond = sync.NewCond(&q.lock) 31 | 32 | return q 33 | } 34 | 35 | func (bq *queue) isFull() bool { 36 | return bq.size == len(bq.elems) 37 | } 38 | 39 | func (bq *queue) isEmpty() bool { 40 | return bq.size <= 0 41 | } 42 | 43 | func (bq *queue) Put(f *frame, pc *protocolClient) { 44 | bq.lock.Lock() 45 | defer bq.lock.Unlock() 46 | 47 | for bq.isFull() { 48 | bq.notFullCond.Wait() 49 | } 50 | 51 | bq.elems[bq.tail] = queueElem{f, pc} 52 | bq.tail = (bq.tail + 1) % len(bq.elems) 53 | bq.size++ 54 | 55 | bq.notEmptyCond.Signal() 56 | } 57 | 58 | func (bq *queue) Get() queueElem { 59 | bq.lock.Lock() 60 | defer bq.lock.Unlock() 61 | 62 | defer bq.notFullCond.Signal() 63 | 64 | for bq.isEmpty() { 65 | bq.notEmptyCond.Wait() 66 | } 67 | 68 | item := bq.elems[bq.head] 69 | bq.head = (bq.head + 1) % len(bq.elems) 70 | bq.size-- 71 | 72 | return item 73 | } 74 | 75 | type asyncScheduler struct { 76 | q *queue 77 | } 78 | 79 | func newAsyncScheduler() *asyncScheduler { 80 | a := asyncScheduler{ 81 | q: newQueue(runtime.NumCPU() * 2), 82 | } 83 | 84 | for i := 0; i < runtime.NumCPU(); i++ { 85 | go a.queueWorker() 86 | } 87 | 88 | return &a 89 | } 90 | 91 | func (a *asyncScheduler) queueWorker() { 92 | for { 93 | qe := a.q.Get() 94 | // Use wrap panic to prevent loosing worker goroutines to panics 95 | err := wrapPanic(func() error { 96 | return qe.pc.frameHandler(qe.f) 97 | }) 98 | if err != nil { 99 | log.Println(err) 100 | continue 101 | } 102 | } 103 | } 104 | 105 | func (a *asyncScheduler) schedule(f *frame, pc *protocolClient) { 106 | a.q.Put(f, pc) 107 | } 108 | -------------------------------------------------------------------------------- /spop/server_test.go: -------------------------------------------------------------------------------- 1 | package spop 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "testing" 10 | "time" 11 | 12 | "github.com/dropmorepackets/haproxy-go/pkg/encoding" 13 | "github.com/dropmorepackets/haproxy-go/pkg/testutil" 14 | ) 15 | 16 | func TestFakeCon(t *testing.T) { 17 | log.SetFlags(log.LstdFlags | log.Lshortfile) 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | 20 | pipe, pipeConn := testutil.PipeConn() 21 | go func() { 22 | defer cancel() 23 | 24 | if err := newHelloFrame(pipe); err != nil { 25 | t.Error(err) 26 | return 27 | } 28 | 29 | if err := newNotifyFrame(pipe); err != nil { 30 | t.Error(err) 31 | return 32 | } 33 | }() 34 | 35 | go func() { 36 | defer cancel() 37 | 38 | select { 39 | case <-ctx.Done(): 40 | case <-time.After(5 * time.Second): 41 | t.Error("timeout") 42 | } 43 | }() 44 | 45 | handler := HandlerFunc(func(_ context.Context, _ *encoding.ActionWriter, m *encoding.Message) { 46 | log.Println(m.NameBytes()) 47 | cancel() 48 | }) 49 | 50 | pc := newProtocolClient(context.Background(), pipeConn, newAsyncScheduler(), handler) 51 | defer pc.Close() 52 | defer pipe.Close() 53 | go pc.Serve() 54 | 55 | <-ctx.Done() 56 | } 57 | 58 | func newNotifyFrame(wr io.Writer) error { 59 | f := acquireFrame() 60 | defer releaseFrame(f) 61 | 62 | f.frameType = frameTypeIDNotify 63 | f.meta.StreamID = uint64(rand.Int63()) 64 | f.meta.FrameID = uint64(rand.Int63()) 65 | f.meta.Flags = frameFlagFin 66 | 67 | if err := f.encodeHeader(); err != nil { 68 | return err 69 | } 70 | 71 | n, err := encoding.PutBytes(f.buf.WriteBytes(), []byte("example")) 72 | if err != nil { 73 | return err 74 | } 75 | f.buf.AdvanceW(n) 76 | f.buf.WriteNBytes(1)[0] = 0 77 | 78 | //TODO Write message 79 | //w := encoding.AcquireActionWriter(f.buf.WriteBytes(), 0) 80 | //defer encoding.ReleaseActionWriter(w) 81 | 82 | //f.buf.AdvanceW(w.Off()) 83 | 84 | binary.BigEndian.PutUint32(f.length, uint32(f.buf.Len())) 85 | wr.Write(f.length) 86 | wr.Write(f.buf.ReadBytes()) 87 | 88 | return nil 89 | } 90 | 91 | func newHelloFrame(wr io.Writer) error { 92 | f := acquireFrame() 93 | defer releaseFrame(f) 94 | 95 | f.frameType = frameTypeIDHaproxyHello 96 | f.meta.StreamID = 0 97 | f.meta.FrameID = 0 98 | f.meta.Flags = frameFlagFin 99 | 100 | if err := f.encodeHeader(); err != nil { 101 | return err 102 | } 103 | 104 | w := encoding.AcquireKVWriter(f.buf.WriteBytes(), 0) 105 | defer encoding.ReleaseKVWriter(w) 106 | 107 | if err := w.SetUInt32(helloKeyMaxFrameSize, maxFrameSize); err != nil { 108 | return err 109 | } 110 | 111 | // TODO 112 | if err := w.SetString(helloKeyEngineID, "random engine"); err != nil { 113 | return err 114 | } 115 | 116 | f.buf.AdvanceW(w.Off()) 117 | 118 | binary.BigEndian.PutUint32(f.length, uint32(f.buf.Len())) 119 | wr.Write(f.length) 120 | wr.Write(f.buf.ReadBytes()) 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] 2 | --------------------------------------------------------------------------------