mirror of
https://github.com/buger/goreplay.git
synced 2026-10-19 16:15:42 +00:00
Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bdd61553f | ||
|
|
194671552a | ||
|
|
8f29ec807f | ||
|
|
fd62d693ee | ||
|
|
3062446395 | ||
|
|
e546fdb631 | ||
|
|
3be10be730 | ||
|
|
ba4f5753be | ||
|
|
ad503d45cb | ||
|
|
dbd10c320a | ||
|
|
e463ba3a53 | ||
|
|
40946831b9 | ||
|
|
99e6fdfd60 | ||
|
|
23825d82f5 | ||
|
|
f53d6c4466 | ||
|
|
243b2cf484 | ||
|
|
c2e7a0f777 | ||
|
|
f3bb50192c | ||
|
|
2497e48e8d | ||
|
|
a289b03483 | ||
|
|
951cd72a92 | ||
|
|
655eacc546 | ||
|
|
752b457501 | ||
|
|
161e6bf08e | ||
|
|
ef925b70b4 | ||
|
|
d785acbc4e | ||
|
|
a14cc2f077 | ||
|
|
d440b3dc8f | ||
|
|
169de158ed | ||
|
|
7f1760619e | ||
|
|
a62930856a | ||
|
|
f780d4a21d | ||
|
|
18f33fb4f7 | ||
|
|
b09d90aef9 | ||
|
|
a4962b5d2d | ||
|
|
a5f5615156 | ||
|
|
b520511c96 | ||
|
|
3f659a16ce | ||
|
|
9c5f011bb7 | ||
|
|
6c89827ba5 | ||
|
|
4aacf73ef2 | ||
|
|
86546f33ef | ||
|
|
1bd550ac03 | ||
|
|
d5c78155c9 | ||
|
|
df73b91a65 | ||
|
|
4a1c08859b | ||
|
|
bb1bca6f3c | ||
|
|
553e9018dd | ||
|
|
02a0904651 | ||
|
|
fe25ddd0fb | ||
|
|
56ed4760f0 | ||
|
|
ca90d796fb | ||
|
|
f4895e00d5 | ||
|
|
4d82e2f652 | ||
|
|
b50ea90cdb | ||
|
|
42d89903b6 | ||
|
|
9bdf27226d | ||
|
|
11d61dcb4d | ||
|
|
3211d251b9 | ||
|
|
6f48d23953 | ||
|
|
bf9d233ab0 | ||
|
|
c9274ac92a | ||
|
|
9e2754f673 | ||
|
|
024e1261a5 | ||
|
|
f11d59ba19 | ||
|
|
f115627991 | ||
|
|
524b9dfa61 | ||
|
|
0f2f93a60f | ||
|
|
214edb45f6 | ||
|
|
79ff8823a3 | ||
|
|
debc3b4384 | ||
|
|
f77fd8af57 | ||
|
|
c01be50d72 | ||
|
|
df2b4be898 | ||
|
|
0902a10c2d | ||
|
|
710cbe10e3 | ||
|
|
b79483b37f | ||
|
|
ec1dcccc31 | ||
|
|
da04e88a56 | ||
|
|
19ad90aa4b | ||
|
|
8e76559b49 | ||
|
|
61b377d123 | ||
|
|
625ed54f1e | ||
|
|
889c1e6d41 | ||
|
|
8edb74e572 | ||
|
|
59b12abbe1 | ||
|
|
5d718e92ea | ||
|
|
148846be93 | ||
|
|
0bb72e0f60 | ||
|
|
af19f0feb8 | ||
|
|
724c0890c9 | ||
|
|
a727ade006 | ||
|
|
92c63de753 | ||
|
|
b6cc6a3606 | ||
|
|
2b993eda49 | ||
|
|
de8a74fdca | ||
|
|
f701dace50 | ||
|
|
ad43405793 | ||
|
|
9468978353 | ||
|
|
db32671c52 | ||
|
|
4b864199cd | ||
|
|
67154f6a62 | ||
|
|
05d09ce049 | ||
|
|
e613a3156e | ||
|
|
bbefdf5710 | ||
|
|
35865bfd8c | ||
|
|
5b0dc99caa | ||
|
|
972c59abfe | ||
|
|
642bb35124 | ||
|
|
58ff8865df | ||
|
|
763ad9b051 | ||
|
|
b1972fb0a8 | ||
|
|
361ea8967e | ||
|
|
f00817c13e | ||
|
|
ebe9e222f3 | ||
|
|
f2857424b0 | ||
|
|
76fe71c0fd | ||
|
|
d315290564 | ||
|
|
7e173b08bb | ||
|
|
061a02e370 | ||
|
|
9a2187ff96 | ||
|
|
07a4ef626c | ||
|
|
71a338cb02 | ||
|
|
14143e46ca | ||
|
|
7c10776064 | ||
|
|
09d7d42254 | ||
|
|
22f0b09883 | ||
|
|
1e8c879bc6 | ||
|
|
e74e945e7f |
@@ -8,3 +8,14 @@ updates:
|
||||
pull-request-branch-name:
|
||||
# Separate sections of the branch name with a hyphen
|
||||
separator: "-"
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: "Checkout repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: "Set up Docker Buildx"
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: "Cache Docker layers"
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3.0.5
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: "Build and push docker image"
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -4,7 +4,7 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.14.x, 1.15.x] # two latest minor versions
|
||||
go-version: [1.18.x, 1.19.x] # two latest minor versions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: update package index
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/cache@v3.0.5
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod # Module download cache
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# This workflow was added by CodeSee. Learn more at https://codesee.io/
|
||||
# This is v2.0 of this workflow file
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: CodeSee
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
codesee:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
name: Analyze the repo with CodeSee
|
||||
steps:
|
||||
- uses: Codesee-io/codesee-action@v2
|
||||
with:
|
||||
codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
|
||||
codesee-url: https://app.codesee.io
|
||||
@@ -0,0 +1,23 @@
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/semgrep.yml
|
||||
schedule:
|
||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
||||
- cron: 53 23 * * *
|
||||
name: Semgrep
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Scan
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
||||
container:
|
||||
image: returntocorp/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: semgrep ci
|
||||
+6
-1
@@ -1,10 +1,14 @@
|
||||
vendor
|
||||
*.swp
|
||||
*.gor
|
||||
*.rpm
|
||||
*.dep
|
||||
*.deb
|
||||
*.pkg
|
||||
|
||||
*.exe
|
||||
*.pprof
|
||||
*.out
|
||||
hey
|
||||
|
||||
*.bin
|
||||
|
||||
@@ -28,3 +32,4 @@ goreplay
|
||||
corpus
|
||||
crashers
|
||||
suppressions
|
||||
dist
|
||||
+18
-20
@@ -2,7 +2,7 @@ END-USER LICENSE AGREEMENT
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
IMPORTANT: THIS SOFTWARE END-USER LICENSE AGREEMENT ("EULA") IS A LEGAL AGREEMENT (“Agreement”) BETWEEN YOU (THE CUSTOMER, EITHER AS AN INDIVIDUAL OR, IF PURCHASED OR OTHERWISE ACQUIRED BY OR FOR AN ENTITY, AS AN ENTITY) AND GoReplay LLC. READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOREPLAY PRO AND RELATED SOFTWARE COMPONENTS (“SOFTWARE”). IT PROVIDES A LICENSE TO USE THE SOFTWARE AND CONTAINS WARRANTY INFORMATION AND LIABILITY DISCLAIMERS. BY INSTALLING AND USING THE SOFTWARE, YOU ARE CONFIRMING YOUR ACCEPTANCE OF THE SOFTWARE AND AGREEING TO BECOME BOUND BY THE TERMS OF THIS AGREEMENT.
|
||||
IMPORTANT: THIS SOFTWARE END-USER LICENSE AGREEMENT ("EULA") IS A LEGAL AGREEMENT (“Agreement”) BETWEEN YOU (THE CUSTOMER, EITHER AS AN INDIVIDUAL OR, IF PURCHASED OR OTHERWISE ACQUIRED BY OR FOR AN ENTITY, AS AN ENTITY), hereafter "Customer", AND Replay Software LLC ("Licensor"). READ IT CAREFULLY BEFORE COMPLETING THE INSTALLATION PROCESS AND USING GOREPLAY PRO AND RELATED SOFTWARE COMPONENTS (“SOFTWARE”). IT PROVIDES A LICENSE TO USE THE SOFTWARE AND CONTAINS WARRANTY INFORMATION AND LIABILITY DISCLAIMERS. BY INSTALLING AND USING THE SOFTWARE, YOU ARE CONFIRMING YOUR ACCEPTANCE OF THE SOFTWARE AND AGREEING TO BECOME BOUND BY THE TERMS OF THIS AGREEMENT.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
@@ -14,17 +14,17 @@ In order to use the Software under this Agreement, you must receive a “Source
|
||||
|
||||
1.2 Unlimited Organization License. If you purchased an Organization License (included with the GoReplay Pro Software), you may install the Software on an unlimited number of Hosts. “Host” means any physical or virtual machine which is controlled by you. You may concurrently run the software on an unlimited number of Hosts.
|
||||
|
||||
1.3 Appliance License. If you purchased an Appliance License, you may distribute the Software in any applications, frameworks, or elements (collectively referred to as an “Application” or “Applications”) that you develop using the Software in accordance with this EULA, provided that such distribution does not violate the restrictions set forth in section 3 of this EULA. You must not remove, obscure or interfere with any copyright, acknowledgment, attribution, trademark, warning or disclaimer statement affixed to, incorporated in or otherwise applied in connection with the Software. You are required to ensure that the Software is not reused by or with any applications other than those with which you distribute it as permitted herein. For example, if You install the Software on a customer’s server, that customer is not permitted to use the Software independently of your Application. You must inform GoReplay LLC of your knowledge of any infringing use of the Software by any of your customers. You are liable for compliance by those third parties with the terms and conditions of this EULA. You will not owe GoReplay LLC any royalties for your distribution of the Software in accordance with this EULA.
|
||||
1.3 Appliance License. If you purchased an Appliance License, you may distribute the Software in any applications, frameworks, or elements (collectively referred to as an “Application” or “Applications”) that you develop using the Software in accordance with this EULA, provided that such distribution does not violate the restrictions set forth in section 3 of this EULA. You must not remove, obscure or interfere with any copyright, acknowledgment, attribution, trademark, warning or disclaimer statement affixed to, incorporated in or otherwise applied in connection with the Software. You are required to ensure that the Software is not reused by or with any applications other than those with which you distribute it as permitted herein. For example, if You install the Software on a customer’s server, that customer is not permitted to use the Software independently of your Application. You must inform Licensor of your knowledge of any infringing use of the Software by any of your customers. You are liable for compliance by those third parties with the terms and conditions of this EULA. You will not owe Licensor any royalties for your distribution of the Software in accordance with this EULA.
|
||||
|
||||
1.4 Archive Copies. You are entitled to make a reasonable amount of copies of the Software for archival purposes. Each copy must reproduce all copyright and other proprietary rights notices on or in the Software Product.
|
||||
|
||||
1.5 Electronic Delivery. All Software and license documentation shall be delivered by electronic means unless otherwise specified on the applicable invoice or at the time of purchase. Software shall be deemed delivered when it is made available for download by you (“Delivery”).
|
||||
|
||||
2. Modifications. GoReplay LLC shall provide you with source code so that you can create Modifications of the original software. “Modification” means: (a) any addition to or deletion from the contents of a file included in the original Software or previous Modifications created by You, or (b) any new file that contains any part of the original Software or previous Modifications. While you retain all rights to any original work authored by you as part of the Modifications, We continue to own all copyright and other intellectual property rights in the Software.
|
||||
2. Modifications. Licensor shall provide you with source code so that you can create Modifications of the original software. “Modification” means: (a) any addition to or deletion from the contents of a file included in the original Software or previous Modifications created by You, or (b) any new file that contains any part of the original Software or previous Modifications. While you retain all rights to any original work authored by you as part of the Modifications, We continue to own all copyright and other intellectual property rights in the Software.
|
||||
|
||||
3. Restricted Uses.
|
||||
|
||||
3.1 You shall not (and shall not allow any third party to): (a) decompile, disassemble, or otherwise reverse engineer the Software or attempt to reconstruct or discover any source code, underlying ideas, algorithms, file formats or programming interfaces of the Software by any means whatsoever (except and only to the extent that applicable law prohibits or restricts reverse engineering restrictions); (b) distribute, sell, sublicense, rent, lease or use the Software for time sharing, hosting, service provider or like purposes, except as expressly permitted under this Agreement; (c) redistribute the Software or Modifications other than by including the Software or a portion thereof within your own product, which must have substantially different functionality than the Software or Modifications and must not allow any third party to use the Software or Modifications, or any portions thereof, for software development or application development purposes; (d) redistribute the Software as part of a product, "appliance" or "virtual server"; (e) redistribute the Software on any server which is not directly under your control; (f) remove any product identification, proprietary, copyright or other notices contained in the Software; (g) modify any part of the Software, create a derivative work of any part of the Software (except as permitted in Section 4), or incorporate the Software, except to the extent expressly authorized in writing by GoReplay LLC; (h) publicly disseminate performance information or analysis (including, without limitation, benchmarks) from any source relating to the Software; (i) utilize any equipment, device, software, or other means designed to circumvent or remove any form of Source URL or copy protection used by GoReplay LLC in connection with the Software, or use the Software together with any authorization code, Source URL, serial number, or other copy protection device not supplied by GoReplay LLC; (j) use the Software to develop a product which is competitive with any GoReplay LLC product offerings; or (k) use unauthorized Source URLS or keycode(s) or distribute or publish Source URLs or keycode(s), except as may be expressly permitted by GoReplay LLC in writing. If your unique Source URL is ever published, GoReplay LLC reserves the right to terminate your access without notice.
|
||||
3.1 You shall not (and shall not allow any third party to): (a) decompile, disassemble, or otherwise reverse engineer the Software or attempt to reconstruct or discover any source code, underlying ideas, algorithms, file formats or programming interfaces of the Software by any means whatsoever (except and only to the extent that applicable law prohibits or restricts reverse engineering restrictions); (b) distribute, sell, sublicense, rent, lease or use the Software for time sharing, hosting, service provider or like purposes, except as expressly permitted under this Agreement; (c) redistribute the Software or Modifications other than by including the Software or a portion thereof within your own product, which must have substantially different functionality than the Software or Modifications and must not allow any third party to use the Software or Modifications, or any portions thereof, for software development or application development purposes; (d) redistribute the Software as part of a product, "appliance" or "virtual server"; (e) redistribute the Software on any server which is not directly under your control; (f) remove any product identification, proprietary, copyright or other notices contained in the Software; (g) modify any part of the Software, create a derivative work of any part of the Software (except as permitted in Section 4), or incorporate the Software, except to the extent expressly authorized in writing by Licensor; (h) publicly disseminate performance information or analysis (including, without limitation, benchmarks) from any source relating to the Software; (i) utilize any equipment, device, software, or other means designed to circumvent or remove any form of Source URL or copy protection used by Licensor in connection with the Software, or use the Software together with any authorization code, Source URL, serial number, or other copy protection device not supplied by Licensor; (j) use the Software to develop a product which is competitive with any Licensor product offerings; or (k) use unauthorized Source URLS or keycode(s) or distribute or publish Source URLs or keycode(s), except as may be expressly permitted by Licensor in writing. If your unique Source URL is ever published, Licensor reserves the right to terminate your access without notice.
|
||||
|
||||
3.2 UNDER NO CIRCUMSTANCES MAY YOU USE THE SOFTWARE AS PART OF A PRODUCT OR SERVICE THAT PROVIDES SIMILAR FUNCTIONALITY TO THE SOFTWARE ITSELF.
|
||||
|
||||
@@ -32,35 +32,33 @@ The Open Source version of the Software (“LGPL Version”) is licensed
|
||||
under the terms of the GNU Lesser General Public License versions 3.0
|
||||
(“LGPL”) and not under this EULA.
|
||||
|
||||
4. Ownership. Notwithstanding anything to the contrary contained herein, except for the limited license rights expressly provided herein, GoReplay LLC and its suppliers have and will retain all rights, title and interest (including, without limitation, all patent, copyright, trademark, trade secret and other intellectual property rights) in and to the Software and all copies, modifications and derivative works thereof (including any changes which incorporate any of your ideas, feedback or suggestions). You acknowledge that you are obtaining only a limited license right to the Software, and that irrespective of any use of the words “purchase”, “sale” or like terms hereunder no ownership rights are being conveyed to you under this Agreement or otherwise.
|
||||
4. Ownership. Notwithstanding anything to the contrary contained herein, except for the limited license rights expressly provided herein, Licensor and its suppliers have and will retain all rights, title and interest (including, without limitation, all patent, copyright, trademark, trade secret and other intellectual property rights) in and to the Software and all copies, modifications and derivative works thereof (including any changes which incorporate any of your ideas, feedback or suggestions). You acknowledge that you are obtaining only a limited license right to the Software, and that irrespective of any use of the words “purchase”, “sale” or like terms hereunder no ownership rights are being conveyed to you under this Agreement or otherwise.
|
||||
|
||||
5. Fees and Payment. The Software license fees will be due and payable in full as set forth in the applicable invoice or at the time of purchase. If the Software does not function properly within two weeks of purchase, please contact us within those two weeks for a refund. You shall be responsible for all taxes, withholdings, duties and levies arising from the order (excluding taxes based on the net income of GoReplay LLC).
|
||||
5. Fees and Payment. The Software license fees will be due and payable in full as set forth in the applicable invoice or at the time of purchase. If the Software does not function properly within two weeks of purchase, please contact us within those two weeks for a refund. You shall be responsible for all taxes, withholdings, duties and levies arising from the order (excluding taxes based on the net income of Licensor).
|
||||
|
||||
6. Support, Maintenance and Services. Subject to the terms and conditions of this Agreement, as set forth in your invoice, and as set forth on the GoReplay Pro support page (https://github.com/buger/goreplay/wiki/Pro-Support), support and maintenance services may be included with the purchase of your license subscription.
|
||||
|
||||
7. Term of Agreement.
|
||||
|
||||
7.1 Term. This Agreement is effective as of the Delivery of the Software and expires at such time as all license and service subscriptions hereunder have expired in accordance with their own terms (the “Term”). For clarification, the term of your license under this Agreement may be perpetual, limited for Evaluation Version, or designated as a fixed-term license in the Invoice, and shall be specified at your time of purchase. Either party may terminate this Agreement (including all related Invoices) if the other party: (a) fails to cure any material breach of this Agreement within thirty (30) days after written notice of such breach, provided that GoReplay LLC may terminate this Agreement immediately upon any breach of Section 3 or if you exceed any other restrictions contained in Section 1, unless otherwise specified in this agreement; (b) ceases operation without a successor; or (c) seeks protection under any bankruptcy, receivership, trust deed, creditors arrangement, composition or comparable proceeding, or if any such proceeding is instituted against such party (and not dismissed within sixty (60) days)). Termination is not an exclusive remedy and the exercise by either party of any remedy under this Agreement will be without prejudice to any other remedies it may have under this Agreement, by law, or otherwise.
|
||||
7.1 Term. This Agreement is effective as of the Delivery of the Software and expires at such time as all license and service subscriptions hereunder have expired in accordance with their own terms (the “Term”). For clarification, the term of your license under this Agreement may be perpetual, limited for Evaluation Version, or designated as a fixed-term license in the Invoice, and shall be specified at your time of purchase. Either party may terminate this Agreement (including all related Invoices) if the other party: (a) fails to cure any material breach of this Agreement within thirty (30) days after written notice of such breach, provided that Licensor may terminate this Agreement immediately upon any breach of Section 3 or if you exceed any other restrictions contained in Section 1, unless otherwise specified in this agreement; (b) ceases operation without a successor; or (c) seeks protection under any bankruptcy, receivership, trust deed, creditors arrangement, composition or comparable proceeding, or if any such proceeding is instituted against such party (and not dismissed within sixty (60) days)). Termination is not an exclusive remedy and the exercise by either party of any remedy under this Agreement will be without prejudice to any other remedies it may have under this Agreement, by law, or otherwise.
|
||||
|
||||
7.2 Termination. Upon any termination of this Agreement, you shall cease any and all use of any Software and destroy all copies thereof.
|
||||
|
||||
7.3 Expiration of License. Upon the expiration of any term under this Agreement, (a) all Software updates and services pursuant to the license shall cease, (b) you may only continue to run existing installations of the Software, (c) you may not install the Software on any additional Hosts, and (d) any new installation of the Software shall require the purchase of a new license subscription from GoReplay LLC.
|
||||
7.3 Expiration of License. Upon the expiration of any term under this Agreement, (a) all Software updates and services pursuant to the license shall cease, (b) you may only continue to run existing installations of the Software, (c) you may not install the Software on any additional Hosts, and (d) any new installation of the Software shall require the purchase of a new license subscription from Licensor.
|
||||
|
||||
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. GoReplay LLC does not warrant that the Software will be free of bugs, errors, viruses or other defects, and GoReplay LLC shall have no liability of any kind for the use of or inability to use the Software, the Software content or any associated service, and you acknowledge that it is not technically practicable for GoReplay LLC to do so.
|
||||
To the maximum extent permitted by applicable law, GoReplay LLC disclaims all warranties, express, implied, arising by law or otherwise, regarding the Software, the Software content and their respective performance or suitability for your intended use, including without limitation any implied warranty of merchantability, fitness for a particular purpose.
|
||||
Notwithstanding the foregoing, GoReplay LLC represents and warrants that it either owns the entire right to, title to, interest in, or has the right to license, the Software, and your proper use of the same will not violate any intellectual property or proprietary rights of another.
|
||||
8. Disclaimer of Warranties. The Software is provided "as is," with all faults, defects and errors, and without warranty of any kind. Licensor does not warrant that the Software will be free of bugs, errors, viruses or other defects, and Licensor shall have no liability of any kind for the use of or inability to use the Software, the Software content or any associated service, and you acknowledge that it is not technically practicable for Licensor to do so.
|
||||
To the maximum extent permitted by applicable law, Licensor disclaims all warranties, express, implied, arising by law or otherwise, regarding the Software, the Software content and their respective performance or suitability for your intended use, including without limitation any implied warranty of merchantability, fitness for a particular purpose.
|
||||
Notwithstanding the foregoing, Licensor represents and warrants that it either owns the entire right to, title to, interest in, or has the right to license, the Software, and your proper use of the same will not violate any intellectual property or proprietary rights of another.
|
||||
|
||||
9. Limitation of Liability.
|
||||
|
||||
In no event will either party be liable for any direct, indirect, consequential, incidental, special, exemplary, or punitive damages or liabilities whatsoever arising from or relating to the Software, the Software content or this Agreement, whether based on contract, tort (including negligence), strict liability or other theory, even if such party has been advised of the possibility of such damages.
|
||||
Except for indemnification obligations in Section 13.5, in no event will Licensor liability exceed the Software license price as indicated in the invoice. The existence of more than one claim will not enlarge or extend this limit.
|
||||
|
||||
In no event will GoReplay LLC liability exceed the Software license price as indicated in the invoice. The existence of more than one claim will not enlarge or extend this limit.
|
||||
|
||||
10. Remedies. Your exclusive remedy and GoReplay LLC’ entire liability for breach of this Agreement shall be limited, at GoReplay LLC’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to GoReplay LLC, payable in accordance with GoReplay LLC' refund policy.
|
||||
10. Remedies. Your exclusive remedy and Licensor’ entire liability for breach of this Agreement shall be limited, at Licensor’ sole and exclusive discretion, to (a) replacement of any defective software or documentation; or (b) refund of the license fee paid to Licensor, payable in accordance with Licensor' refund policy.
|
||||
|
||||
11. Acknowledgements.
|
||||
|
||||
11.1 Consent to the Use of Data. You agree that GoReplay LLC and its affiliates may collect and use technical information gathered as part of the product support services to be used in an anonymized manner. GoReplay LLC may use this information solely to improve products and services and will not disclose this information in a form that personally identifies you.
|
||||
11.1 Consent to the Use of Data. You agree that Licensor and its affiliates may collect and use technical information gathered as part of the product support services to be used in an anonymized manner. Licensor may use this information solely to improve products and services and will not disclose this information in a form that personally identifies you.
|
||||
|
||||
11.2 Verification. We or a certified auditor acting on our behalf, may, upon its reasonable request and at its expense, audit you with respect to the use of the Software, subject to your reasonable information security and confidentiality protections. Such audit may be conducted by mail, electronic means or through an in-person visit to your place of business. Any such in-person audit shall be conducted during regular business hours at your facilities and shall not unreasonably interfere with your business activities. We shall not remove, copy, or redistribute any electronic material during the course of an audit. If an audit reveals that you are using the Software in a way that is in material violation of the terms of the EULA, then you shall pay our reasonable costs of conducting the audit. In the case of a material violation, you agree to pay Us any amounts owing that are attributable to the unauthorized use. In the alternative, We reserve the right, at our sole option, to terminate the licenses for the Software.
|
||||
|
||||
@@ -73,13 +71,13 @@ In no event will GoReplay LLC liability exceed the Software license price as ind
|
||||
|
||||
13.1 Entire Agreement. This Agreement sets forth our entire agreement with respect to the Software and the subject matter hereof and supersedes all prior and contemporaneous understandings and agreements whether written or oral.
|
||||
|
||||
13.2 Amendment. GoReplay LLC reserves the right, in its sole discretion, to amend this Agreement from time. Amendments to this Agreement can be located at: https://github.com/buger/goreplay/blob/master/COMM-LICENSE.
|
||||
13.2 Amendment. Licensor reserves the right, in its sole discretion, to amend this Agreement from time. Amendments to this Agreement can be located at: https://github.com/buger/goreplay/blob/master/COMM-LICENSE.
|
||||
|
||||
13.3 Assignment. You may not assign this Agreement or any of its rights under this Agreement without the prior written consent of GoReplay LLC and any attempted assignment without such consent shall be void.
|
||||
13.3 Assignment. You may not assign this Agreement or any of its rights under this Agreement without the prior written consent of Licensor and any attempted assignment without such consent shall be void.
|
||||
|
||||
13.4 Export Compliance. Each party agrees to comply with all applicable laws and regulations, including laws, regulations, orders or other restrictions on export, re-export or redistribution of software.
|
||||
|
||||
13.5 Indemnification. You agree to defend, indemnify, and hold harmless GoReplay LLC from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) arising out of your use of the Software or breach of this Agreement.
|
||||
13.5 Indemnification. Licensor agrees to defend, indemnify, and hold harmless You from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) arising out of a claim brought by a third-party alleging that the Software directly infringes such third-party’s intellectual property rights; provided that Licensor shall have no indemnity obligation for claims arising out of your modification to the Software. You agree to defend, indemnify, and hold harmless Licensor from and against any lawsuits, claims, losses, damages, fines and expenses (including attorneys' fees and costs) brought by a third-party arising out of your modifications to the Software or breach of this Agreement.
|
||||
|
||||
13.6 Governing Law. This Agreement is governed by the laws of the State of Oregon and the United States without regard to conflicts of laws provisions thereof, and without regard to the United Nations Convention on the International Sale of Goods or the Uniform Computer Information Transactions Act, as currently enacted by any jurisdiction or as may be codified or amended from time to time by any jurisdiction. The jurisdiction and venue for actions related to the subject matter hereof shall be the state of Oregon and United States federal courts located in Portland, Oregon, and both parties hereby submit to the personal jurisdiction of such courts.
|
||||
|
||||
@@ -91,4 +89,4 @@ In no event will GoReplay LLC liability exceed the Software license price as ind
|
||||
|
||||
13.10 Headings. The headings of sections and paragraphs of this Agreement are for convenience of reference only and are not intended to restrict, affect or be of any weight in the interpretation or construction of the provisions of such sections or paragraphs.
|
||||
|
||||
14. Contact Information. If you have any questions about this EULA, or if you want to contact GoReplay LLC for any reason, please direct correspondence to info@gortool.com
|
||||
14. Contact Information. If you have any questions about this EULA, or if you want to contact Licensor for any reason, please direct correspondence to hello@goreplay.org
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.12 as builder
|
||||
FROM alpine:3.16 as builder
|
||||
|
||||
ARG RELEASE_VERSION
|
||||
|
||||
@@ -7,5 +7,6 @@ RUN wget https://github.com/buger/goreplay/releases/download/${RELEASE_VERSION}/
|
||||
RUN tar xzf gor.tar.gz
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /gor .
|
||||
ENTRYPOINT ["./gor"]
|
||||
|
||||
+3
-5
@@ -1,14 +1,12 @@
|
||||
FROM golang:1.14
|
||||
FROM golang:1.19
|
||||
|
||||
RUN apt-get update && apt-get install ruby vim-common -y
|
||||
|
||||
RUN apt-get install flex bison -y
|
||||
RUN wget http://www.tcpdump.org/release/libpcap-1.9.1.tar.gz && tar xzf libpcap-1.9.1.tar.gz && cd libpcap-1.9.1 && ./configure && make install
|
||||
|
||||
RUN go get github.com/google/gopacket
|
||||
RUN go get -u golang.org/x/lint/golint
|
||||
RUN wget http://www.tcpdump.org/release/libpcap-1.10.0.tar.gz && tar xzf libpcap-1.10.0.tar.gz && cd libpcap-1.10.0 && ./configure && make install
|
||||
|
||||
WORKDIR /go/src/github.com/buger/goreplay/
|
||||
ADD . /go/src/github.com/buger/goreplay/
|
||||
|
||||
RUN go get golang.org/x/lint/golint
|
||||
RUN go get
|
||||
|
||||
@@ -1,69 +1,98 @@
|
||||
SOURCE = $(shell ls -1 *.go | grep -v _test.go)
|
||||
PROJECT_NAME := goreplay
|
||||
SOURCE_PATH = /go/src/github.com/buger/goreplay/
|
||||
PORT = 8000
|
||||
FADDR = :8000
|
||||
CONTAINER=gor
|
||||
PREFIX=
|
||||
RUN = docker run -v `pwd`:$(SOURCE_PATH) -e AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) -e AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) -p 0.0.0.0:$(PORT):$(PORT) -t -i $(CONTAINER)
|
||||
DIST_PATH = dist
|
||||
CONTAINER_AMD=gor-amd64
|
||||
CONTAINER_ARM=gor-arm64
|
||||
RUN = docker run --rm -v `pwd`:$(SOURCE_PATH) -e AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) -e AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) -p 0.0.0.0:$(PORT):$(PORT) -t -i $(CONTAINER_AMD)
|
||||
BENCHMARK = BenchmarkRAWInput
|
||||
TEST = TestRawListenerBench
|
||||
BIN_NAME = gor
|
||||
VERSION = DEV-$(shell date +%s)
|
||||
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -extldflags \"-static\" -X main.DEMO=$(DEMO)"
|
||||
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION)$(PREFIX) -X main.DEMO=$(DEMO)"
|
||||
FADDR = ":8000"
|
||||
VERSION := DEV-$(shell date +%s)
|
||||
LDFLAGS = -ldflags "-X main.VERSION=$(VERSION) -extldflags \"-static\" -X main.DEMO=$(DEMO)"
|
||||
MAC_LDFLAGS = -ldflags "-X main.VERSION=$(VERSION) -X main.DEMO=$(DEMO)"
|
||||
DOCKER_FPM_CMD := docker run --rm -t -v `pwd`:/src -w /src fleetdm/fpm
|
||||
|
||||
FPMCOMMON= \
|
||||
--name goreplay \
|
||||
FPM_COMMON= \
|
||||
--name $(PROJECT_NAME) \
|
||||
--description "GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis." \
|
||||
-v $(VERSION) \
|
||||
--vendor "Leonid Bugaev" \
|
||||
-m "<support@goreplay.org>" \
|
||||
--url "https://goreplay.org" \
|
||||
-s dir \
|
||||
-C /tmp/gor-build \
|
||||
-s dir
|
||||
|
||||
release: release-x64 release-mac
|
||||
release: clean release-linux-amd64 release-linux-arm64 release-mac-amd64 release-mac-arm64 release-windows
|
||||
|
||||
release-bin:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
go mod vendor
|
||||
|
||||
release-bin-mac:
|
||||
GOOS=darwin go build -o $(BIN_NAME) $(MAC_LDFLAGS)
|
||||
release-bin-linux-amd64: vendor
|
||||
docker run --platform linux/amd64 --rm -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER_AMD) go build -mod=vendor -o $(BIN_NAME) -tags netgo $(LDFLAGS) ./cmd/gor/
|
||||
|
||||
release-x64:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=amd64 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
tar -czf gor_$(VERSION)$(PREFIX)_x64.tar.gz $(BIN_NAME)
|
||||
mkdir -p /tmp/gor-build
|
||||
mv ./$(BIN_NAME) /tmp/gor-build/$(BIN_NAME)
|
||||
cd /tmp/gor-build
|
||||
rm -f goreplay_$(VERSION)_amd64.deb
|
||||
rm -f goreplay-$(VERSION)-1.x86_64.rpm
|
||||
fpm $(FPMCOMMON) -a amd64 -t deb ./=/usr/local/bin
|
||||
fpm $(FPMCOMMON) -a amd64 -t rpm ./=/usr/local/bin
|
||||
rm -rf /tmp/gor-build
|
||||
release-bin-linux-arm64: vendor
|
||||
docker run --platform linux/arm64 --rm -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=arm64 -i $(CONTAINER_ARM) go build -mod=vendor -o $(BIN_NAME) -tags netgo $(LDFLAGS) ./cmd/gor/
|
||||
|
||||
release-x86:
|
||||
docker run -v `pwd`:$(SOURCE_PATH) -t --env GOOS=linux --env GOARCH=386 -i $(CONTAINER) go build -o $(BIN_NAME) -tags netgo $(LDFLAGS)
|
||||
tar -czf gor_$(VERSION)$(PREFIX)_x86.tar.gz $(BIN_NAME)
|
||||
rm $(BIN_NAME)
|
||||
release-bin-mac-amd64: vendor
|
||||
GOOS=darwin go build -mod=vendor -o $(BIN_NAME) $(MAC_LDFLAGS) ./cmd/gor/
|
||||
|
||||
release-mac:
|
||||
go build -o $(BIN_NAME) $(MAC_LDFLAGS)
|
||||
tar -czf gor_$(VERSION)$(PREFIX)_mac.tar.gz $(BIN_NAME)
|
||||
mkdir -p /tmp/gor-build
|
||||
mv ./$(BIN_NAME) /tmp/gor-build/$(BIN_NAME)
|
||||
cd /tmp/gor-build
|
||||
rm -f goreplay-$(VERSION).pkg
|
||||
fpm $(FPMCOMMON) -a amd64 -t osxpkg ./=/usr/local/bin
|
||||
rm -rf /tmp/gor-build
|
||||
release-bin-mac-arm64: vendor
|
||||
GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -mod=vendor -o $(BIN_NAME) $(MAC_LDFLAGS)
|
||||
|
||||
release-bin-windows: vendor
|
||||
docker run -it --rm -v `pwd`:$(SOURCE_PATH) -w $(SOURCE_PATH) -e CGO_ENABLED=1 docker.elastic.co/beats-dev/golang-crossbuild:1.19.2-main --build-cmd "make VERSION=$(VERSION) build" -p "windows/amd64" ./cmd/gor/
|
||||
mv $(BIN_NAME) "$(BIN_NAME).exe"
|
||||
|
||||
release-linux-amd64: dist release-bin-linux-amd64
|
||||
tar -czf $(DIST_PATH)/gor_$(VERSION)_linux_amd64.tar.gz $(BIN_NAME)
|
||||
$(DOCKER_FPM_CMD) $(FPM_COMMON) -f -t deb -a arm64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
$(DOCKER_FPM_CMD) $(FPM_COMMON) -f -t rpm -a arm64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
rm -rf $(BIN_NAME)
|
||||
|
||||
release-linux-arm64: dist release-bin-linux-arm64
|
||||
tar -czf $(DIST_PATH)/gor_$(VERSION)_linux_arm64.tar.gz $(BIN_NAME)
|
||||
$(DOCKER_FPM_CMD) $(FPM_COMMON) -f -t deb -a arm64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
$(DOCKER_FPM_CMD) $(FPM_COMMON) -f -t rpm -a arm64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
rm -rf $(BIN_NAME)
|
||||
|
||||
release-mac-amd64: dist release-bin-mac-amd64
|
||||
tar -czf $(DIST_PATH)/gor_$(VERSION)_darwin_amd64.tar.gz $(BIN_NAME)
|
||||
fpm $(FPM_COMMON) -f -t osxpkg -a amd64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
mv ./$(DIST_PATH)/$(PROJECT_NAME)-$(VERSION).pkg ./$(DIST_PATH)/$(PROJECT_NAME)-$(VERSION)-amd64.pkg
|
||||
rm -rf $(BIN_NAME)
|
||||
|
||||
release-mac-arm64: dist release-bin-mac-arm64
|
||||
tar -czf $(DIST_PATH)/gor_$(VERSION)_darwin_arm64.tar.gz $(BIN_NAME)
|
||||
fpm $(FPM_COMMON) -f -t osxpkg -a arm64 -p ./$(DIST_PATH) ./gor=/usr/local/bin
|
||||
mv ./$(DIST_PATH)/$(PROJECT_NAME)-$(VERSION).pkg ./$(DIST_PATH)/$(PROJECT_NAME)-$(VERSION)-arm64.pkg
|
||||
rm -rf $(BIN_NAME)
|
||||
|
||||
release-windows: dist release-bin-windows
|
||||
zip $(DIST_PATH)/gor-$(VERSION)_windows.zip "$(BIN_NAME).exe"
|
||||
rm -rf "$(BIN_NAME).exe"
|
||||
|
||||
clean:
|
||||
rm -rf $(DIST_PATH)
|
||||
|
||||
build:
|
||||
go build -mod=vendor -o $(BIN_NAME) $(LDFLAGS)
|
||||
|
||||
install:
|
||||
go install $(MAC_LDFLAGS)
|
||||
|
||||
build:
|
||||
docker build -t $(CONTAINER) -f Dockerfile.dev .
|
||||
build-env: build-amd64-env build-arm64-env
|
||||
|
||||
build-amd64-env:
|
||||
docker buildx build --platform linux/amd64 -t $(CONTAINER_AMD) -f Dockerfile.dev .
|
||||
|
||||
build-arm64-env:
|
||||
docker buildx build --platform linux/arm64 -t $(CONTAINER_ARM) -f Dockerfile.dev .
|
||||
|
||||
build-docker:
|
||||
docker build -t gor-dev -f Dockerfile .
|
||||
|
||||
profile:
|
||||
go build && ./$(BIN_NAME) --output-http="http://localhost:9000" --input-dummy 0 --input-raw :9000 --input-http :9000 --memprofile=./mem.out --cpuprofile=./cpu.out --stats --output-http-stats --output-http-timeout 100ms
|
||||
@@ -102,7 +131,7 @@ profile_test:
|
||||
|
||||
# Used mainly for debugging, because docker container do not have access to parent machine ports
|
||||
run:
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --verbose --debug --middleware "./examples/middleware/echo.sh" --output-file requests.gor
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-dummy=0 --output-http="http://localhost:9000" --input-raw-track-response --input-raw 127.0.0.1:9000 --verbose 0 --middleware "./examples/middleware/echo.sh" --output-file requests.gor
|
||||
|
||||
run-2:
|
||||
$(RUN) go run $(LDFLAGS) $(SOURCE) --input-raw :8000 --input-raw-bpf-filter "dst port 8000" --output-stdout --output-http "http://localhost:8000" --input-dummy=0
|
||||
@@ -128,19 +157,5 @@ replay:
|
||||
bash:
|
||||
$(RUN) /bin/bash
|
||||
|
||||
|
||||
FPMCOMMON= \
|
||||
--name gor \
|
||||
--description "GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis." \
|
||||
-v $(VERSION) \
|
||||
--vendor "Leonid Bugaev" \
|
||||
-m "<support@goreplay.org>" \
|
||||
--url "https://goreplay.org" \
|
||||
-s dir \
|
||||
-C /tmp/gor-build \
|
||||
|
||||
build_packages:
|
||||
mkdir -p /tmp/gor-build
|
||||
go build -i -o /tmp/gor-build/$(BIN_NAME)
|
||||
fpm $(FPMCOMMON) -a amd64 -t deb ./=/usr/local/bin
|
||||
fpm $(FPMCOMMON) -a amd64 -t rpm ./=/usr/local/bin
|
||||
dist:
|
||||
mkdir -p $(DIST_PATH)
|
||||
@@ -1,10 +1,10 @@
|
||||
[](https://github.com/buger/goreplay/releases) [](https://codebeat.co/projects/github-com-buger-gor) [](https://goreportcard.com/report/github.com/buger/gor) [](https://gitter.im/buger/gor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://houndci.com)
|
||||
<a href="https://semgrep.dev/login?utm_source=github&utm_medium=badge&utm_campaign=growth-oss"><img src="https://img.shields.io/badge/semgrep-security-green.svg" /></a> [](https://github.com/buger/goreplay/releases) [](https://codebeat.co/projects/github-com-buger-gor) [](https://goreportcard.com/report/github.com/buger/gor) [](https://gitter.im/buger/gor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://houndci.com)
|
||||
|
||||

|
||||
|
||||
## https://goreplay.org/
|
||||
|
||||
GoReplay is an open-source network monitoring tool which can record your live traffic, and use it for shadowing, load testing, monitoring and detailed analysis.
|
||||
GoReplay is an open-source network monitoring tool which can record your live traffic and use it for shadowing, load testing, monitoring and detailed analysis.
|
||||
|
||||
## About
|
||||
|
||||
@@ -13,29 +13,28 @@ As your application grows, the effort required to test it also grows exponential
|
||||
GoReplay increases your confidence in code deployments, configuration and infrastructure changes.
|
||||
|
||||
|
||||
GoReplay offers unique approach for shadowing. Instead of being a proxy, GoReplay in background listen for traffic on your network interface, requiring no changes in your production infrastructure, rather then running GoReplay daemon on the same machine as your service.
|
||||
GoReplay offers a unique approach for shadowing. Instead of being a proxy, GoReplay listens in the background for traffic on your network interfaces, requiring no changes in your production infrastructure, other than running GoReplay daemon on the same machine as your service.
|
||||
|
||||

|
||||
|
||||
Check [latest documentation](http://github.com/buger/goreplay/wiki).
|
||||
|
||||
## Installation
|
||||
Download latest binary from https://github.com/buger/goreplay/releases or [compile by yourself](https://github.com/buger/goreplay/wiki/Compilation).
|
||||
Download the latest binary from https://github.com/buger/goreplay/releases or [compile by yourself](https://github.com/buger/goreplay/wiki/Compilation).
|
||||
|
||||
## Getting started
|
||||
|
||||
The most basic setup will be `sudo ./gor --input-raw :8000 --output-stdout` which acts like tcpdump.
|
||||
If you already have test environment you can start replaying: `sudo ./gor --input-raw :8000 --output-http http://staging.env`.
|
||||
If you already have a test environment, you can start replaying by running: `sudo ./gor --input-raw :8000 --output-http http://staging.env`.
|
||||
|
||||
See the our [documentation](https://github.com/buger/goreplay/wiki/) and [Getting started](https://github.com/buger/goreplay/wiki/Getting-Started) page for more info.
|
||||
See our [documentation](https://github.com/buger/goreplay/wiki/) and the [Getting Started](https://github.com/buger/goreplay/wiki/Getting-Started) page for more info.
|
||||
|
||||
## Newsletter
|
||||
Subscribe to our [newsletter](https://www.getdrip.com/forms/89690474/submissions/new) to stay informed about the latest features and changes to Gor project.
|
||||
|
||||
Subscribe to our [newsletter](https://www.getdrip.com/forms/89690474/submissions/new) to stay informed about the latest features and changes to the Gor project.
|
||||
|
||||
## Want to Upgrade?
|
||||
|
||||
We have created a [GoReplay PRO](https://goreplay.org/pro.html) extension which provides additional features such as support for binary protocols like Thrift or ProtocolBuffers, saving and replaying from cloud storage, TCP sessions replication, etc. The PRO version also includes a commercial-friendly license, dedicated support, and it also allows you to support high-quality open source development.
|
||||
We have created a [GoReplay PRO](https://goreplay.org/pro.html) extension which provides additional features such as support for binary protocols like Thrift or ProtocolBuffers, saving and replaying from cloud storage, TCP session replication, etc. The PRO version also includes a commercial-friendly license, dedicated support, and it also allows you to support high-quality open source development.
|
||||
|
||||
|
||||
## Problems?
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/size"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Handler is a function that is used to handle packets
|
||||
type Handler func(*Packet)
|
||||
|
||||
// PcapOptions options that can be set on a pcap capture handle,
|
||||
// these options take effect on inactive pcap handles
|
||||
type PcapOptions struct {
|
||||
BufferTimeout time.Duration `json:"input-raw-buffer-timeout"`
|
||||
TimestampType string `json:"input-raw-timestamp-type"`
|
||||
BPFFilter string `json:"input-raw-bpf-filter"`
|
||||
BufferSize size.Size `json:"input-raw-buffer-size"`
|
||||
Promiscuous bool `json:"input-raw-promisc"`
|
||||
Monitor bool `json:"input-raw-monitor"`
|
||||
Snaplen bool `json:"input-raw-override-snaplen"`
|
||||
}
|
||||
|
||||
// Listener handle traffic capture, this is its representation.
|
||||
type Listener struct {
|
||||
sync.Mutex
|
||||
Transport string // transport layer default to tcp
|
||||
Activate func() error // function is used to activate the engine. it must be called before reading packets
|
||||
Handles map[string]gopacket.PacketDataSource
|
||||
Interfaces []net.Interface
|
||||
loopIndex int
|
||||
Reading chan bool // this channel is closed when the listener has started reading packets
|
||||
PcapOptions
|
||||
Engine EngineType
|
||||
port uint16 // src or/and dst port
|
||||
trackResponse bool
|
||||
|
||||
host string // pcap file name or interface (name, hardware addr, index or ip address)
|
||||
|
||||
closeDone chan struct{}
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// EngineType ...
|
||||
type EngineType uint8
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EnginePcap EngineType = 1 << iota
|
||||
EnginePcapFile
|
||||
EngineRawSocket
|
||||
)
|
||||
|
||||
// Set is here so that EngineType can implement flag.Var
|
||||
func (eng *EngineType) Set(v string) error {
|
||||
switch v {
|
||||
case "", "libpcap":
|
||||
*eng = EnginePcap
|
||||
case "pcap_file":
|
||||
*eng = EnginePcapFile
|
||||
case "raw_socket", "af_packet":
|
||||
*eng = EngineRawSocket
|
||||
default:
|
||||
return fmt.Errorf("invalid engine %s", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eng *EngineType) String() (e string) {
|
||||
switch *eng {
|
||||
case EnginePcapFile:
|
||||
e = "pcap_file"
|
||||
case EnginePcap:
|
||||
e = "libpcap"
|
||||
case EngineRawSocket:
|
||||
e = "raw_socket"
|
||||
default:
|
||||
e = ""
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// NewListener creates and initialize a new Listener. if transport or/and engine are invalid/unsupported
|
||||
// is "tcp" and "pcap", are assumed. l.Engine and l.Transport can help to get the values used.
|
||||
// if there is an error it will be associated with getting network interfaces
|
||||
func NewListener(host string, port uint16, transport string, engine EngineType, trackResponse bool) (l *Listener, err error) {
|
||||
l = &Listener{}
|
||||
|
||||
l.host = host
|
||||
l.port = port
|
||||
l.Transport = "tcp"
|
||||
if transport != "" {
|
||||
l.Transport = transport
|
||||
}
|
||||
l.Handles = make(map[string]gopacket.PacketDataSource)
|
||||
l.trackResponse = trackResponse
|
||||
l.closeDone = make(chan struct{})
|
||||
l.quit = make(chan struct{})
|
||||
l.Reading = make(chan bool)
|
||||
switch engine {
|
||||
default:
|
||||
l.Engine = EnginePcap
|
||||
l.Activate = l.activatePcap
|
||||
case EngineRawSocket:
|
||||
l.Engine = EngineRawSocket
|
||||
l.Activate = l.activateRawSocket
|
||||
case EnginePcapFile:
|
||||
l.Engine = EnginePcapFile
|
||||
l.Activate = l.activatePcapFile
|
||||
return
|
||||
}
|
||||
err = l.setInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetPcapOptions set pcap options for all yet to be actived pcap handles
|
||||
// setting this on already activated handles will not have any effect
|
||||
func (l *Listener) SetPcapOptions(opts PcapOptions) {
|
||||
l.PcapOptions = opts
|
||||
}
|
||||
|
||||
// Listen listens for packets from the handles, and call handler on every packet received
|
||||
// until the context done signal is sent or there is unrecoverable error on all handles.
|
||||
// this function must be called after activating pcap handles
|
||||
func (l *Listener) Listen(ctx context.Context, handler Handler) (err error) {
|
||||
l.read(handler)
|
||||
done := ctx.Done()
|
||||
select {
|
||||
case <-done:
|
||||
close(l.quit) // signal close on all handles
|
||||
<-l.closeDone // wait all handles to be closed
|
||||
err = ctx.Err()
|
||||
case <-l.closeDone: // all handles closed voluntarily
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ListenBackground is like listen but can run concurrently and signal error through channel
|
||||
func (l *Listener) ListenBackground(ctx context.Context, handler Handler) chan error {
|
||||
err := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(err)
|
||||
if e := l.Listen(ctx, handler); err != nil {
|
||||
err <- e
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
// Filter returns automatic filter applied by goreplay
|
||||
// to a pcap handle of a specific interface
|
||||
func (l *Listener) Filter(ifi net.Interface) (filter string) {
|
||||
// https://www.tcpdump.org/manpages/pcap-filter.7.html
|
||||
|
||||
port := fmt.Sprintf("portrange 0-%d", 1<<16-1)
|
||||
if l.port != 0 {
|
||||
port = fmt.Sprintf("port %d", l.port)
|
||||
}
|
||||
dir := " dst " // direction
|
||||
if l.trackResponse {
|
||||
dir = " "
|
||||
}
|
||||
filter = fmt.Sprintf("(%s%s%s)", l.Transport, dir, port)
|
||||
if listenAll(l.host) || isDevice(l.host, ifi) {
|
||||
return
|
||||
}
|
||||
filter = fmt.Sprintf("(%s%s%s and host %s)", l.Transport, dir, port, l.host)
|
||||
return
|
||||
}
|
||||
|
||||
// PcapDumpHandler returns a handler to write packet data in PCAP
|
||||
// format, See http://wiki.wireshark.org/Development/LibpcapFileFormathandler.
|
||||
// if link layer is invalid Ethernet is assumed
|
||||
func PcapDumpHandler(file *os.File, link layers.LinkType) (handler func(packet *Packet) error, err error) {
|
||||
if link.String() == "" {
|
||||
link = layers.LinkTypeEthernet
|
||||
}
|
||||
w := NewWriterNanos(file)
|
||||
err = w.WriteFileHeader(64<<10, link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return func(packet *Packet) error {
|
||||
return w.WritePacket(*packet.Info, packet.Data)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PcapHandle returns new pcap Handle from dev on success.
|
||||
// this function should be called after setting all necessary options for this listener
|
||||
func (l *Listener) PcapHandle(ifi net.Interface) (handle *pcap.Handle, err error) {
|
||||
var inactive *pcap.InactiveHandle
|
||||
inactive, err = pcap.NewInactiveHandle(ifi.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inactive handle error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
defer inactive.CleanUp()
|
||||
if l.TimestampType != "" {
|
||||
var ts pcap.TimestampSource
|
||||
ts, err = pcap.TimestampSourceFromString(l.TimestampType)
|
||||
err = inactive.SetTimestampSource(ts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q: supported timestamps: %q, interface: %q", err, inactive.SupportedTimestamps(), ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.Promiscuous {
|
||||
if err = inactive.SetPromisc(l.Promiscuous); err != nil {
|
||||
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.Monitor {
|
||||
if err = inactive.SetRFMon(l.Monitor); err != nil && !errors.Is(err, pcap.CannotSetRFMon) {
|
||||
return nil, fmt.Errorf("monitor mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
var snap int
|
||||
if l.Snaplen {
|
||||
snap = 64<<10 + 200
|
||||
} else if ifi.MTU > 0 {
|
||||
snap = ifi.MTU + 200
|
||||
}
|
||||
err = inactive.SetSnapLen(snap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot length error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if l.BufferSize > 0 {
|
||||
err = inactive.SetBufferSize(int(l.BufferSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handle buffer size error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.BufferTimeout == 0 {
|
||||
l.BufferTimeout = pcap.BlockForever
|
||||
}
|
||||
err = inactive.SetTimeout(l.BufferTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handle buffer timeout error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
handle, err = inactive.Activate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PCAP Activate device error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if l.BPFFilter != "" {
|
||||
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
|
||||
l.BPFFilter = "(" + l.BPFFilter + ")"
|
||||
}
|
||||
} else {
|
||||
l.BPFFilter = l.Filter(ifi)
|
||||
}
|
||||
err = handle.SetBPFFilter(l.BPFFilter)
|
||||
if err != nil {
|
||||
handle.Close()
|
||||
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, l.BPFFilter, ifi.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SocketHandle returns new unix ethernet handle associated with this listener settings
|
||||
func (l *Listener) SocketHandle(ifi net.Interface) (handle Socket, err error) {
|
||||
handle, err = NewSocket(ifi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sock raw error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if err = handle.SetPromiscuous(l.Promiscuous || l.Monitor); err != nil {
|
||||
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if l.BPFFilter != "" {
|
||||
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
|
||||
l.BPFFilter = "(" + l.BPFFilter + ")"
|
||||
}
|
||||
} else {
|
||||
l.BPFFilter = l.Filter(ifi)
|
||||
}
|
||||
if err = handle.SetBPFFilter(l.BPFFilter); err != nil {
|
||||
handle.Close()
|
||||
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, l.BPFFilter, ifi.Name)
|
||||
}
|
||||
handle.SetLoopbackIndex(int32(l.loopIndex))
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Listener) read(handler Handler) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
for key, handle := range l.Handles {
|
||||
go func(key string, hndl gopacket.PacketDataSource) {
|
||||
defer l.closeHandles(key)
|
||||
linkSize := 14
|
||||
linkType := int(layers.LinkTypeEthernet)
|
||||
if _, ok := hndl.(*pcap.Handle); ok {
|
||||
linkType = int(hndl.(*pcap.Handle).LinkType())
|
||||
linkSize, ok = pcapLinkTypeLength(linkType)
|
||||
if !ok {
|
||||
if os.Getenv("GORDEBUG") != "0" {
|
||||
log.Printf("can not identify link type of an interface '%s'\n", key)
|
||||
}
|
||||
return // can't find the linktype size
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-l.quit:
|
||||
return
|
||||
default:
|
||||
data, ci, err := hndl.ReadPacketData()
|
||||
if err == nil {
|
||||
handler(NewPacket(data, linkType, linkSize, &ci))
|
||||
continue
|
||||
}
|
||||
if enext, ok := err.(pcap.NextError); ok && enext == pcap.NextErrorTimeoutExpired {
|
||||
continue
|
||||
}
|
||||
if eno, ok := err.(unix.Errno); ok && eno.Temporary() {
|
||||
continue
|
||||
}
|
||||
if enet, ok := err.(*net.OpError); ok && (enet.Temporary() || enet.Timeout()) {
|
||||
continue
|
||||
}
|
||||
if err == io.EOF || err == io.ErrClosedPipe {
|
||||
return
|
||||
}
|
||||
if os.Getenv("GORDEBUG") != "0" {
|
||||
log.Printf("stopped reading from %s interface with error %s\n", key, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}(key, handle)
|
||||
}
|
||||
close(l.Reading)
|
||||
}
|
||||
|
||||
func (l *Listener) closeHandles(key string) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
if handle, ok := l.Handles[key]; ok {
|
||||
if _, ok = handle.(Socket); ok {
|
||||
handle.(Socket).Close()
|
||||
} else {
|
||||
handle.(*pcap.Handle).Close()
|
||||
}
|
||||
delete(l.Handles, key)
|
||||
if len(l.Handles) == 0 {
|
||||
close(l.closeDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) activatePcap() error {
|
||||
var e error
|
||||
var msg string
|
||||
for _, ifi := range l.Interfaces {
|
||||
var handle *pcap.Handle
|
||||
handle, e = l.PcapHandle(ifi)
|
||||
if e != nil {
|
||||
msg += ("\n" + e.Error())
|
||||
continue
|
||||
}
|
||||
l.Handles[ifi.Name] = handle
|
||||
}
|
||||
if len(l.Handles) == 0 {
|
||||
return fmt.Errorf("pcap handles error:%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) activateRawSocket() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return fmt.Errorf("sock_raw is not stabilized on OS other than linux")
|
||||
}
|
||||
var msg string
|
||||
var e error
|
||||
for _, ifi := range l.Interfaces {
|
||||
var handle Socket
|
||||
handle, e = l.SocketHandle(ifi)
|
||||
if e != nil {
|
||||
msg += ("\n" + e.Error())
|
||||
continue
|
||||
}
|
||||
l.Handles[ifi.Name] = handle
|
||||
}
|
||||
if len(l.Handles) == 0 {
|
||||
return fmt.Errorf("raw socket handles error:%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) activatePcapFile() (err error) {
|
||||
var handle *pcap.Handle
|
||||
var e error
|
||||
if handle, e = pcap.OpenOffline(l.host); e != nil {
|
||||
return fmt.Errorf("open pcap file error: %q", e)
|
||||
}
|
||||
if l.BPFFilter != "" {
|
||||
if l.BPFFilter[0] != '(' || l.BPFFilter[len(l.BPFFilter)-1] != ')' {
|
||||
l.BPFFilter = "(" + l.BPFFilter + ")"
|
||||
}
|
||||
} else {
|
||||
addr := l.host
|
||||
l.host = ""
|
||||
l.BPFFilter = l.Filter(net.Interface{})
|
||||
l.host = addr
|
||||
}
|
||||
if e = handle.SetBPFFilter(l.BPFFilter); e != nil {
|
||||
handle.Close()
|
||||
return fmt.Errorf("BPF filter error: %q, filter: %s", e, l.BPFFilter)
|
||||
}
|
||||
l.Handles["pcap_file"] = handle
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Listener) setInterfaces() (err error) {
|
||||
var ifis []net.Interface
|
||||
ifis, err = net.Interfaces()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := range ifis {
|
||||
if ifis[i].Flags&net.FlagLoopback != 0 {
|
||||
l.loopIndex = ifis[i].Index
|
||||
}
|
||||
if ifis[i].Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
if isDevice(l.host, ifis[i]) {
|
||||
l.Interfaces = []net.Interface{ifis[i]}
|
||||
return
|
||||
}
|
||||
addrs, e := ifis[i].Addrs()
|
||||
if e != nil {
|
||||
// don't give up on a failure from a single interface
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if cutMask(addr) == l.host {
|
||||
l.Interfaces = []net.Interface{ifis[i]}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
l.Interfaces = ifis
|
||||
return
|
||||
}
|
||||
|
||||
func cutMask(addr net.Addr) string {
|
||||
mask := addr.String()
|
||||
for i, v := range mask {
|
||||
if v == '/' {
|
||||
return mask[:i]
|
||||
}
|
||||
}
|
||||
return mask
|
||||
}
|
||||
|
||||
func isDevice(addr string, ifi net.Interface) bool {
|
||||
return addr == ifi.Name || addr == fmt.Sprintf("%d", ifi.Index) || (addr != "" && addr == ifi.HardwareAddr.String())
|
||||
}
|
||||
|
||||
func listenAll(addr string) bool {
|
||||
switch addr {
|
||||
case "", "0.0.0.0", "[::]", "::":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
var LoopBack = func() net.Interface {
|
||||
ifis, _ := net.Interfaces()
|
||||
for _, v := range ifis {
|
||||
if v.Flags&net.FlagLoopback != 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ifis[0]
|
||||
}()
|
||||
|
||||
func TestSetInterfaces(t *testing.T) {
|
||||
l := &Listener{}
|
||||
l.host = "127.0.0.1"
|
||||
l.setInterfaces()
|
||||
if len(l.Interfaces) != 1 {
|
||||
t.Error("expected a single interface")
|
||||
}
|
||||
l.host = LoopBack.HardwareAddr.String()
|
||||
l.setInterfaces()
|
||||
if l.Interfaces[0].Name != LoopBack.Name && len(l.Interfaces) != 1 {
|
||||
t.Error("interface should be loop back interface")
|
||||
}
|
||||
l.host = ""
|
||||
l.setInterfaces()
|
||||
if len(l.Interfaces) < 1 {
|
||||
t.Error("should get all interfaces")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBPFFilter(t *testing.T) {
|
||||
l := &Listener{}
|
||||
l.host = "127.0.0.1"
|
||||
l.Transport = "tcp"
|
||||
l.setInterfaces()
|
||||
filter := l.Filter(l.Interfaces[0])
|
||||
if filter != "(tcp dst portrange 0-65535 and host 127.0.0.1)" {
|
||||
t.Error("wrong filter", filter)
|
||||
}
|
||||
l.port = 8000
|
||||
l.trackResponse = true
|
||||
filter = l.Filter(l.Interfaces[0])
|
||||
if filter != "(tcp port 8000 and host 127.0.0.1)" {
|
||||
t.Error("wrong filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPcapDump(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "pcap_file")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
|
||||
packets := Packets(1, 5, 5, 4)
|
||||
for i := 0; i < len(packets); i++ {
|
||||
if i == 1 {
|
||||
// change dst port
|
||||
binary.BigEndian.PutUint16(packets[i].TransLayer[2:], 8001)
|
||||
}
|
||||
if i == 4 {
|
||||
packets[i].Info.CaptureLength = 40
|
||||
}
|
||||
h(packets[i])
|
||||
}
|
||||
name := f.Name()
|
||||
f.Close()
|
||||
testPcapDumpEngine(name, t)
|
||||
}
|
||||
|
||||
func testPcapDumpEngine(f string, t *testing.T) {
|
||||
defer os.Remove(f)
|
||||
l, err := NewListener(f, 8000, "", EnginePcapFile, true)
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %q", err)
|
||||
return
|
||||
}
|
||||
pckts := 0
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
err = l.Listen(ctx, func(packet *Packet) {
|
||||
if packet.Info.CaptureLength != 57 {
|
||||
t.Errorf("expected packet length to be %d, got %d", 57, packet.Info.CaptureLength)
|
||||
}
|
||||
pckts++
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %q", err)
|
||||
}
|
||||
if pckts != 3 {
|
||||
t.Errorf("expected %d packets, got %d packets", 3, pckts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPcapHandler(t *testing.T) {
|
||||
l, err := NewListener(LoopBack.Name, 8000, "", EnginePcap, true)
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
return
|
||||
}
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
return
|
||||
}
|
||||
defer l.Handles[LoopBack.Name].(*pcap.Handle).Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
return
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
_, _ = net.Dial("tcp", "127.0.0.1:8000")
|
||||
}
|
||||
sts, _ := l.Handles[LoopBack.Name].(*pcap.Handle).Stats()
|
||||
if sts.PacketsReceived < 5 {
|
||||
t.Errorf("expected >=5 packets got %d", sts.PacketsReceived)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocketHandler(t *testing.T) {
|
||||
l, err := NewListener(LoopBack.Name, 8000, "", EngineRawSocket, true)
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer l.Handles[LoopBack.Name].(*SockRaw).Close()
|
||||
if err != nil {
|
||||
t.Errorf("expected error to be nil, got %v", err)
|
||||
return
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
_, _ = net.Dial("tcp", "127.0.0.1:8000")
|
||||
}
|
||||
sts, _ := l.Handles[LoopBack.Name].(*SockRaw).Stats()
|
||||
if sts.Packets < 5 {
|
||||
t.Errorf("expected >=5 packets got %d", sts.Packets)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPcapDump(b *testing.B) {
|
||||
f, err := ioutil.TempFile("", "pcap_file")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
defer os.Remove(f.Name())
|
||||
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
|
||||
packets := Packets(1, b.N, 5, 4)
|
||||
for i := 0; i < len(packets); i++ {
|
||||
h(packets[i])
|
||||
}
|
||||
f.Close()
|
||||
b.Logf("%d packets in %s", b.N, time.Since(now))
|
||||
}
|
||||
|
||||
func BenchmarkPcapFile(b *testing.B) {
|
||||
f, err := ioutil.TempFile("", "pcap_file")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
h, _ := PcapDumpHandler(f, layers.LinkTypeLoop)
|
||||
packets := Packets(1, b.N, 5, 4)
|
||||
for i := 0; i < len(packets); i++ {
|
||||
h(packets[i])
|
||||
}
|
||||
name := f.Name()
|
||||
f.Close()
|
||||
b.ResetTimer()
|
||||
var l *Listener
|
||||
l, err = NewListener(name, 8000, "", EnginePcapFile, true)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
pckts := 0
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
if err = l.Listen(ctx, func(packet *Packet) {
|
||||
if packet.Info.CaptureLength != 49 {
|
||||
b.Errorf("expected packet length to be %d, got %d", 49, packet.Info.CaptureLength)
|
||||
}
|
||||
pckts++
|
||||
}); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
b.Logf("%d/%d packets in %s", pckts, b.N, time.Since(now))
|
||||
}
|
||||
|
||||
// used to benchmark sock engine
|
||||
var buf [1024]byte
|
||||
|
||||
func init() {
|
||||
for i := 0; i < len(buf); i++ {
|
||||
buf[i] = 0xff
|
||||
}
|
||||
}
|
||||
|
||||
func handler(n, counter *int32) Handler {
|
||||
return func(p *Packet) {
|
||||
nn := int32(len(p.Data))
|
||||
atomic.AddInt32(n, nn)
|
||||
atomic.AddInt32(counter, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPcap(b *testing.B) {
|
||||
var err error
|
||||
n := new(int32)
|
||||
counter := new(int32)
|
||||
l, err := NewListener(LoopBack.Name, 8000, "", EnginePcap, false)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
l.PcapOptions.BPFFilter = "udp dst port 8000 and host 127.0.0.1"
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
errCh := l.ListenBackground(context.Background(), handler(n, counter))
|
||||
select {
|
||||
case <-l.Reading:
|
||||
case err = <-errCh:
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
var conn net.Conn
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
conn, err = net.Dial("udp", "127.0.0.1:8000")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
b.StartTimer()
|
||||
_, err = conn.Write(buf[:])
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(atomic.LoadInt32(n)), "buf")
|
||||
b.ReportMetric(float64(atomic.LoadInt32(counter)), "packets")
|
||||
}
|
||||
|
||||
func BenchmarkRawSocket(b *testing.B) {
|
||||
var err error
|
||||
n := new(int32)
|
||||
counter := new(int32)
|
||||
l, err := NewListener(LoopBack.Name, 8000, "", EngineRawSocket, false)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
l.PcapOptions.BPFFilter = "udp dst port 8000 and host 127.0.0.1"
|
||||
err = l.Activate()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
errCh := l.ListenBackground(context.Background(), handler(n, counter))
|
||||
select {
|
||||
case <-l.Reading:
|
||||
case err = <-errCh:
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
var conn net.Conn
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
conn, err = net.Dial("udp", "127.0.0.1:8000")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
b.StartTimer()
|
||||
_, err = conn.Write(buf[:])
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
b.ReportMetric(float64(atomic.LoadInt32(n)), "buf")
|
||||
b.ReportMetric(float64(atomic.LoadInt32(counter)), "packets")
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
// Packet properties of a pcaket
|
||||
type Packet struct {
|
||||
Data []byte
|
||||
LinkLayer []byte
|
||||
NetLayer []byte
|
||||
NetOptsLen int // length of extension headers(IPV6) or options(IPV4)
|
||||
TransLayer []byte
|
||||
TransOptsLen int // length of tcp options
|
||||
Payload []byte
|
||||
|
||||
LinkType int
|
||||
Info *gopacket.CaptureInfo
|
||||
Err error
|
||||
}
|
||||
|
||||
// NewPacket parses packet and stop at the first error encountered
|
||||
// pckt.Error will be nil if packet was parsed successfully
|
||||
func NewPacket(data []byte, lType, lTypeLen int, cp *gopacket.CaptureInfo) (p *Packet) {
|
||||
p = new(Packet)
|
||||
p.Info = cp
|
||||
p.Data = data
|
||||
if len(data) < lTypeLen {
|
||||
p.Err = ErrHdrLength("Link")
|
||||
return
|
||||
}
|
||||
p.LinkType = lType
|
||||
p.LinkLayer = data[:lTypeLen]
|
||||
if len(data) <= lTypeLen {
|
||||
p.Err = ErrHdrMissing("IPv4 or IPv6")
|
||||
return
|
||||
}
|
||||
data = data[lTypeLen:]
|
||||
var proto byte
|
||||
if data[0]>>4 == 4 {
|
||||
// IPv4 header
|
||||
if len(data) < 20 {
|
||||
p.Err = ErrHdrLength("IPv4")
|
||||
return
|
||||
}
|
||||
proto = data[9]
|
||||
ihl := int(data[0]&0x0F) * 4
|
||||
if ihl < 20 {
|
||||
p.Err = ErrHdrInvalid("IPv4's IHL")
|
||||
return
|
||||
}
|
||||
if len(data) < ihl {
|
||||
p.Err = ErrHdrLength("IPv4 opts")
|
||||
p.NetLayer = data[:20]
|
||||
return
|
||||
}
|
||||
p.NetOptsLen = ihl - 20
|
||||
p.NetLayer = data[:ihl]
|
||||
} else if data[0]>>4 == 6 {
|
||||
if len(data) < 40 {
|
||||
p.Err = ErrHdrLength("IPv6")
|
||||
return
|
||||
}
|
||||
proto = data[6]
|
||||
totalLen := 40
|
||||
for ipv6ExtensionHdr(proto) {
|
||||
hdr := len(data) - totalLen
|
||||
if hdr < 8 {
|
||||
p.Err = ErrHdrExpected("IPv6 opts")
|
||||
break
|
||||
}
|
||||
extLen := 8
|
||||
if proto != 44 {
|
||||
extLen = int(data[totalLen+1]+1) * 8
|
||||
}
|
||||
if hdr < extLen {
|
||||
p.Err = ErrHdrLength("IPv6 opts")
|
||||
break
|
||||
}
|
||||
p.NetOptsLen += extLen
|
||||
proto = data[totalLen]
|
||||
totalLen += extLen
|
||||
}
|
||||
p.NetLayer = data[:totalLen]
|
||||
if p.Err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
p.Err = ErrHdrExpected("IPv4 or IPv6")
|
||||
return
|
||||
}
|
||||
if proto != 6 {
|
||||
p.Err = ErrHdrExpected("TCP")
|
||||
return
|
||||
}
|
||||
if len(data) <= len(p.NetLayer) {
|
||||
p.Err = ErrHdrMissing("TCP")
|
||||
return
|
||||
}
|
||||
data = data[len(p.NetLayer):]
|
||||
// TCP header
|
||||
if len(data) < 20 {
|
||||
p.Err = ErrHdrLength("TCP")
|
||||
return
|
||||
}
|
||||
dOf := int(data[12]>>4) * 4
|
||||
if dOf < 20 {
|
||||
p.Err = ErrHdrInvalid("TCP's data offset")
|
||||
return
|
||||
}
|
||||
if len(data) < dOf {
|
||||
p.Err = ErrHdrLength("TCP opts")
|
||||
p.TransLayer = data[:20]
|
||||
return
|
||||
}
|
||||
p.TransLayer = data[:dOf]
|
||||
p.TransOptsLen = dOf - 20
|
||||
if len(data) > dOf {
|
||||
p.Payload = data[dOf:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ErrHdrLength returned on short header length
|
||||
type ErrHdrLength string
|
||||
|
||||
func (err ErrHdrLength) Error() string {
|
||||
return "short " + string(err) + " length"
|
||||
}
|
||||
|
||||
// ErrHdrMissing returned on missing header(s)
|
||||
type ErrHdrMissing string
|
||||
|
||||
func (err ErrHdrMissing) Error() string {
|
||||
return "missing " + string(err) + " header(s)"
|
||||
}
|
||||
|
||||
// ErrHdrExpected returned when header(s) are different from the one expected
|
||||
type ErrHdrExpected string
|
||||
|
||||
func (err ErrHdrExpected) Error() string {
|
||||
return "expected " + string(err) + " header(s)"
|
||||
}
|
||||
|
||||
// ErrHdrInvalid returned when header(s) are different from the one expected
|
||||
type ErrHdrInvalid string
|
||||
|
||||
func (err ErrHdrInvalid) Error() string {
|
||||
return "invalid " + string(err) + " value"
|
||||
}
|
||||
|
||||
func pcapLinkTypeLength(lType int) (int, bool) {
|
||||
switch layers.LinkType(lType) {
|
||||
case layers.LinkTypeEthernet:
|
||||
return 14, true
|
||||
case layers.LinkTypeNull, layers.LinkTypeLoop:
|
||||
return 4, true
|
||||
case layers.LinkTypeRaw, 12, 14:
|
||||
return 0, true
|
||||
case layers.LinkTypeIPv4, layers.LinkTypeIPv6:
|
||||
// (TODO:) look out for IP encapsulation?
|
||||
return 0, true
|
||||
case layers.LinkTypeLinuxSLL:
|
||||
return 16, true
|
||||
case layers.LinkTypeFDDI:
|
||||
return 13, true
|
||||
case 226 /*DLT_IPNET*/ :
|
||||
// https://www.tcpdump.org/linktypes/LINKTYPE_IPNET.html
|
||||
return 24, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/IPv6_packet#Extension_headers
|
||||
func ipv6ExtensionHdr(b byte) bool {
|
||||
// TODO: support all extension headers
|
||||
return b == 0 || b == 43 || b == 44
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
func generateHeader4(seq uint32, length uint16) []byte {
|
||||
hdr := make([]byte, 4+24+24, 4+24+24)
|
||||
binary.BigEndian.PutUint32(hdr, uint32(layers.ProtocolFamilyIPv4))
|
||||
|
||||
ip := hdr[4:]
|
||||
ip[0] = 4<<4 | 6
|
||||
binary.BigEndian.PutUint16(ip[2:4], length+24+24)
|
||||
ip[9] = uint8(layers.IPProtocolTCP)
|
||||
copy(ip[12:16], []byte{127, 0, 0, 1})
|
||||
copy(ip[16:], []byte{127, 0, 0, 1})
|
||||
|
||||
// set tcp header
|
||||
tcp := ip[24:]
|
||||
tcp[12] = 6 << 4
|
||||
binary.BigEndian.PutUint16(tcp, 5535)
|
||||
binary.BigEndian.PutUint16(tcp[2:], 8000)
|
||||
binary.BigEndian.PutUint32(tcp[4:], seq)
|
||||
return hdr
|
||||
}
|
||||
|
||||
func generateHeader6(seq uint32, length uint16) []byte {
|
||||
hdr := make([]byte, 4+40+32+24, 4+40+32+24)
|
||||
binary.BigEndian.PutUint32(hdr, uint32(layers.ProtocolFamilyIPv6Linux))
|
||||
|
||||
ip := hdr[4:]
|
||||
ip[0] = 6 << 4
|
||||
binary.BigEndian.PutUint16(ip[4:], length+32+24)
|
||||
var ipAddr [16]byte
|
||||
ipAddr[15] = 0x01
|
||||
copy(ip[8:], ip[:])
|
||||
copy(ip[24:], ip[:])
|
||||
copy(ip[40:], []byte{
|
||||
// net-layer (IPv6-Opts)
|
||||
0x2b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
uint8(layers.IPProtocolTCP), 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
})
|
||||
|
||||
// set tcp header
|
||||
tcp := ip[40+32:]
|
||||
tcp[12] = 6 << 4
|
||||
binary.BigEndian.PutUint16(tcp, 5535)
|
||||
binary.BigEndian.PutUint16(tcp[2:], 8000)
|
||||
binary.BigEndian.PutUint32(tcp[4:], seq)
|
||||
return hdr
|
||||
}
|
||||
|
||||
func Packets(start uint32, _len int, length uint16, version byte) []*Packet {
|
||||
var packets = make([]*Packet, _len)
|
||||
for i := start; i < start+uint32(_len); i++ {
|
||||
var h []byte
|
||||
if version == 4 {
|
||||
h = generateHeader4(i, length)
|
||||
} else {
|
||||
h = generateHeader6(i, length)
|
||||
}
|
||||
d := append(h, make([]byte, int(length))...)
|
||||
ci := &gopacket.CaptureInfo{Length: len(d), CaptureLength: len(d), Timestamp: time.Now()}
|
||||
packets[i-start] = NewPacket(d, int(layers.LinkTypeLoop), 4, ci)
|
||||
}
|
||||
return packets
|
||||
}
|
||||
|
||||
func TestIPv4Packet(t *testing.T) {
|
||||
pckt := packet(append(generateHeader4(1024, 10), make([]byte, 10)...))
|
||||
if pckt.Err != nil {
|
||||
t.Error(pckt)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:2]).Err; !errors.Is(err, ErrHdrLength("Link")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("Link"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:20]).Err; !errors.Is(err, ErrHdrLength("IPv4")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv4"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:27]).Err; !errors.Is(err, ErrHdrLength("IPv4 opts")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv4 opts"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:40]).Err; !errors.Is(err, ErrHdrLength("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:50]).Err; !errors.Is(err, ErrHdrLength("TCP opts")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
|
||||
return
|
||||
}
|
||||
pckt.TransLayer[12] = 0x10
|
||||
if err := packet(pckt.Data[:50]).Err; !errors.Is(err, ErrHdrInvalid("TCP's data offset")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("TCP's data offset"), err)
|
||||
return
|
||||
}
|
||||
pckt.TransLayer[12] = 0x60
|
||||
if err := packet(pckt.Data[:28]).Err; !errors.Is(err, ErrHdrMissing("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrMissing("TCP"), err)
|
||||
return
|
||||
}
|
||||
pckt.NetLayer[9] = 0x02
|
||||
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrExpected("TCP"), err)
|
||||
return
|
||||
}
|
||||
pckt.NetLayer[9] = 0x06
|
||||
pckt.NetLayer[0] = 0x44
|
||||
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrInvalid("IPv4's IHL")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("IPv4's IHL"), err)
|
||||
return
|
||||
}
|
||||
pckt.NetLayer[0] = 0x56
|
||||
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("IPv4 or IPv6")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrExpected("IPv4 or IPv6"), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPv6Packet(t *testing.T) {
|
||||
pckt := packet(append(generateHeader6(1024, 10), make([]byte, 10)...))
|
||||
if pckt.Err != nil {
|
||||
t.Error(pckt)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:4]).Err; !errors.Is(err, ErrHdrMissing("IPv4 or IPv6")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrMissing("IPv4 or IPv6"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:40]).Err; !errors.Is(err, ErrHdrLength("IPv6")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv6"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:52]).Err; !errors.Is(err, ErrHdrLength("IPv6 opts")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("IPv6 opts"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:80]).Err; !errors.Is(err, ErrHdrLength("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
|
||||
return
|
||||
}
|
||||
if err := packet(pckt.Data[:98]).Err; !errors.Is(err, ErrHdrLength("TCP opts")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrLength("TCP opts"), err)
|
||||
return
|
||||
}
|
||||
pckt.TransLayer[12] = 0x10
|
||||
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrInvalid("TCP's data offset")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrInvalid("TCP's data offset"), err)
|
||||
return
|
||||
}
|
||||
pckt.TransLayer[12] = 0x60
|
||||
if err := packet(pckt.Data[:76]).Err; !errors.Is(err, ErrHdrMissing("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrMissing("TCP"), err)
|
||||
return
|
||||
}
|
||||
pckt.NetLayer[56] = 0x02
|
||||
if err := packet(pckt.Data).Err; !errors.Is(err, ErrHdrExpected("TCP")) {
|
||||
t.Errorf("should fail with %q, got %q", ErrHdrExpected("TCP"), err)
|
||||
return
|
||||
}
|
||||
pckt.NetLayer[56] = 0x06
|
||||
}
|
||||
|
||||
func packet(data []byte) *Packet {
|
||||
return NewPacket(data, int(layers.LinkTypeLoop), 4, &gopacket.CaptureInfo{})
|
||||
}
|
||||
|
||||
func BenchmarkNewPacketIPv4(b *testing.B) {
|
||||
data := append(generateHeader4(1204, 10), make([]byte, 10)...)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := packet(data).Err; err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewPacketIPv6(b *testing.B) {
|
||||
data := append(generateHeader6(1024, 10), make([]byte, 10)...)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := packet(data).Err; err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
-18
@@ -3,15 +3,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
_ "net/http/pprof"
|
||||
httppptof "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
_ "runtime/debug"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -22,8 +24,42 @@ var (
|
||||
memprofile = flag.String("memprofile", "", "write memory profile to this file")
|
||||
)
|
||||
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
func init() {
|
||||
var defaultServeMux http.ServeMux
|
||||
http.DefaultServeMux = &defaultServeMux
|
||||
|
||||
http.HandleFunc("/debug/vars", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprintf(w, "{\n")
|
||||
first := true
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
if kv.Key == "memstats" || kv.Key == "cmdline" {
|
||||
return
|
||||
}
|
||||
|
||||
if !first {
|
||||
fmt.Fprintf(w, ",\n")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
||||
})
|
||||
fmt.Fprintf(w, "\n}\n")
|
||||
})
|
||||
|
||||
http.HandleFunc("/debug/pprof/", httppptof.Index)
|
||||
http.HandleFunc("/debug/pprof/cmdline", httppptof.Cmdline)
|
||||
http.HandleFunc("/debug/pprof/profile", httppptof.Profile)
|
||||
http.HandleFunc("/debug/pprof/symbol", httppptof.Symbol)
|
||||
http.HandleFunc("/debug/pprof/trace", httppptof.Trace)
|
||||
}
|
||||
|
||||
func loggingMiddleware(addr string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/loop" {
|
||||
_, err := http.Get("http://" + addr)
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
rb, _ := httputil.DumpRequest(r, false)
|
||||
log.Println(string(rb))
|
||||
next.ServeHTTP(w, r)
|
||||
@@ -36,23 +72,23 @@ func main() {
|
||||
}
|
||||
|
||||
args := os.Args[1:]
|
||||
var plugins *InOutPlugins
|
||||
var plugins *goreplay.InOutPlugins
|
||||
if len(args) > 0 && args[0] == "file-server" {
|
||||
if len(args) != 2 {
|
||||
log.Fatal("You should specify port and IP (optional) for the file server. Example: `gor file-server :80`")
|
||||
}
|
||||
dir, _ := os.Getwd()
|
||||
|
||||
Debug(0, "Started example file server for current directory on address ", args[1])
|
||||
goreplay.Debug(0, "Started example file server for current directory on address ", args[1])
|
||||
|
||||
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(http.FileServer(http.Dir(dir)))))
|
||||
log.Fatal(http.ListenAndServe(args[1], loggingMiddleware(args[1], http.FileServer(http.Dir(dir)))))
|
||||
} else {
|
||||
flag.Parse()
|
||||
checkSettings()
|
||||
plugins = NewPlugins()
|
||||
goreplay.CheckSettings()
|
||||
plugins = goreplay.NewPlugins()
|
||||
}
|
||||
|
||||
log.Printf("[PPID %d and PID %d] Version:%s\n", os.Getppid(), os.Getpid(), VERSION)
|
||||
log.Printf("[PPID %d and PID %d] Version:%s\n", os.Getppid(), os.Getpid(), goreplay.VERSION)
|
||||
|
||||
if len(plugins.Inputs) == 0 || len(plugins.Outputs) == 0 {
|
||||
log.Fatal("Required at least 1 input and 1 output")
|
||||
@@ -66,20 +102,20 @@ func main() {
|
||||
profileCPU(*cpuprofile)
|
||||
}
|
||||
|
||||
if Settings.Pprof != "" {
|
||||
if goreplay.Settings.Pprof != "" {
|
||||
go func() {
|
||||
log.Println(http.ListenAndServe(Settings.Pprof, nil))
|
||||
log.Println(http.ListenAndServe(goreplay.Settings.Pprof, nil))
|
||||
}()
|
||||
}
|
||||
|
||||
closeCh := make(chan int)
|
||||
emitter := NewEmitter()
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
if Settings.ExitAfter > 0 {
|
||||
log.Printf("Running gor for a duration of %s\n", Settings.ExitAfter)
|
||||
emitter := goreplay.NewEmitter()
|
||||
go emitter.Start(plugins, goreplay.Settings.Middleware)
|
||||
if goreplay.Settings.ExitAfter > 0 {
|
||||
log.Printf("Running gor for a duration of %s\n", goreplay.Settings.ExitAfter)
|
||||
|
||||
time.AfterFunc(Settings.ExitAfter, func() {
|
||||
log.Printf("gor run timeout %s\n", Settings.ExitAfter)
|
||||
time.AfterFunc(goreplay.Settings.ExitAfter, func() {
|
||||
log.Printf("gor run timeout %s\n", goreplay.Settings.ExitAfter)
|
||||
close(closeCh)
|
||||
})
|
||||
}
|
||||
@@ -94,7 +130,6 @@ func main() {
|
||||
}
|
||||
emitter.Close()
|
||||
os.Exit(exit)
|
||||
|
||||
}
|
||||
|
||||
func profileCPU(cpuprofile string) {
|
||||
@@ -37,6 +37,11 @@ sudo gor --input-raw :80 --input-raw-engine "raw_socket" --output-http "http://s
|
||||
|
||||
You can read more about [[Replaying HTTP traffic]].
|
||||
|
||||
You can use VXLAN or traffic mirroring from AWS to capture the traffic. The 4789 UDP port will be opened and that works as you are launched GoReplay on the source machine.
|
||||
|
||||
```
|
||||
gor --input-raw :80 --input-raw-engine vxlan -output-stdout
|
||||
```
|
||||
|
||||
### Tracking original IP addresses
|
||||
You can use `--input-raw-realip-header` option to specify header name: If not blank, injects header with given name and real IP value to the request payload. Usually, this header should be named: `X-Real-IP`, but you can specify any name.
|
||||
|
||||
+3
-4
@@ -1,14 +1,13 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
|
||||
elastigo "github.com/mattbaird/elastigo/lib"
|
||||
)
|
||||
|
||||
@@ -57,7 +56,7 @@ type ESRequestResponse struct {
|
||||
//
|
||||
// Proper format is: scheme://[userinfo@]host/index_name
|
||||
// userinfo is: user[:password]
|
||||
// net/url.Parse() does not fail if scheme is not provided but actualy does not
|
||||
// net/url.Parse() does not fail if scheme is not provided but actually does not
|
||||
// handle URI properly.
|
||||
// So we must 'validate' URI format to match requirements to use net/url.Parse()
|
||||
func parseURI(URI string) (err error, index string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
+10
-28
@@ -1,14 +1,14 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/byteutils"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/byteutils"
|
||||
"github.com/coocood/freecache"
|
||||
)
|
||||
|
||||
// Emitter represents an abject to manage plugins communication
|
||||
@@ -76,9 +76,7 @@ func (e *Emitter) Close() {
|
||||
func CopyMulty(src PluginReader, writers ...PluginWriter) error {
|
||||
wIndex := 0
|
||||
modifier := NewHTTPModifier(&Settings.ModifierConfig)
|
||||
filteredRequests := make(map[string]int64)
|
||||
filteredRequestsLastCleanTime := time.Now().UnixNano()
|
||||
filteredCount := 0
|
||||
filteredRequests := freecache.NewCache(200 * 1024 * 1024) // 200M
|
||||
|
||||
for {
|
||||
msg, err := src.PluginRead()
|
||||
@@ -97,7 +95,7 @@ func CopyMulty(src PluginReader, writers ...PluginWriter) error {
|
||||
Debug(2, fmt.Sprintf("[EMITTER] Found malformed record %q from %q", msg.Meta, src))
|
||||
continue
|
||||
}
|
||||
requestID := byteutils.SliceToString(meta[1])
|
||||
requestID := meta[1]
|
||||
// start a subroutine only when necessary
|
||||
if Settings.Verbose >= 3 {
|
||||
Debug(3, "[EMITTER] input: ", byteutils.SliceToString(msg.Meta[:len(msg.Meta)-1]), " from: ", src)
|
||||
@@ -108,16 +106,15 @@ func CopyMulty(src PluginReader, writers ...PluginWriter) error {
|
||||
msg.Data = modifier.Rewrite(msg.Data)
|
||||
// If modifier tells to skip request
|
||||
if len(msg.Data) == 0 {
|
||||
filteredRequests[requestID] = time.Now().UnixNano()
|
||||
filteredCount++
|
||||
filteredRequests.Set(requestID, []byte{}, 60) //
|
||||
continue
|
||||
}
|
||||
Debug(3, "[EMITTER] Rewritten input:", requestID, "from:", src)
|
||||
|
||||
} else {
|
||||
if _, ok := filteredRequests[requestID]; ok {
|
||||
delete(filteredRequests, requestID)
|
||||
filteredCount--
|
||||
_, err := filteredRequests.Get(requestID)
|
||||
if err == nil {
|
||||
filteredRequests.Del(requestID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -152,26 +149,11 @@ func CopyMulty(src PluginReader, writers ...PluginWriter) error {
|
||||
}
|
||||
} else {
|
||||
for _, dst := range writers {
|
||||
if _, err := dst.PluginWrite(msg); err != nil {
|
||||
if _, err := dst.PluginWrite(msg); err != nil && err != io.ErrClosedPipe {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run GC on each 1000 request
|
||||
if filteredCount > 0 && filteredCount%1000 == 0 {
|
||||
// Clean up filtered requests for which we didn't get a response to filter
|
||||
now := time.Now().UnixNano()
|
||||
if now-filteredRequestsLastCleanTime > int64(60*time.Second) {
|
||||
for k, v := range filteredRequests {
|
||||
if now-v > int64(60*time.Second) {
|
||||
delete(filteredRequests, k)
|
||||
filteredCount--
|
||||
}
|
||||
}
|
||||
filteredRequestsLastCleanTime = time.Now().UnixNano()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
|
||||
function log {
|
||||
if [[ ! -v GOR_TEST ]]; then # if we are not testing
|
||||
if [[ -n "$GOR_TEST" ]]; then # if we are not testing
|
||||
# Logging to stderr, because stdout/stdin used for data transfer
|
||||
>&2 echo "[DEBUG][ECHO] $1"
|
||||
fi
|
||||
|
||||
@@ -24,9 +24,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
"os"
|
||||
)
|
||||
|
||||
// requestID -> originalToken
|
||||
|
||||
@@ -1,19 +1,80 @@
|
||||
module github.com/buger/goreplay
|
||||
|
||||
go 1.14
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/Shopify/sarama v1.26.4
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 // indirect
|
||||
github.com/aws/aws-sdk-go v1.33.2
|
||||
github.com/Shopify/sarama v1.38.1
|
||||
github.com/aws/aws-sdk-go v1.44.262
|
||||
github.com/coocood/freecache v1.2.3
|
||||
github.com/google/gopacket v1.1.20-0.20210429153827-3eaba0894325
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/klauspost/compress v1.16.5 // indirect
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/xdg-go/scram v1.1.2
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
k8s.io/apimachinery v0.27.1
|
||||
k8s.io/client-go v0.27.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/araddon/gou v0.0.0-20211019181548-e7d08105776c // indirect
|
||||
github.com/bitly/go-hostpool v0.1.0 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/google/gopacket v1.1.18
|
||||
github.com/klauspost/compress v1.10.10 // indirect
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/eapache/go-resiliency v1.3.0 // indirect
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
|
||||
github.com/go-logr/logr v1.2.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
|
||||
github.com/jcmturner/gofork v1.7.6 // indirect
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.17 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.27.1 // indirect
|
||||
k8s.io/klog/v2 v2.100.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,117 +1,373 @@
|
||||
github.com/Shopify/sarama v1.26.4 h1:+17TxUq/PJEAfZAll0T7XJjSgQWCpaQSoki/x5yN8o8=
|
||||
github.com/Shopify/sarama v1.26.4/go.mod h1:NbSGBSSndYaIhRcBtY9V0U7AyH+x71bG668AuWys/yU=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61 h1:Xz25cuW4REGC5W5UtpMU3QItMIImag615HiQcRbxqKQ=
|
||||
github.com/araddon/gou v0.0.0-20190110011759-c797efecbb61/go.mod h1:ikc1XA58M+Rx7SEbf0bLJCfBkwayZ8T5jBo5FXK8Uz8=
|
||||
github.com/aws/aws-sdk-go v1.33.2 h1:8TVrnPnSD7I+AmDp66xBUvS3K0J+jH09YXdrkJ34ey0=
|
||||
github.com/aws/aws-sdk-go v1.33.2/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/sarama v1.38.1 h1:lqqPUPQZ7zPqYlWpTh+LQ9bhYNu2xJL6k1SJN4WVe2A=
|
||||
github.com/Shopify/sarama v1.38.1/go.mod h1:iwv9a67Ha8VNa+TifujYoWGxWnu2kNVAQdSdZ4X2o5g=
|
||||
github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/araddon/gou v0.0.0-20211019181548-e7d08105776c h1:XUqw//RExYoxW4Eie8MuKp8sEDAZI1gMHX/daUFgZww=
|
||||
github.com/araddon/gou v0.0.0-20211019181548-e7d08105776c/go.mod h1:ikc1XA58M+Rx7SEbf0bLJCfBkwayZ8T5jBo5FXK8Uz8=
|
||||
github.com/aws/aws-sdk-go v1.44.262 h1:gyXpcJptWoNkK+DiAiaBltlreoWKQXjAIh6FRh60F+I=
|
||||
github.com/aws/aws-sdk-go v1.44.262/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
|
||||
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coocood/freecache v1.2.3 h1:lcBwpZrwBZRZyLk/8EMyQVXRiFl663cCuMOrjCALeto=
|
||||
github.com/coocood/freecache v1.2.3/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q=
|
||||
github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0=
|
||||
github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE=
|
||||
github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=
|
||||
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
|
||||
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY=
|
||||
github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.20-0.20210429153827-3eaba0894325 h1:YmIcZ5Var3BAQ64AW98Iiys5Ih4fiU0xK41+8isC5Ec=
|
||||
github.com/google/gopacket v1.1.20-0.20210429153827-3eaba0894325/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA=
|
||||
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
|
||||
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b h1:v29yPGHhOqw7VHEnTeQFAth3SsBrmwc8JfuhNY0G34k=
|
||||
github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b/go.mod h1:5MWrJXKRQyhQdUCF+vu6U5c4nQpg70vW3eHaU0/AYbU=
|
||||
github.com/pierrec/lz4 v2.4.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk=
|
||||
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
|
||||
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
|
||||
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
|
||||
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72 h1:+ELyKg6m8UBf0nPFSqD0mi7zUfwPyXo23HNjMnXPz7w=
|
||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0 h1:a9tsXlIDD9SKxotJMK3niV7rPZAJeX2aD/0yg3qlIrg=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0=
|
||||
k8s.io/api v0.27.1/go.mod h1:z5g/BpAiD+f6AArpqNjkY+cji8ueZDU/WV1jcj5Jk4E=
|
||||
k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc=
|
||||
k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM=
|
||||
k8s.io/client-go v0.27.1 h1:oXsfhW/qncM1wDmWBIuDzRHNS2tLhK3BZv512Nc59W8=
|
||||
k8s.io/client-go v0.27.1/go.mod h1:f8LHMUkVb3b9N8bWturc+EDtVVVwZ7ueTVquFAJb2vA=
|
||||
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
|
||||
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
|
||||
k8s.io/utils v0.0.0-20230505201702-9f6742963106 h1:EObNQ3TW2D+WptiYXlApGNLVy0zm/JIBVY9i+M4wpAU=
|
||||
k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
+2
-3
@@ -1,12 +1,11 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
type HTTPModifier struct {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -24,9 +24,7 @@ type HTTPModifierConfig struct {
|
||||
Methods HTTPMethods `json:"http-allow-method"`
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header, --http-disallow-header options
|
||||
//
|
||||
type headerFilter struct {
|
||||
name []byte
|
||||
regexp *regexp.Regexp
|
||||
@@ -56,9 +54,7 @@ func (h *HTTPHeaderFilters) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-basic-auth-filter option
|
||||
//
|
||||
type basicAuthFilter struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
@@ -82,9 +78,7 @@ func (h *HTTPHeaderBasicAuthFilters) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-header-hash and --http-allow-param-hash options
|
||||
//
|
||||
type hashFilter struct {
|
||||
name []byte
|
||||
percent uint32
|
||||
@@ -129,9 +123,7 @@ func (h *HTTPHashFilters) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-header option
|
||||
//
|
||||
type httpHeader struct {
|
||||
Name string
|
||||
Value string
|
||||
@@ -160,9 +152,7 @@ func (h *HTTPHeaders) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-set-param option
|
||||
//
|
||||
type httpParam struct {
|
||||
Name []byte
|
||||
Value []byte
|
||||
@@ -208,9 +198,7 @@ func (h *HTTPMethods) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-rewrite-url option
|
||||
//
|
||||
type urlRewrite struct {
|
||||
src *regexp.Regexp
|
||||
target []byte
|
||||
@@ -237,9 +225,7 @@ func (r *URLRewriteMap) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-rewrite-header option
|
||||
//
|
||||
type headerRewrite struct {
|
||||
header []byte
|
||||
src *regexp.Regexp
|
||||
@@ -275,9 +261,7 @@ func (r *HeaderRewriteMap) Set(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Handling of --http-allow-url option
|
||||
//
|
||||
type urlRegexp struct {
|
||||
regexp *regexp.Regexp
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHTTPModifierWithoutConfig(t *testing.T) {
|
||||
|
||||
+2
-3
@@ -1,14 +1,13 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"io/ioutil"
|
||||
"net/http/httputil"
|
||||
"strconv"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func prettifyHTTP(p []byte) []byte {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
func TestHTTPPrettifierGzip(t *testing.T) {
|
||||
|
||||
+7
-10
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"time"
|
||||
@@ -36,16 +36,13 @@ func (i *DummyInput) PluginRead() (*Message, error) {
|
||||
func (i *DummyInput) emit() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
uuid := uuid()
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano(), -1)
|
||||
i.data <- append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
|
||||
for range ticker.C {
|
||||
uuid := uuid()
|
||||
reqh := payloadHeader(RequestPayload, uuid, time.Now().UnixNano(), -1)
|
||||
i.data <- append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
|
||||
|
||||
resh := payloadHeader(ResponsePayload, uuid, time.Now().UnixNano()+1, 1)
|
||||
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
}
|
||||
resh := payloadHeader(ResponsePayload, uuid, time.Now().UnixNano()+1, 1)
|
||||
i.data <- append(resh, []byte("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")...)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+200
-36
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"container/heap"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -20,27 +23,73 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
type fileInputReader struct {
|
||||
reader *bufio.Reader
|
||||
type filePayload struct {
|
||||
data []byte
|
||||
file io.ReadCloser
|
||||
timestamp int64
|
||||
closed int32 // Value of 0 indicates that the file is still open.
|
||||
s3 bool
|
||||
}
|
||||
|
||||
func (f *fileInputReader) parseNext() error {
|
||||
// An IntHeap is a min-heap of ints.
|
||||
type payloadQueue struct {
|
||||
sync.RWMutex
|
||||
s []*filePayload
|
||||
}
|
||||
|
||||
func (h payloadQueue) Len() int { return len(h.s) }
|
||||
func (h payloadQueue) Less(i, j int) bool { return h.s[i].timestamp < h.s[j].timestamp }
|
||||
func (h payloadQueue) Swap(i, j int) { h.s[i], h.s[j] = h.s[j], h.s[i] }
|
||||
|
||||
func (h *payloadQueue) Push(x interface{}) {
|
||||
// Push and Pop use pointer receivers because they modify the slice's length,
|
||||
// not just its contents.
|
||||
h.s = append(h.s, x.(*filePayload))
|
||||
}
|
||||
|
||||
func (h *payloadQueue) Pop() interface{} {
|
||||
old := h.s
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
h.s = old[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (h payloadQueue) Idx(i int) *filePayload {
|
||||
return h.s[i]
|
||||
}
|
||||
|
||||
type fileInputReader struct {
|
||||
reader *bufio.Reader
|
||||
file io.ReadCloser
|
||||
closed int32 // Value of 0 indicates that the file is still open.
|
||||
s3 bool
|
||||
queue payloadQueue
|
||||
readDepth int
|
||||
dryRun bool
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *fileInputReader) parse(init chan struct{}) error {
|
||||
payloadSeparatorAsBytes := []byte(payloadSeparator)
|
||||
var buffer bytes.Buffer
|
||||
var initialized bool
|
||||
|
||||
lineNum := 0
|
||||
|
||||
for {
|
||||
line, err := f.reader.ReadBytes('\n')
|
||||
lineNum++
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
Debug(1, err)
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
f.Close()
|
||||
|
||||
if !initialized {
|
||||
close(init)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,21 +97,61 @@ func (f *fileInputReader) parseNext() error {
|
||||
asBytes := buffer.Bytes()
|
||||
meta := payloadMeta(asBytes)
|
||||
|
||||
f.timestamp, _ = strconv.ParseInt(string(meta[2]), 10, 64)
|
||||
f.data = asBytes[:len(asBytes)-1]
|
||||
if len(meta) < 3 {
|
||||
Debug(1, fmt.Sprintf("Found malformed record, file: %s, line %d", f.path, lineNum))
|
||||
buffer = bytes.Buffer{}
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
timestamp, _ := strconv.ParseInt(string(meta[2]), 10, 64)
|
||||
data := asBytes[:len(asBytes)-1]
|
||||
|
||||
f.queue.Lock()
|
||||
heap.Push(&f.queue, &filePayload{
|
||||
timestamp: timestamp,
|
||||
data: data,
|
||||
})
|
||||
f.queue.Unlock()
|
||||
|
||||
for {
|
||||
if f.queue.Len() < f.readDepth {
|
||||
break
|
||||
}
|
||||
|
||||
if !initialized {
|
||||
close(init)
|
||||
initialized = true
|
||||
}
|
||||
|
||||
if !f.dryRun {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
buffer = bytes.Buffer{}
|
||||
continue
|
||||
}
|
||||
|
||||
buffer.Write(line)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (f *fileInputReader) ReadPayload() []byte {
|
||||
defer f.parseNext()
|
||||
func (f *fileInputReader) wait() {
|
||||
for {
|
||||
if atomic.LoadInt32(&f.closed) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
return f.data
|
||||
if f.queue.Len() > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !f.dryRun {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes this plugin
|
||||
@@ -75,7 +164,7 @@ func (f *fileInputReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFileInputReader(path string) *fileInputReader {
|
||||
func newFileInputReader(path string, readDepth int, dryRun bool) *fileInputReader {
|
||||
var file io.ReadCloser
|
||||
var err error
|
||||
|
||||
@@ -90,7 +179,7 @@ func newFileInputReader(path string) *fileInputReader {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := &fileInputReader{file: file, closed: 0}
|
||||
r := &fileInputReader{path: path, file: file, closed: 0, readDepth: readDepth, dryRun: dryRun}
|
||||
if strings.HasSuffix(path, ".gz") {
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
@@ -102,7 +191,11 @@ func newFileInputReader(path string) *fileInputReader {
|
||||
r.reader = bufio.NewReader(file)
|
||||
}
|
||||
|
||||
r.parseNext()
|
||||
heap.Init(&r.queue)
|
||||
|
||||
init := make(chan struct{})
|
||||
go r.parse(init)
|
||||
<-init
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -116,16 +209,25 @@ type FileInput struct {
|
||||
readers []*fileInputReader
|
||||
speedFactor float64
|
||||
loop bool
|
||||
readDepth int
|
||||
dryRun bool
|
||||
maxWait time.Duration
|
||||
|
||||
stats *expvar.Map
|
||||
}
|
||||
|
||||
// NewFileInput constructor for FileInput. Accepts file path as argument.
|
||||
func NewFileInput(path string, loop bool) (i *FileInput) {
|
||||
func NewFileInput(path string, loop bool, readDepth int, maxWait time.Duration, dryRun bool) (i *FileInput) {
|
||||
i = new(FileInput)
|
||||
i.data = make(chan []byte, 1000)
|
||||
i.exit = make(chan bool)
|
||||
i.path = path
|
||||
i.speedFactor = 1
|
||||
i.loop = loop
|
||||
i.readDepth = readDepth
|
||||
i.stats = expvar.NewMap("file-" + path)
|
||||
i.dryRun = dryRun
|
||||
i.maxWait = maxWait
|
||||
|
||||
if err := i.init(); err != nil {
|
||||
return
|
||||
@@ -155,30 +257,31 @@ func (i *FileInput) init() (err error) {
|
||||
|
||||
resp, err := svc.ListObjects(params)
|
||||
if err != nil {
|
||||
Debug(0, "[INPUT-FILE] Error while retreiving list of files from S3", i.path, err)
|
||||
Debug(2, "[INPUT-FILE] Error while retrieving list of files from S3", i.path, err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, c := range resp.Contents {
|
||||
matches = append(matches, "s3://"+bucket+"/"+(*c.Key))
|
||||
}
|
||||
} else if matches, err = filepath.Glob(i.path); err != nil{
|
||||
Debug(0, "[INPUT-FILE] Wrong file pattern", i.path, err)
|
||||
return
|
||||
|
||||
} else if matches, err = filepath.Glob(i.path); err != nil {
|
||||
Debug(2, "[INPUT-FILE] Wrong file pattern", i.path, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
Debug(0, "[INPUT-FILE] No files match pattern: ", i.path)
|
||||
return errors.New("No matching files")
|
||||
Debug(2, "[INPUT-FILE] No files match pattern: ", i.path)
|
||||
return errors.New("no matching files")
|
||||
}
|
||||
|
||||
i.readers = make([]*fileInputReader, len(matches))
|
||||
|
||||
for idx, p := range matches {
|
||||
i.readers[idx] = newFileInputReader(p)
|
||||
i.readers[idx] = newFileInputReader(p, i.readDepth, i.dryRun)
|
||||
}
|
||||
|
||||
i.stats.Add("reader_count", int64(len(matches)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -189,6 +292,7 @@ func (i *FileInput) PluginRead() (*Message, error) {
|
||||
case <-i.exit:
|
||||
return nil, ErrorStopped
|
||||
case buf := <-i.data:
|
||||
i.stats.Add("read_from", 1)
|
||||
msg.Meta, msg.Data = payloadMetaWithBody(buf)
|
||||
return &msg, nil
|
||||
}
|
||||
@@ -201,11 +305,17 @@ func (i *FileInput) String() string {
|
||||
// Find reader with smallest timestamp e.g next payload in row
|
||||
func (i *FileInput) nextReader() (next *fileInputReader) {
|
||||
for _, r := range i.readers {
|
||||
if r == nil || atomic.LoadInt32(&r.closed) != 0 {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if next == nil || r.timestamp < next.timestamp {
|
||||
r.wait()
|
||||
|
||||
if r.queue.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if next == nil || r.queue.Idx(0).timestamp < next.queue.Idx(0).timestamp {
|
||||
next = r
|
||||
continue
|
||||
}
|
||||
@@ -217,6 +327,11 @@ func (i *FileInput) nextReader() (next *fileInputReader) {
|
||||
func (i *FileInput) emit() {
|
||||
var lastTime int64 = -1
|
||||
|
||||
var maxWait, firstWait, minWait int64
|
||||
minWait = math.MaxInt64
|
||||
|
||||
i.stats.Add("negative_wait", 0)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.exit:
|
||||
@@ -236,17 +351,48 @@ func (i *FileInput) emit() {
|
||||
}
|
||||
}
|
||||
|
||||
reader.queue.RLock()
|
||||
payload := heap.Pop(&reader.queue).(*filePayload)
|
||||
i.stats.Add("total_counter", 1)
|
||||
i.stats.Add("total_bytes", int64(len(payload.data)))
|
||||
reader.queue.RUnlock()
|
||||
|
||||
if lastTime != -1 {
|
||||
diff := reader.timestamp - lastTime
|
||||
lastTime = reader.timestamp
|
||||
diff := payload.timestamp - lastTime
|
||||
|
||||
if firstWait == 0 {
|
||||
firstWait = diff
|
||||
}
|
||||
|
||||
if i.speedFactor != 1 {
|
||||
diff = int64(float64(diff) / i.speedFactor)
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(diff))
|
||||
if i.maxWait > 0 && diff > int64(i.maxWait) {
|
||||
diff = int64(i.maxWait)
|
||||
}
|
||||
|
||||
if diff >= 0 {
|
||||
lastTime = payload.timestamp
|
||||
|
||||
if !i.dryRun {
|
||||
time.Sleep(time.Duration(diff))
|
||||
}
|
||||
|
||||
i.stats.Add("total_wait", diff)
|
||||
|
||||
if diff > maxWait {
|
||||
maxWait = diff
|
||||
}
|
||||
|
||||
if diff < minWait {
|
||||
minWait = diff
|
||||
}
|
||||
} else {
|
||||
i.stats.Add("negative_wait", 1)
|
||||
}
|
||||
} else {
|
||||
lastTime = reader.timestamp
|
||||
lastTime = payload.timestamp
|
||||
}
|
||||
|
||||
// Recheck if we have exited since last check.
|
||||
@@ -254,12 +400,30 @@ func (i *FileInput) emit() {
|
||||
case <-i.exit:
|
||||
return
|
||||
default:
|
||||
i.data <- reader.ReadPayload()
|
||||
if !i.dryRun {
|
||||
i.data <- payload.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug(0, fmt.Sprintf("[INPUT-FILE] FileInput: end of file '%s'\n", i.path))
|
||||
i.stats.Set("first_wait", time.Duration(firstWait))
|
||||
i.stats.Set("max_wait", time.Duration(maxWait))
|
||||
i.stats.Set("min_wait", time.Duration(minWait))
|
||||
|
||||
Debug(2, fmt.Sprintf("[INPUT-FILE] FileInput: end of file '%s'\n", i.path))
|
||||
|
||||
if i.dryRun {
|
||||
fmt.Printf("Records found: %v\nFiles processed: %v\nBytes processed: %v\nMax wait: %v\nMin wait: %v\nFirst wait: %v\nIt will take `%v` to replay at current speed.\nFound %v records with out of order timestamp\n",
|
||||
i.stats.Get("total_counter"),
|
||||
i.stats.Get("reader_count"),
|
||||
i.stats.Get("total_bytes"),
|
||||
i.stats.Get("max_wait"),
|
||||
i.stats.Get("min_wait"),
|
||||
i.stats.Get("first_wait"),
|
||||
time.Duration(i.stats.Get("total_wait").(*expvar.Int).Value()),
|
||||
i.stats.Get("negative_wait"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes this plugin
|
||||
|
||||
+16
-16
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -29,9 +29,9 @@ func TestInputFileWithGET(t *testing.T) {
|
||||
// The read request should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ func TestInputFileWithPayloadLargerThan64Kb(t *testing.T) {
|
||||
// The read request should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -80,9 +80,9 @@ func TestInputFileWithGETAndPOST(t *testing.T) {
|
||||
// The read requests should match the original request
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads){
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
} else if !expectedCaptureFile.PayloadsEqual(readPayloads) {
|
||||
t.Error("Request read back from file should match")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func TestInputFileMultipleFilesWithRequestsOnly(t *testing.T) {
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false, 100, 0, false)
|
||||
|
||||
for i := '1'; i <= '4'; i++ {
|
||||
msg, _ := input.PluginRead()
|
||||
@@ -130,7 +130,7 @@ func TestInputFileRequestsWithLatency(t *testing.T) {
|
||||
file.Write([]byte("1 3 250000000\nrequest3"))
|
||||
file.Write([]byte(payloadSeparator))
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), false)
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), false, 100, 0, false)
|
||||
|
||||
start := time.Now().UnixNano()
|
||||
for i := 0; i < 3; i++ {
|
||||
@@ -170,7 +170,7 @@ func TestInputFileMultipleFilesWithRequestsAndResponses(t *testing.T) {
|
||||
file2.Write([]byte(payloadSeparator))
|
||||
file2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false, 100, 0, false)
|
||||
|
||||
for i := '1'; i <= '4'; i++ {
|
||||
msg, _ := input.PluginRead()
|
||||
@@ -198,7 +198,7 @@ func TestInputFileLoop(t *testing.T) {
|
||||
file.Write([]byte(payloadSeparator))
|
||||
file.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), true)
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d", rnd), true, 100, 0, false)
|
||||
|
||||
// Even if we have just 2 requests in file, it should indifinitly loop
|
||||
for i := 0; i < 1000; i++ {
|
||||
@@ -226,7 +226,7 @@ func TestInputFileCompressed(t *testing.T) {
|
||||
name2 := output2.file.Name()
|
||||
output2.Close()
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false)
|
||||
input := NewFileInput(fmt.Sprintf("/tmp/%d*", rnd), false, 100, 0, false)
|
||||
for i := 0; i < 2000; i++ {
|
||||
input.PluginRead()
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func CreateCaptureFile(requestGenerator *RequestGenerator) *CaptureFile {
|
||||
func ReadFromCaptureFile(captureFile *os.File, count int, callback writeCallback) (err error) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
input := NewFileInput(captureFile.Name(), false)
|
||||
input := NewFileInput(captureFile.Name(), false, 100, 0, false)
|
||||
output := NewTestOutput(func(msg *Message) {
|
||||
callback(msg)
|
||||
wg.Done()
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/Shopify/sarama/mocks"
|
||||
)
|
||||
|
||||
// KafkaInput is used for recieving Kafka messages and
|
||||
// KafkaInput is used for receiving Kafka messages and
|
||||
// transforming them into HTTP payloads.
|
||||
type KafkaInput struct {
|
||||
config *InputKafkaConfig
|
||||
@@ -19,8 +19,8 @@ type KafkaInput struct {
|
||||
}
|
||||
|
||||
// NewKafkaInput creates instance of kafka consumer client with TLS config
|
||||
func NewKafkaInput(address string, config *InputKafkaConfig, tlsConfig *KafkaTLSConfig) *KafkaInput {
|
||||
c := NewKafkaConfig(tlsConfig)
|
||||
func NewKafkaInput(_ string, config *InputKafkaConfig, tlsConfig *KafkaTLSConfig) *KafkaInput {
|
||||
c := NewKafkaConfig(&config.SASLConfig, tlsConfig)
|
||||
|
||||
var con sarama.Consumer
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
+56
-100
@@ -1,102 +1,78 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/capture"
|
||||
"github.com/buger/goreplay/internal/tcp"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/capture"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"github.com/buger/goreplay/size"
|
||||
"github.com/buger/goreplay/tcp"
|
||||
)
|
||||
|
||||
// TCPProtocol is a number to indicate type of protocol
|
||||
type TCPProtocol uint8
|
||||
|
||||
const (
|
||||
// ProtocolHTTP ...
|
||||
ProtocolHTTP TCPProtocol = iota
|
||||
// ProtocolBinary ...
|
||||
ProtocolBinary
|
||||
)
|
||||
|
||||
// Set is here so that TCPProtocol can implement flag.Var
|
||||
func (protocol *TCPProtocol) Set(v string) error {
|
||||
switch v {
|
||||
case "", "http":
|
||||
*protocol = ProtocolHTTP
|
||||
case "binary":
|
||||
*protocol = ProtocolBinary
|
||||
default:
|
||||
return fmt.Errorf("unsupported protocol %s", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (protocol *TCPProtocol) String() string {
|
||||
switch *protocol {
|
||||
case ProtocolBinary:
|
||||
return "binary"
|
||||
case ProtocolHTTP:
|
||||
return "http"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// RAWInputConfig represents configuration that can be applied on raw input
|
||||
type RAWInputConfig struct {
|
||||
capture.PcapOptions
|
||||
Expire time.Duration `json:"input-raw-expire"`
|
||||
CopyBufferSize size.Size `json:"copy-buffer-size"`
|
||||
Engine capture.EngineType `json:"input-raw-engine"`
|
||||
TrackResponse bool `json:"input-raw-track-response"`
|
||||
Protocol TCPProtocol `json:"input-raw-protocol"`
|
||||
RealIPHeader string `json:"input-raw-realip-header"`
|
||||
Stats bool `json:"input-raw-stats"`
|
||||
quit chan bool // Channel used only to indicate goroutine should shutdown
|
||||
host string
|
||||
port uint16
|
||||
}
|
||||
type RAWInputConfig = capture.PcapOptions
|
||||
|
||||
// RAWInput used for intercepting traffic for given address
|
||||
type RAWInput struct {
|
||||
sync.Mutex
|
||||
RAWInputConfig
|
||||
config RAWInputConfig
|
||||
messageStats []tcp.Stats
|
||||
listener *capture.Listener
|
||||
message chan *tcp.Message
|
||||
messageParser *tcp.MessageParser
|
||||
cancelListener context.CancelFunc
|
||||
closed bool
|
||||
|
||||
quit chan bool // Channel used only to indicate goroutine should shutdown
|
||||
host string
|
||||
ports []uint16
|
||||
}
|
||||
|
||||
// NewRAWInput constructor for RAWInput. Accepts raw input config as arguments.
|
||||
func NewRAWInput(address string, config RAWInputConfig) (i *RAWInput) {
|
||||
i = new(RAWInput)
|
||||
i.RAWInputConfig = config
|
||||
i.message = make(chan *tcp.Message, 10000)
|
||||
i.config = config
|
||||
i.quit = make(chan bool)
|
||||
var host, _port string
|
||||
var err error
|
||||
var port int
|
||||
host, _port, err = net.SplitHostPort(address)
|
||||
|
||||
host, _ports, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
log.Fatalf("input-raw: error while parsing address: %s", err)
|
||||
}
|
||||
if _port != "" {
|
||||
port, err = strconv.Atoi(_port)
|
||||
// If we are reading pcap file, no port needed
|
||||
if strings.HasSuffix(address, "pcap") {
|
||||
host = address
|
||||
_ports = "0"
|
||||
err = nil
|
||||
} else if strings.HasPrefix(address, "k8s://") {
|
||||
portIndex := strings.LastIndex(address, ":")
|
||||
host = address[:portIndex]
|
||||
_ports = address[portIndex+1:]
|
||||
} else {
|
||||
log.Fatalf("input-raw: error while parsing address: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("parsing port error: %v", err)
|
||||
if strings.HasSuffix(host, "pcap") {
|
||||
i.config.Engine = capture.EnginePcapFile
|
||||
}
|
||||
|
||||
var ports []uint16
|
||||
if _ports != "" {
|
||||
portsStr := strings.Split(_ports, ",")
|
||||
|
||||
for _, portStr := range portsStr {
|
||||
port, err := strconv.Atoi(strings.TrimSpace(portStr))
|
||||
if err != nil {
|
||||
log.Fatalf("parsing port error: %v", err)
|
||||
}
|
||||
ports = append(ports, uint16(port))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
i.host = host
|
||||
i.port = uint16(port)
|
||||
i.ports = ports
|
||||
|
||||
i.listen(address)
|
||||
|
||||
@@ -110,14 +86,15 @@ func (i *RAWInput) PluginRead() (*Message, error) {
|
||||
select {
|
||||
case <-i.quit:
|
||||
return nil, ErrorStopped
|
||||
case msgTCP = <-i.message:
|
||||
case msgTCP = <-i.listener.Messages():
|
||||
msg.Data = msgTCP.Data()
|
||||
}
|
||||
|
||||
var msgType byte = ResponsePayload
|
||||
if msgTCP.IsIncoming {
|
||||
if msgTCP.Direction == tcp.DirIncoming {
|
||||
msgType = RequestPayload
|
||||
if i.RealIPHeader != "" {
|
||||
msg.Data = proto.SetHeader(msg.Data, []byte(i.RealIPHeader), []byte(msgTCP.SrcAddr))
|
||||
if i.config.RealIPHeader != "" {
|
||||
msg.Data = proto.SetHeader(msg.Data, []byte(i.config.RealIPHeader), []byte(msgTCP.SrcAddr))
|
||||
}
|
||||
}
|
||||
msg.Meta = payloadHeader(msgType, msgTCP.UUID(), msgTCP.Start.UnixNano(), msgTCP.End.UnixNano()-msgTCP.Start.UnixNano())
|
||||
@@ -130,7 +107,7 @@ func (i *RAWInput) PluginRead() (*Message, error) {
|
||||
if msgTCP.TimedOut {
|
||||
Debug(2, "[INPUT-RAW] message timeout reached, increase input-raw-expire")
|
||||
}
|
||||
if i.Stats {
|
||||
if i.config.Stats {
|
||||
stat := msgTCP.Stats
|
||||
go i.addStats(stat)
|
||||
}
|
||||
@@ -140,24 +117,19 @@ func (i *RAWInput) PluginRead() (*Message, error) {
|
||||
|
||||
func (i *RAWInput) listen(address string) {
|
||||
var err error
|
||||
i.listener, err = capture.NewListener(i.host, i.port, "", i.Engine, i.TrackResponse)
|
||||
i.listener, err = capture.NewListener(i.host, i.ports, i.config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
i.listener.SetPcapOptions(i.PcapOptions)
|
||||
|
||||
err = i.listener.Activate()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
pool := tcp.NewMessagePool(i.CopyBufferSize, i.Expire, Debug, i.handler)
|
||||
pool.MatchUUID(i.TrackResponse)
|
||||
if i.Protocol == ProtocolHTTP {
|
||||
pool.Start = http1StartHint
|
||||
pool.End = http1EndHint
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, i.cancelListener = context.WithCancel(context.Background())
|
||||
errCh := i.listener.ListenBackground(ctx, pool.Handler)
|
||||
errCh := i.listener.ListenBackground(ctx)
|
||||
<-i.listener.Reading
|
||||
Debug(1, i)
|
||||
go func() {
|
||||
@@ -166,12 +138,8 @@ func (i *RAWInput) listen(address string) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *RAWInput) handler(m *tcp.Message) {
|
||||
i.message <- m
|
||||
}
|
||||
|
||||
func (i *RAWInput) String() string {
|
||||
return fmt.Sprintf("Intercepting traffic from: %s:%d", i.host, i.port)
|
||||
return fmt.Sprintf("Intercepting traffic from: %s:%s", i.host, strings.Join(strings.Fields(fmt.Sprint(i.ports)), ","))
|
||||
}
|
||||
|
||||
// GetStats returns the stats so far and reset the stats
|
||||
@@ -205,15 +173,3 @@ func (i *RAWInput) addStats(mStats tcp.Stats) {
|
||||
i.messageStats = append(i.messageStats, mStats)
|
||||
i.Unlock()
|
||||
}
|
||||
|
||||
func http1StartHint(pckt *tcp.Packet) (isIncoming, isOutgoing bool) {
|
||||
isIncoming = proto.HasRequestTitle(pckt.Payload)
|
||||
if isIncoming {
|
||||
return
|
||||
}
|
||||
return false, proto.HasResponseTitle(pckt.Payload)
|
||||
}
|
||||
|
||||
func http1EndHint(m *tcp.Message) bool {
|
||||
return proto.HasFullPayload(m.Data(), m)
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,7 +1,10 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/buger/goreplay/internal/capture"
|
||||
"github.com/buger/goreplay/internal/tcp"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -13,9 +16,6 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/capture"
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
const testRawExpire = time.Millisecond * 200
|
||||
@@ -43,7 +43,7 @@ func TestRAWInputIPv4(t *testing.T) {
|
||||
conf := RAWInputConfig{
|
||||
Engine: capture.EnginePcap,
|
||||
Expire: 0,
|
||||
Protocol: ProtocolHTTP,
|
||||
Protocol: tcp.ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
RealIPHeader: "X-Real-IP",
|
||||
}
|
||||
@@ -58,6 +58,7 @@ func TestRAWInputIPv4(t *testing.T) {
|
||||
} else {
|
||||
respCounter++
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
@@ -71,14 +72,18 @@ func TestRAWInputIPv4(t *testing.T) {
|
||||
emitter := NewEmitter()
|
||||
defer emitter.Close()
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
// time.Sleep(time.Second)
|
||||
for i := 0; i < 1; i++ {
|
||||
wg.Add(2)
|
||||
_, err = http.Get(addr)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
const want = 10
|
||||
if reqCounter != respCounter && reqCounter != want {
|
||||
@@ -108,7 +113,7 @@ func TestRAWInputNoKeepAlive(t *testing.T) {
|
||||
conf := RAWInputConfig{
|
||||
Engine: capture.EnginePcap,
|
||||
Expire: testRawExpire,
|
||||
Protocol: ProtocolHTTP,
|
||||
Protocol: tcp.ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
}
|
||||
input := NewRAWInput(":"+port, conf)
|
||||
@@ -116,10 +121,11 @@ func TestRAWInputNoKeepAlive(t *testing.T) {
|
||||
output := NewTestOutput(func(msg *Message) {
|
||||
if msg.Meta[0] == '1' {
|
||||
atomic.AddInt64(&reqCounter, 1)
|
||||
wg.Done()
|
||||
} else {
|
||||
atomic.AddInt64(&respCounter, 1)
|
||||
wg.Done()
|
||||
}
|
||||
wg.Done()
|
||||
})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
@@ -173,7 +179,7 @@ func TestRAWInputIPv6(t *testing.T) {
|
||||
var respCounter, reqCounter int64
|
||||
conf := RAWInputConfig{
|
||||
Engine: capture.EnginePcap,
|
||||
Protocol: ProtocolHTTP,
|
||||
Protocol: tcp.ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
}
|
||||
input := NewRAWInput(originAddr, conf)
|
||||
@@ -228,10 +234,11 @@ func TestInputRAWChunkedEncoding(t *testing.T) {
|
||||
|
||||
originAddr := strings.Replace(origin.Listener.Addr().String(), "[::]", "127.0.0.1", -1)
|
||||
conf := RAWInputConfig{
|
||||
Engine: capture.EnginePcap,
|
||||
Expire: time.Second,
|
||||
Protocol: ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
Engine: capture.EnginePcap,
|
||||
Expire: time.Second,
|
||||
Protocol: tcp.ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
AllowIncomplete: true,
|
||||
}
|
||||
input := NewRAWInput(originAddr, conf)
|
||||
|
||||
@@ -309,7 +316,7 @@ func BenchmarkRAWInputWithReplay(b *testing.B) {
|
||||
conf := RAWInputConfig{
|
||||
Engine: capture.EnginePcap,
|
||||
Expire: testRawExpire,
|
||||
Protocol: ProtocolHTTP,
|
||||
Protocol: tcp.ProtocolHTTP,
|
||||
TrackResponse: true,
|
||||
}
|
||||
input := NewRAWInput(originAddr, conf)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build !linux
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
)
|
||||
|
||||
func newAfpacketHandle(device string, snaplen int, block_size int, num_blocks int,
|
||||
useVLAN bool, timeout time.Duration) (*afpacketHandle, error) {
|
||||
return nil, fmt.Errorf("Not implemented")
|
||||
}
|
||||
|
||||
func afpacketComputeSize(targetSizeMb int, snaplen int, pageSize int) (
|
||||
frameSize int, blockSize int, numBlocks int, err error) {
|
||||
return 0, 0, 0, fmt.Errorf("Not implemented")
|
||||
}
|
||||
|
||||
type afpacketHandle struct{}
|
||||
|
||||
// ReadPacketData satisfies PacketDataSource interface
|
||||
func (h *afpacketHandle) ReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {
|
||||
return nil, gopacket.CaptureInfo{}, fmt.Errorf("Not implemented")
|
||||
}
|
||||
|
||||
// SetBPFFilter translates a BPF filter string into BPF RawInstruction and applies them.
|
||||
func (h *afpacketHandle) SetBPFFilter(filter string, snaplen int) (err error) {
|
||||
return fmt.Errorf("Not implemented")
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
//go:build linux
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/afpacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"golang.org/x/net/bpf"
|
||||
|
||||
_ "github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
type afpacketHandle struct {
|
||||
TPacket *afpacket.TPacket
|
||||
}
|
||||
|
||||
func newAfpacketHandle(device string, snaplen int, block_size int, num_blocks int,
|
||||
useVLAN bool, timeout time.Duration) (*afpacketHandle, error) {
|
||||
|
||||
h := &afpacketHandle{}
|
||||
var err error
|
||||
|
||||
if device == "any" {
|
||||
h.TPacket, err = afpacket.NewTPacket(
|
||||
afpacket.OptFrameSize(snaplen),
|
||||
afpacket.OptBlockSize(block_size),
|
||||
afpacket.OptNumBlocks(num_blocks),
|
||||
afpacket.OptAddVLANHeader(false),
|
||||
afpacket.OptPollTimeout(timeout),
|
||||
afpacket.SocketRaw,
|
||||
afpacket.TPacketVersion3)
|
||||
} else {
|
||||
h.TPacket, err = afpacket.NewTPacket(
|
||||
afpacket.OptInterface(device),
|
||||
afpacket.OptFrameSize(snaplen),
|
||||
afpacket.OptBlockSize(block_size),
|
||||
afpacket.OptNumBlocks(num_blocks),
|
||||
afpacket.OptAddVLANHeader(false),
|
||||
afpacket.OptPollTimeout(timeout),
|
||||
afpacket.SocketRaw,
|
||||
afpacket.TPacketVersion3)
|
||||
}
|
||||
return h, err
|
||||
}
|
||||
|
||||
// ReadPacketData satisfies PacketDataSource interface
|
||||
func (h *afpacketHandle) ReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {
|
||||
return h.TPacket.ReadPacketData()
|
||||
}
|
||||
|
||||
// SetBPFFilter translates a BPF filter string into BPF RawInstruction and applies them.
|
||||
func (h *afpacketHandle) SetBPFFilter(filter string, snaplen int) (err error) {
|
||||
pcapBPF, err := pcap.CompileBPFFilter(layers.LinkTypeEthernet, snaplen, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bpfIns := []bpf.RawInstruction{}
|
||||
for _, ins := range pcapBPF {
|
||||
bpfIns2 := bpf.RawInstruction{
|
||||
Op: ins.Code,
|
||||
Jt: ins.Jt,
|
||||
Jf: ins.Jf,
|
||||
K: ins.K,
|
||||
}
|
||||
bpfIns = append(bpfIns, bpfIns2)
|
||||
}
|
||||
if h.TPacket.SetBPF(bpfIns); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.TPacket.SetBPF(bpfIns)
|
||||
}
|
||||
|
||||
// LinkType returns ethernet link type.
|
||||
func (h *afpacketHandle) LinkType() layers.LinkType {
|
||||
return layers.LinkTypeEthernet
|
||||
}
|
||||
|
||||
// Close will close afpacket source.
|
||||
func (h *afpacketHandle) Close() {
|
||||
h.TPacket.Close()
|
||||
}
|
||||
|
||||
// SocketStats prints received, dropped, queue-freeze packet stats.
|
||||
func (h *afpacketHandle) SocketStats() (as afpacket.SocketStats, asv afpacket.SocketStatsV3, err error) {
|
||||
return h.TPacket.SocketStats()
|
||||
}
|
||||
|
||||
// afpacketComputeSize computes the block_size and the num_blocks in such a way that the
|
||||
// allocated mmap buffer is close to but smaller than target_size_mb.
|
||||
// The restriction is that the block_size must be divisible by both the
|
||||
// frame size and page size.
|
||||
func afpacketComputeSize(targetSizeMb int, snaplen int, pageSize int) (
|
||||
frameSize int, blockSize int, numBlocks int, err error) {
|
||||
|
||||
if snaplen < pageSize {
|
||||
frameSize = pageSize / (pageSize / snaplen)
|
||||
} else {
|
||||
frameSize = (snaplen/pageSize + 1) * pageSize
|
||||
}
|
||||
|
||||
// 128 is the default from the gopacket library so just use that
|
||||
blockSize = frameSize * 128
|
||||
numBlocks = (targetSizeMb * 1024 * 1024) / blockSize
|
||||
|
||||
if numBlocks == 0 {
|
||||
return 0, 0, 0, fmt.Errorf("Interface buffersize is too small")
|
||||
}
|
||||
|
||||
return frameSize, blockSize, numBlocks, nil
|
||||
}
|
||||
@@ -0,0 +1,911 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"github.com/buger/goreplay/internal/tcp"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var stats *expvar.Map
|
||||
|
||||
func init() {
|
||||
stats = expvar.NewMap("raw")
|
||||
stats.Init()
|
||||
}
|
||||
|
||||
// PacketHandler is a function that is used to handle packets
|
||||
type PacketHandler func(*tcp.Packet)
|
||||
|
||||
type PcapStatProvider interface {
|
||||
Stats() (*pcap.Stats, error)
|
||||
}
|
||||
|
||||
type PcapSetFilter interface {
|
||||
SetBPFFilter(string) error
|
||||
}
|
||||
|
||||
// PcapOptions options that can be set on a pcap capture handle,
|
||||
// these options take effect on inactive pcap handles
|
||||
type PcapOptions struct {
|
||||
BufferTimeout time.Duration `json:"input-raw-buffer-timeout"`
|
||||
TimestampType string `json:"input-raw-timestamp-type"`
|
||||
BPFFilter string `json:"input-raw-bpf-filter"`
|
||||
BufferSize size.Size `json:"input-raw-buffer-size"`
|
||||
Promiscuous bool `json:"input-raw-promisc"`
|
||||
Monitor bool `json:"input-raw-monitor"`
|
||||
Snaplen bool `json:"input-raw-override-snaplen"`
|
||||
Engine EngineType `json:"input-raw-engine"`
|
||||
VXLANPort int `json:"input-raw-vxlan-port"`
|
||||
VXLANVNIs []int `json:"input-raw-vxlan-vni"`
|
||||
VLAN bool `json:"input-raw-vlan"`
|
||||
VLANVIDs []int `json:"input-raw-vlan-vid"`
|
||||
Expire time.Duration `json:"input-raw-expire"`
|
||||
TrackResponse bool `json:"input-raw-track-response"`
|
||||
Protocol tcp.TCPProtocol `json:"input-raw-protocol"`
|
||||
RealIPHeader string `json:"input-raw-realip-header"`
|
||||
Stats bool `json:"input-raw-stats"`
|
||||
AllowIncomplete bool `json:"input-raw-allow-incomplete"`
|
||||
IgnoreInterface []string `json:"input-raw-ignore-interface"`
|
||||
Transport string
|
||||
}
|
||||
|
||||
// Listener handle traffic capture, this is its representation.
|
||||
type Listener struct {
|
||||
sync.Mutex
|
||||
|
||||
config PcapOptions
|
||||
|
||||
Activate func() error // function is used to activate the engine. it must be called before reading packets
|
||||
Handles map[string]packetHandle
|
||||
Interfaces []pcap.Interface
|
||||
loopIndex int
|
||||
Reading chan bool // this channel is closed when the listener has started reading packets
|
||||
messages chan *tcp.Message
|
||||
|
||||
ports []uint16
|
||||
host string // pcap file name or interface (name, hardware addr, index or ip address)
|
||||
|
||||
closeDone chan struct{}
|
||||
quit chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
type packetHandle struct {
|
||||
handler gopacket.PacketDataSource
|
||||
ips []net.IP
|
||||
}
|
||||
|
||||
// EngineType ...
|
||||
type EngineType uint8
|
||||
|
||||
// Available engines for intercepting traffic
|
||||
const (
|
||||
EnginePcap EngineType = 1 << iota
|
||||
EnginePcapFile
|
||||
EngineRawSocket
|
||||
EngineAFPacket
|
||||
EngineVXLAN
|
||||
)
|
||||
|
||||
// Set is here so that EngineType can implement flag.Var
|
||||
func (eng *EngineType) Set(v string) error {
|
||||
switch v {
|
||||
case "", "libpcap":
|
||||
*eng = EnginePcap
|
||||
case "pcap_file":
|
||||
*eng = EnginePcapFile
|
||||
case "raw_socket":
|
||||
*eng = EngineRawSocket
|
||||
case "af_packet":
|
||||
*eng = EngineAFPacket
|
||||
case "vxlan":
|
||||
*eng = EngineVXLAN
|
||||
default:
|
||||
return fmt.Errorf("invalid engine %s", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eng *EngineType) String() (e string) {
|
||||
switch *eng {
|
||||
case EnginePcapFile:
|
||||
e = "pcap_file"
|
||||
case EnginePcap:
|
||||
e = "libpcap"
|
||||
case EngineRawSocket:
|
||||
e = "raw_socket"
|
||||
case EngineAFPacket:
|
||||
e = "af_packet"
|
||||
case EngineVXLAN:
|
||||
e = "vxlan"
|
||||
default:
|
||||
e = ""
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// NewListener creates and initialize a new Listener. if transport or/and engine are invalid/unsupported
|
||||
// is "tcp" and "pcap", are assumed. l.Engine and l.Transport can help to get the values used.
|
||||
// if there is an error it will be associated with getting network interfaces
|
||||
func NewListener(host string, ports []uint16, config PcapOptions) (l *Listener, err error) {
|
||||
l = &Listener{}
|
||||
|
||||
l.host = host
|
||||
if l.host == "localhost" {
|
||||
l.host = "127.0.0.1"
|
||||
}
|
||||
l.ports = ports
|
||||
|
||||
l.config = config
|
||||
l.config.Transport = "tcp"
|
||||
l.Handles = make(map[string]packetHandle)
|
||||
|
||||
l.closeDone = make(chan struct{})
|
||||
l.quit = make(chan struct{})
|
||||
l.Reading = make(chan bool)
|
||||
l.messages = make(chan *tcp.Message, 10000)
|
||||
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
l.config.BPFFilter = l.Filter(pcap.Interface{}, k8sIPs(l.host[6:])...)
|
||||
}
|
||||
|
||||
switch config.Engine {
|
||||
default:
|
||||
l.Activate = l.activatePcap
|
||||
case EngineRawSocket:
|
||||
l.Activate = l.activateRawSocket
|
||||
case EngineAFPacket:
|
||||
l.Activate = l.activateAFPacket
|
||||
case EnginePcapFile:
|
||||
l.Activate = l.activatePcapFile
|
||||
return
|
||||
case EngineVXLAN:
|
||||
l.Activate = l.activateVxLanSocket
|
||||
return
|
||||
}
|
||||
|
||||
err = l.setInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Listen listens for packets from the handles, and call handler on every packet received
|
||||
// until the context done signal is sent or there is unrecoverable error on all handles.
|
||||
// this function must be called after activating pcap handles
|
||||
func (l *Listener) Listen(ctx context.Context) (err error) {
|
||||
l.Lock()
|
||||
for key, handle := range l.Handles {
|
||||
go l.readHandle(key, handle)
|
||||
}
|
||||
l.Unlock()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if l.closed {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Pod IP changes
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
newFilter := l.Filter(pcap.Interface{}, k8sIPs(l.host[6:])...)
|
||||
if newFilter != l.config.BPFFilter {
|
||||
fmt.Println("k8s pods configuration changed, new filter: ", newFilter)
|
||||
for _, h := range l.Handles {
|
||||
if _, ok := h.handler.(PcapSetFilter); ok {
|
||||
h.handler.(PcapSetFilter).SetBPFFilter(newFilter)
|
||||
}
|
||||
}
|
||||
|
||||
l.config.BPFFilter = newFilter
|
||||
}
|
||||
}
|
||||
|
||||
var prevInterfaces []string
|
||||
for _, in := range l.Interfaces {
|
||||
prevInterfaces = append(prevInterfaces, in.Name)
|
||||
}
|
||||
l.setInterfaces()
|
||||
|
||||
for _, in := range l.Interfaces {
|
||||
var found bool
|
||||
|
||||
for _, prev := range prevInterfaces {
|
||||
if in.Name == prev {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Println("Found new interface:", in.Name)
|
||||
l.Lock()
|
||||
l.Activate()
|
||||
|
||||
for key, handle := range l.Handles {
|
||||
if key == in.Name {
|
||||
fmt.Println("Activating capture on:", in.Name)
|
||||
go l.readHandle(key, handle)
|
||||
break
|
||||
}
|
||||
}
|
||||
l.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
close(l.Reading)
|
||||
done := ctx.Done()
|
||||
select {
|
||||
case <-done:
|
||||
close(l.quit) // signal close on all handles
|
||||
<-l.closeDone // wait all handles to be closed
|
||||
err = ctx.Err()
|
||||
case <-l.closeDone: // all handles closed voluntarily
|
||||
}
|
||||
|
||||
l.closed = true
|
||||
return
|
||||
}
|
||||
|
||||
// ListenBackground is like listen but can run concurrently and signal error through channel
|
||||
func (l *Listener) ListenBackground(ctx context.Context) chan error {
|
||||
err := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(err)
|
||||
if e := l.Listen(ctx); err != nil {
|
||||
err <- e
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
// Allowed format:
|
||||
//
|
||||
// [namespace/]pod/[pod_name]
|
||||
// [namespace/]deployment/[deployment_name]
|
||||
// [namespace/]daemonset/[daemonset_name]
|
||||
// [namespace/]labelSelector/[selector]
|
||||
// [namespace/]fieldSelector/[selector]
|
||||
func k8sIPs(addr string) []string {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// creates the clientset
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
sections := strings.Split(addr, "/")
|
||||
|
||||
if len(sections) < 2 {
|
||||
panic("Not supported k8s scheme. Allowed values: [namespace/]pod/[pod_name], [namespace/]deployment/[deployment_name], [namespace/]daemonset/[daemonset_name], [namespace/]label/[label-name]/[label-value]")
|
||||
}
|
||||
|
||||
// If no namespace passed, assume it is ALL
|
||||
switch sections[0] {
|
||||
case "pod", "deployment", "daemonset", "labelSelector", "fieldSelector":
|
||||
sections = append([]string{""}, sections...)
|
||||
}
|
||||
|
||||
namespace, selectorType, selectorValue := sections[0], sections[1], sections[2]
|
||||
|
||||
labelSelector := ""
|
||||
fieldSelector := ""
|
||||
|
||||
switch selectorType {
|
||||
case "pod":
|
||||
fieldSelector = "metadata.name=" + selectorValue
|
||||
case "deployment":
|
||||
labelSelector = "app=" + selectorValue
|
||||
case "daemonset":
|
||||
labelSelector = "pod-template-generation=1,name=" + selectorValue
|
||||
case "labelSelector":
|
||||
labelSelector = selectorValue
|
||||
case "fieldSelector":
|
||||
fieldSelector = selectorValue
|
||||
}
|
||||
|
||||
pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector, FieldSelector: fieldSelector})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var podIPs []string
|
||||
for _, pod := range pods.Items {
|
||||
for _, podIP := range pod.Status.PodIPs {
|
||||
podIPs = append(podIPs, podIP.IP)
|
||||
}
|
||||
}
|
||||
return podIPs
|
||||
}
|
||||
|
||||
// Filter returns automatic filter applied by goreplay
|
||||
// to a pcap handle of a specific interface
|
||||
func (l *Listener) Filter(ifi pcap.Interface, hosts ...string) (filter string) {
|
||||
// https://www.tcpdump.org/manpages/pcap-filter.7.html
|
||||
|
||||
if len(hosts) == 0 {
|
||||
// If k8s have not found any IPs
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
hosts = []string{}
|
||||
} else {
|
||||
hosts = []string{l.host}
|
||||
|
||||
if listenAll(l.host) || isDevice(l.host, ifi) {
|
||||
hosts = interfaceAddresses(ifi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filter = portsFilter(l.config.Transport, "dst", l.ports)
|
||||
|
||||
if len(hosts) != 0 && !l.config.Promiscuous {
|
||||
filter = fmt.Sprintf("((%s) and (%s))", filter, hostsFilter("dst", hosts))
|
||||
} else {
|
||||
filter = fmt.Sprintf("(%s)", filter)
|
||||
}
|
||||
|
||||
if l.config.TrackResponse {
|
||||
responseFilter := portsFilter(l.config.Transport, "src", l.ports)
|
||||
|
||||
if len(hosts) != 0 && !l.config.Promiscuous {
|
||||
responseFilter = fmt.Sprintf("((%s) and (%s))", responseFilter, hostsFilter("src", hosts))
|
||||
} else {
|
||||
responseFilter = fmt.Sprintf("(%s)", responseFilter)
|
||||
}
|
||||
|
||||
filter = fmt.Sprintf("%s or %s", filter, responseFilter)
|
||||
}
|
||||
|
||||
if l.config.VLAN {
|
||||
if len(l.config.VLANVIDs) > 0 {
|
||||
for _, vi := range l.config.VLANVIDs {
|
||||
filter = fmt.Sprintf("vlan %d and ", vi) + filter
|
||||
}
|
||||
} else {
|
||||
filter = "vlan and " + filter
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PcapHandle returns new pcap Handle from dev on success.
|
||||
// this function should be called after setting all necessary options for this listener
|
||||
func (l *Listener) PcapHandle(ifi pcap.Interface) (handle *pcap.Handle, err error) {
|
||||
var inactive *pcap.InactiveHandle
|
||||
inactive, err = pcap.NewInactiveHandle(ifi.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inactive handle error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
defer inactive.CleanUp()
|
||||
|
||||
if l.config.TimestampType != "" && l.config.TimestampType != "go" {
|
||||
var ts pcap.TimestampSource
|
||||
ts, err = pcap.TimestampSourceFromString(l.config.TimestampType)
|
||||
fmt.Println("Setting custom Timestamp Source. Supported values: `go`, ", inactive.SupportedTimestamps())
|
||||
err = inactive.SetTimestampSource(ts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q: supported timestamps: %q, interface: %q", err, inactive.SupportedTimestamps(), ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.config.Promiscuous {
|
||||
if err = inactive.SetPromisc(l.config.Promiscuous); err != nil {
|
||||
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.config.Monitor {
|
||||
if err = inactive.SetRFMon(l.config.Monitor); err != nil && !errors.Is(err, pcap.CannotSetRFMon) {
|
||||
return nil, fmt.Errorf("monitor mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
|
||||
var snap int
|
||||
|
||||
if !l.config.Snaplen {
|
||||
infs, _ := net.Interfaces()
|
||||
for _, i := range infs {
|
||||
if i.Name == ifi.Name {
|
||||
snap = i.MTU + 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if snap == 0 {
|
||||
snap = 64<<10 + 200
|
||||
}
|
||||
|
||||
err = inactive.SetSnapLen(snap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("snapshot length error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if l.config.BufferSize > 0 {
|
||||
err = inactive.SetBufferSize(int(l.config.BufferSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handle buffer size error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
}
|
||||
if l.config.BufferTimeout == 0 {
|
||||
l.config.BufferTimeout = 2000 * time.Millisecond
|
||||
}
|
||||
err = inactive.SetTimeout(l.config.BufferTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("handle buffer timeout error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
handle, err = inactive.Activate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("PCAP Activate device error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
|
||||
bpfFilter := l.config.BPFFilter
|
||||
if bpfFilter == "" {
|
||||
bpfFilter = l.Filter(ifi)
|
||||
}
|
||||
fmt.Println("Interface:", ifi.Name, ". BPF Filter:", bpfFilter)
|
||||
err = handle.SetBPFFilter(bpfFilter)
|
||||
if err != nil {
|
||||
handle.Close()
|
||||
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, bpfFilter, ifi.Name)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SocketHandle returns new unix ethernet handle associated with this listener settings
|
||||
func (l *Listener) SocketHandle(ifi pcap.Interface) (handle Socket, err error) {
|
||||
handle, err = NewSocket(ifi)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sock raw error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if err = handle.SetPromiscuous(l.config.Promiscuous || l.config.Monitor); err != nil {
|
||||
return nil, fmt.Errorf("promiscuous mode error: %q, interface: %q", err, ifi.Name)
|
||||
}
|
||||
if l.config.BPFFilter == "" {
|
||||
l.config.BPFFilter = l.Filter(ifi)
|
||||
}
|
||||
fmt.Println("BPF Filter: ", l.config.BPFFilter)
|
||||
if err = handle.SetBPFFilter(l.config.BPFFilter); err != nil {
|
||||
handle.Close()
|
||||
return nil, fmt.Errorf("BPF filter error: %q%s, interface: %q", err, l.config.BPFFilter, ifi.Name)
|
||||
}
|
||||
handle.SetLoopbackIndex(int32(l.loopIndex))
|
||||
return
|
||||
}
|
||||
|
||||
func http1StartHint(pckt *tcp.Packet) (isRequest, isResponse bool) {
|
||||
if proto.HasRequestTitle(pckt.Payload) {
|
||||
return true, false
|
||||
}
|
||||
|
||||
if proto.HasResponseTitle(pckt.Payload) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
// No request or response detected
|
||||
return false, false
|
||||
}
|
||||
|
||||
func http1EndHint(m *tcp.Message) bool {
|
||||
if m.MissingChunk() {
|
||||
return false
|
||||
}
|
||||
|
||||
req, res := http1StartHint(m.Packets()[0])
|
||||
return proto.HasFullPayload(m, m.PacketData()...) && (req || res)
|
||||
}
|
||||
|
||||
func (l *Listener) readHandle(key string, hndl packetHandle) {
|
||||
runtime.LockOSThread()
|
||||
|
||||
defer l.closeHandles(key)
|
||||
linkSize := 14
|
||||
linkType := int(layers.LinkTypeEthernet)
|
||||
if _, ok := hndl.handler.(*pcap.Handle); ok {
|
||||
linkType = int(hndl.handler.(*pcap.Handle).LinkType())
|
||||
linkSize, ok = pcapLinkTypeLength(linkType, l.config.VLAN)
|
||||
if !ok {
|
||||
if os.Getenv("GORDEBUG") != "0" {
|
||||
log.Printf("can not identify link type of an interface '%s'\n", key)
|
||||
}
|
||||
return // can't find the linktype size
|
||||
}
|
||||
}
|
||||
|
||||
messageParser := tcp.NewMessageParser(l.messages, l.ports, hndl.ips, l.config.Expire, l.config.AllowIncomplete)
|
||||
|
||||
if l.config.Protocol == tcp.ProtocolHTTP {
|
||||
messageParser.Start = http1StartHint
|
||||
messageParser.End = http1EndHint
|
||||
}
|
||||
|
||||
timer := time.NewTicker(1 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-l.quit:
|
||||
return
|
||||
case <-timer.C:
|
||||
if h, ok := hndl.handler.(PcapStatProvider); ok {
|
||||
s, err := h.Stats()
|
||||
if err == nil {
|
||||
stats.Add("packets_received", int64(s.PacketsReceived))
|
||||
stats.Add("packets_dropped", int64(s.PacketsDropped))
|
||||
stats.Add("packets_if_dropped", int64(s.PacketsIfDropped))
|
||||
}
|
||||
}
|
||||
default:
|
||||
data, ci, err := hndl.handler.ReadPacketData()
|
||||
if err == nil {
|
||||
if l.config.TimestampType == "go" {
|
||||
ci.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
messageParser.PacketHandler(&tcp.PcapPacket{
|
||||
Data: data,
|
||||
LType: linkType,
|
||||
LTypeLen: linkSize,
|
||||
Ci: &ci,
|
||||
})
|
||||
continue
|
||||
}
|
||||
if enext, ok := err.(pcap.NextError); ok && enext == pcap.NextErrorTimeoutExpired {
|
||||
continue
|
||||
}
|
||||
if eno, ok := err.(syscall.Errno); ok && eno.Temporary() {
|
||||
continue
|
||||
}
|
||||
if enet, ok := err.(*net.OpError); ok && (enet.Temporary() || enet.Timeout()) {
|
||||
continue
|
||||
}
|
||||
if err == io.EOF || err == io.ErrClosedPipe {
|
||||
log.Printf("stopped reading from %s interface with error %s\n", key, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("stopped reading from %s interface with error %s\n", key, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Messages() chan *tcp.Message {
|
||||
return l.messages
|
||||
}
|
||||
|
||||
func (l *Listener) closeHandles(key string) {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
if handle, ok := l.Handles[key]; ok {
|
||||
if c, ok := handle.handler.(io.Closer); ok {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
delete(l.Handles, key)
|
||||
if len(l.Handles) == 0 {
|
||||
close(l.closeDone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) activatePcap() error {
|
||||
var e error
|
||||
var msg string
|
||||
for _, ifi := range l.Interfaces {
|
||||
if _, found := l.Handles[ifi.Name]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
var handle *pcap.Handle
|
||||
handle, e = l.PcapHandle(ifi)
|
||||
if e != nil {
|
||||
msg += ("\n" + e.Error())
|
||||
continue
|
||||
}
|
||||
l.Handles[ifi.Name] = packetHandle{
|
||||
handler: handle,
|
||||
ips: interfaceIPs(ifi),
|
||||
}
|
||||
}
|
||||
if len(l.Handles) == 0 {
|
||||
return fmt.Errorf("pcap handles error:%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) activateVxLanSocket() error {
|
||||
handler, err := newVXLANHandler(l.config.VXLANPort, l.config.VXLANVNIs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.Handles["vxlan"] = packetHandle{
|
||||
handler: handler,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) activateRawSocket() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return fmt.Errorf("sock_raw is not stabilized on OS other than linux")
|
||||
}
|
||||
var msg string
|
||||
var e error
|
||||
for _, ifi := range l.Interfaces {
|
||||
if _, found := l.Handles[ifi.Name]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
var handle Socket
|
||||
handle, e = l.SocketHandle(ifi)
|
||||
if e != nil {
|
||||
msg += ("\n" + e.Error())
|
||||
continue
|
||||
}
|
||||
l.Handles[ifi.Name] = packetHandle{
|
||||
handler: handle,
|
||||
ips: interfaceIPs(ifi),
|
||||
}
|
||||
}
|
||||
if len(l.Handles) == 0 {
|
||||
return fmt.Errorf("raw socket handles error:%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) activatePcapFile() (err error) {
|
||||
var handle *pcap.Handle
|
||||
var e error
|
||||
if handle, e = pcap.OpenOffline(l.host); e != nil {
|
||||
return fmt.Errorf("open pcap file error: %q", e)
|
||||
}
|
||||
|
||||
tmp := l.host
|
||||
l.host = ""
|
||||
l.config.BPFFilter = l.Filter(pcap.Interface{})
|
||||
l.host = tmp
|
||||
|
||||
if e = handle.SetBPFFilter(l.config.BPFFilter); e != nil {
|
||||
handle.Close()
|
||||
return fmt.Errorf("BPF filter error: %q, filter: %s", e, l.config.BPFFilter)
|
||||
}
|
||||
|
||||
fmt.Println("BPF Filter:", l.config.BPFFilter)
|
||||
|
||||
l.Handles["pcap_file"] = packetHandle{
|
||||
handler: handle,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Listener) activateAFPacket() error {
|
||||
szFrame, szBlock, numBlocks, err := afpacketComputeSize(32, 32<<10, os.Getpagesize())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msg string
|
||||
for _, ifi := range l.Interfaces {
|
||||
if _, found := l.Handles[ifi.Name]; found {
|
||||
continue
|
||||
}
|
||||
|
||||
handle, err := newAfpacketHandle(ifi.Name, szFrame, szBlock, numBlocks, false, pcap.BlockForever)
|
||||
|
||||
if err != nil {
|
||||
msg += ("\n" + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if l.config.BPFFilter == "" {
|
||||
l.config.BPFFilter = l.Filter(ifi)
|
||||
}
|
||||
fmt.Println("Interface:", ifi.Name, ". BPF Filter:", l.config.BPFFilter)
|
||||
handle.SetBPFFilter(l.config.BPFFilter, 64<<10)
|
||||
|
||||
l.Handles[ifi.Name] = packetHandle{
|
||||
handler: handle,
|
||||
ips: interfaceIPs(ifi),
|
||||
}
|
||||
}
|
||||
|
||||
if len(l.Handles) == 0 {
|
||||
return fmt.Errorf("pcap handles error:%s", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) setInterfaces() (err error) {
|
||||
var pifis []pcap.Interface
|
||||
pifis, err = pcap.FindAllDevs()
|
||||
ifis, _ := net.Interfaces()
|
||||
l.Interfaces = []pcap.Interface{}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, pi := range pifis {
|
||||
ignore := false
|
||||
for _, ig := range l.config.IgnoreInterface {
|
||||
if pi.Name == ig {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(l.host, "k8s://") {
|
||||
if !strings.HasPrefix(pi.Name, "veth") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if isDevice(l.host, pi) {
|
||||
l.Interfaces = []pcap.Interface{pi}
|
||||
return
|
||||
}
|
||||
|
||||
var ni net.Interface
|
||||
for _, i := range ifis {
|
||||
if i.Name == pi.Name {
|
||||
ni = i
|
||||
break
|
||||
}
|
||||
|
||||
addrs, _ := i.Addrs()
|
||||
for _, a := range addrs {
|
||||
for _, pa := range pi.Addresses {
|
||||
if a.String() == pa.IP.String() {
|
||||
ni = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ni.Flags&net.FlagLoopback != 0 {
|
||||
l.loopIndex = ni.Index
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
if len(pi.Addresses) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if ni.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
l.Interfaces = append(l.Interfaces, pi)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isDevice(addr string, ifi pcap.Interface) bool {
|
||||
// Windows npcap loopback have no IPs
|
||||
if addr == "127.0.0.1" && ifi.Name == `\Device\NPF_Loopback` {
|
||||
return true
|
||||
}
|
||||
|
||||
if addr == ifi.Name {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(addr, "*") {
|
||||
if strings.HasPrefix(ifi.Name, addr[:len(addr)-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, _addr := range ifi.Addresses {
|
||||
if _addr.IP.String() == addr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func interfaceAddresses(ifi pcap.Interface) []string {
|
||||
var hosts []string
|
||||
for _, addr := range ifi.Addresses {
|
||||
hosts = append(hosts, addr.IP.String())
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
func interfaceIPs(ifi pcap.Interface) []net.IP {
|
||||
var ips []net.IP
|
||||
for _, addr := range ifi.Addresses {
|
||||
ips = append(ips, addr.IP)
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func listenAll(addr string) bool {
|
||||
switch addr {
|
||||
case "", "0.0.0.0", "[::]", "::":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func portsFilter(transport string, direction string, ports []uint16) string {
|
||||
if len(ports) == 0 || ports[0] == 0 {
|
||||
return fmt.Sprintf("%s %s portrange 0-%d", transport, direction, 1<<16-1)
|
||||
}
|
||||
|
||||
var filters []string
|
||||
for _, port := range ports {
|
||||
filters = append(filters, fmt.Sprintf("%s %s port %d", transport, direction, port))
|
||||
}
|
||||
return strings.Join(filters, " or ")
|
||||
}
|
||||
|
||||
func hostsFilter(direction string, hosts []string) string {
|
||||
var hostsFilters []string
|
||||
for _, host := range hosts {
|
||||
hostsFilters = append(hostsFilters, fmt.Sprintf("%s host %s", direction, host))
|
||||
}
|
||||
|
||||
return strings.Join(hostsFilters, " or ")
|
||||
}
|
||||
|
||||
func pcapLinkTypeLength(lType int, vlan bool) (int, bool) {
|
||||
switch layers.LinkType(lType) {
|
||||
case layers.LinkTypeEthernet:
|
||||
if vlan {
|
||||
return 18, true
|
||||
} else {
|
||||
return 14, true
|
||||
}
|
||||
case layers.LinkTypeNull, layers.LinkTypeLoop:
|
||||
return 4, true
|
||||
case layers.LinkTypeRaw, 12, 14:
|
||||
return 0, true
|
||||
case layers.LinkTypeIPv4, layers.LinkTypeIPv6:
|
||||
// (TODO:) look out for IP encapsulation?
|
||||
return 0, true
|
||||
case layers.LinkTypeLinuxSLL:
|
||||
return 16, true
|
||||
case layers.LinkTypeFDDI:
|
||||
return 13, true
|
||||
case 226 /*DLT_IPNET*/ :
|
||||
// https://www.tcpdump.org/linktypes/LINKTYPE_IPNET.html
|
||||
return 24, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetInterfaces(t *testing.T) {
|
||||
listener := &Listener{
|
||||
loopIndex: 99999,
|
||||
}
|
||||
listener.setInterfaces()
|
||||
|
||||
for _, nic := range listener.Interfaces {
|
||||
if (len(nic.Addresses)) == 0 {
|
||||
t.Errorf("nic %s was captured with 0 addresses", nic.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if listener.loopIndex == 99999 {
|
||||
t.Errorf("loopback nic index was not found")
|
||||
}
|
||||
}
|
||||
@@ -9,27 +9,34 @@ example:
|
||||
|
||||
// for the transport should be "tcp"
|
||||
listener, err := capture.NewListener(host, port, transport, engine, trackResponse)
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
listener.SetPcapOptions(opts)
|
||||
err = listner.Activate()
|
||||
if err != nil {
|
||||
// handle it
|
||||
}
|
||||
|
||||
if err := listener.Listen(context.Background(), handler); err != nil {
|
||||
// handle error
|
||||
}
|
||||
if err != nil {
|
||||
// handle it
|
||||
}
|
||||
|
||||
if err := listener.Listen(context.Background(), handler); err != nil {
|
||||
// handle error
|
||||
}
|
||||
|
||||
// or
|
||||
errCh := listener.ListenBackground(context.Background(), handler) // runs in the backgorund
|
||||
errCh := listener.ListenBackground(context.Background(), handler) // runs in the background
|
||||
select {
|
||||
case err := <- errCh:
|
||||
|
||||
// handle error
|
||||
|
||||
case <-quit:
|
||||
|
||||
//
|
||||
|
||||
case <- l.Reading: // if we have started reading
|
||||
}
|
||||
|
||||
*/
|
||||
package capture // import github.com/buger/goreplay/capture
|
||||
@@ -35,18 +35,18 @@ const versionMinor = 4
|
||||
// an append), you must call WriteFileHeader before WritePacket. Packet
|
||||
// timestamps are written with nanosecond precision.
|
||||
//
|
||||
// // Write a new file:
|
||||
// f, _ := os.Create("/tmp/file.pcap")
|
||||
// w := pcapgo.NewWriterNanos(f)
|
||||
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
|
||||
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
|
||||
// f.Close()
|
||||
// // Append to existing file (must have same snaplen and linktype)
|
||||
// f2, _ := os.OpenFile("/tmp/fileNano.pcap", os.O_APPEND, 0700)
|
||||
// w2 := pcapgo.NewWriter(f2)
|
||||
// // no need for file header, it's already written.
|
||||
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
|
||||
// f2.Close()
|
||||
// // Write a new file:
|
||||
// f, _ := os.Create("/tmp/file.pcap")
|
||||
// w := pcapgo.NewWriterNanos(f)
|
||||
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
|
||||
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
|
||||
// f.Close()
|
||||
// // Append to existing file (must have same snaplen and linktype)
|
||||
// f2, _ := os.OpenFile("/tmp/fileNano.pcap", os.O_APPEND, 0700)
|
||||
// w2 := pcapgo.NewWriter(f2)
|
||||
// // no need for file header, it's already written.
|
||||
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
|
||||
// f2.Close()
|
||||
func NewWriterNanos(w io.Writer) *Writer {
|
||||
return &Writer{w: w, tsScaler: nanosPerNano}
|
||||
}
|
||||
@@ -54,20 +54,20 @@ func NewWriterNanos(w io.Writer) *Writer {
|
||||
// NewWriter returns a new writer object, for writing packet data out
|
||||
// to the given writer. If this is a new empty writer (as opposed to
|
||||
// an append), you must call WriteFileHeader before WritePacket.
|
||||
// Packet timestamps are written witn microsecond precision.
|
||||
// Packet timestamps are written with microsecond precision.
|
||||
//
|
||||
// // Write a new file:
|
||||
// f, _ := os.Create("/tmp/file.pcap")
|
||||
// w := pcapgo.NewWriter(f)
|
||||
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
|
||||
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
|
||||
// f.Close()
|
||||
// // Append to existing file (must have same snaplen and linktype)
|
||||
// f2, _ := os.OpenFile("/tmp/file.pcap", os.O_APPEND, 0700)
|
||||
// w2 := pcapgo.NewWriter(f2)
|
||||
// // no need for file header, it's already written.
|
||||
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
|
||||
// f2.Close()
|
||||
// // Write a new file:
|
||||
// f, _ := os.Create("/tmp/file.pcap")
|
||||
// w := pcapgo.NewWriter(f)
|
||||
// w.WriteFileHeader(65536, layers.LinkTypeEthernet) // new file, must do this.
|
||||
// w.WritePacket(gopacket.CaptureInfo{...}, data1)
|
||||
// f.Close()
|
||||
// // Append to existing file (must have same snaplen and linktype)
|
||||
// f2, _ := os.OpenFile("/tmp/file.pcap", os.O_APPEND, 0700)
|
||||
// w2 := pcapgo.NewWriter(f2)
|
||||
// // no need for file header, it's already written.
|
||||
// w2.WritePacket(gopacket.CaptureInfo{...}, data2)
|
||||
// f2.Close()
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{w: w, tsScaler: nanosPerMicro}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build linux && !arm64
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
@@ -44,7 +46,23 @@ type SockRaw struct {
|
||||
}
|
||||
|
||||
// NewSocket returns new M'maped sock_raw on packet version 2.
|
||||
func NewSocket(ifi net.Interface) (*SockRaw, error) {
|
||||
func NewSocket(pifi pcap.Interface) (*SockRaw, error) {
|
||||
var ifi net.Interface
|
||||
|
||||
infs, _ := net.Interfaces()
|
||||
found := false
|
||||
for _, i := range infs {
|
||||
if i.Name == pifi.Name {
|
||||
ifi = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("can't find matching interface")
|
||||
}
|
||||
|
||||
// sock create
|
||||
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(ETHALL))
|
||||
if err != nil {
|
||||
@@ -217,7 +235,7 @@ func (sock *SockRaw) SetBPFFilter(expr string) error {
|
||||
return unix.SetsockoptSockFprog(sock.fd, unix.SOL_SOCKET, unix.SO_ATTACH_FILTER, fprog)
|
||||
}
|
||||
|
||||
// SetPromiscuous sets promiscous mode to the required value. for better result capture on all interfaces instead.
|
||||
// SetPromiscuous sets promiscuous mode to the required value. for better result capture on all interfaces instead.
|
||||
// If it is enabled, traffic not destined for the interface will also be captured.
|
||||
func (sock *SockRaw) SetPromiscuous(b bool) error {
|
||||
sock.mu.Lock()
|
||||
@@ -1,13 +1,14 @@
|
||||
// +build !linux
|
||||
//go:build !linux || arm64 || darwin
|
||||
|
||||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
// NewSocket returns new M'maped sock_raw on packet version 2.
|
||||
func NewSocket(_ net.Interface) (Socket, error) {
|
||||
func NewSocket(_ pcap.Interface) (Socket, error) {
|
||||
return nil, errors.New("afpacket socket is only available on linux")
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const VxLanPacketSize = 1526 //vxlan 8 B + ethernet II 1518 B
|
||||
|
||||
type vxlanHandle struct {
|
||||
connection *net.UDPConn
|
||||
packetChannel chan gopacket.Packet
|
||||
vnis []int
|
||||
}
|
||||
|
||||
func newVXLANHandler(port int, vnis []int) (*vxlanHandle, error) {
|
||||
if port == 0 {
|
||||
port = 4789
|
||||
}
|
||||
|
||||
addr := net.UDPAddr{
|
||||
Port: port,
|
||||
IP: net.ParseIP("0.0.0.0"),
|
||||
}
|
||||
|
||||
vxlanHandle := &vxlanHandle{}
|
||||
con, err := net.ListenUDP("udp", &addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(err.Error())
|
||||
}
|
||||
vxlanHandle.connection = con
|
||||
vxlanHandle.packetChannel = make(chan gopacket.Packet, 1000)
|
||||
vxlanHandle.vnis = vnis
|
||||
go vxlanHandle.reader()
|
||||
|
||||
return vxlanHandle, nil
|
||||
}
|
||||
|
||||
func (v *vxlanHandle) reader() {
|
||||
for {
|
||||
inputBytes := make([]byte, VxLanPacketSize)
|
||||
length, _, err := v.connection.ReadFromUDP(inputBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
packet := gopacket.NewPacket(inputBytes[:length], layers.LayerTypeVXLAN, gopacket.NoCopy)
|
||||
ci := packet.Metadata()
|
||||
ci.Timestamp = time.Now()
|
||||
ci.CaptureLength = length
|
||||
ci.Length = length
|
||||
|
||||
if len(v.vnis) > 0 && !v.vniIsAllowed(packet) {
|
||||
continue
|
||||
}
|
||||
|
||||
v.packetChannel <- packet
|
||||
}
|
||||
}
|
||||
|
||||
func (v *vxlanHandle) vniIsAllowed(packet gopacket.Packet) bool {
|
||||
defaultState := false
|
||||
if layer := packet.Layer(layers.LayerTypeVXLAN); layer != nil {
|
||||
vxlan, _ := layer.(*layers.VXLAN)
|
||||
for _, vn := range v.vnis {
|
||||
if vn > 0 && int(vxlan.VNI) == vn {
|
||||
return true
|
||||
}
|
||||
|
||||
if vn < 0 {
|
||||
if int(vxlan.VNI) == -vn {
|
||||
return false
|
||||
}
|
||||
defaultState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultState
|
||||
}
|
||||
|
||||
func (v *vxlanHandle) ReadPacketData() ([]byte, gopacket.CaptureInfo, error) {
|
||||
packet := <-v.packetChannel
|
||||
layer := packet.Layer(layers.LayerTypeVXLAN)
|
||||
bytes := layer.LayerPayload()
|
||||
|
||||
return bytes, packet.Metadata().CaptureInfo, nil
|
||||
}
|
||||
|
||||
func (v *vxlanHandle) Close() error {
|
||||
if v.connection != nil {
|
||||
return v.connection.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrDisposed is returned when an operation is performed on a disposed
|
||||
// queue.
|
||||
ErrDisposed = errors.New(`queue: disposed`)
|
||||
|
||||
// ErrTimeout is returned when an applicable queue operation times out.
|
||||
ErrTimeout = errors.New(`queue: poll timed out`)
|
||||
|
||||
// ErrEmptyQueue is returned when an non-applicable queue operation was called
|
||||
// due to the queue's empty item state
|
||||
ErrEmptyQueue = errors.New(`queue: empty queue`)
|
||||
)
|
||||
|
||||
// roundUp takes a uint64 greater than 0 and rounds it up to the next
|
||||
// power of 2.
|
||||
func roundUp(v uint64) uint64 {
|
||||
v--
|
||||
v |= v >> 1
|
||||
v |= v >> 2
|
||||
v |= v >> 4
|
||||
v |= v >> 8
|
||||
v |= v >> 16
|
||||
v |= v >> 32
|
||||
v++
|
||||
return v
|
||||
}
|
||||
|
||||
type node struct {
|
||||
position uint64
|
||||
data interface{}
|
||||
}
|
||||
|
||||
type nodes []node
|
||||
|
||||
// RingBuffer is a MPMC buffer that achieves threadsafety with CAS operations
|
||||
// only. A put on full or get on empty call will block until an item
|
||||
// is put or retrieved. Calling Dispose on the RingBuffer will unblock
|
||||
// any blocked threads with an error. This buffer is similar to the buffer
|
||||
// described here: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue
|
||||
// with some minor additions.
|
||||
type RingBuffer struct {
|
||||
_padding0 [8]uint64
|
||||
queue uint64
|
||||
_padding1 [8]uint64
|
||||
dequeue uint64
|
||||
_padding2 [8]uint64
|
||||
mask, disposed uint64
|
||||
_padding3 [8]uint64
|
||||
nodes nodes
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) init(size uint64) {
|
||||
size = roundUp(size)
|
||||
rb.nodes = make(nodes, size)
|
||||
for i := uint64(0); i < size; i++ {
|
||||
rb.nodes[i] = node{position: i}
|
||||
}
|
||||
rb.mask = size - 1 // so we don't have to do this with every put/get operation
|
||||
}
|
||||
|
||||
// Put adds the provided item to the queue. If the queue is full, this
|
||||
// call will block until an item is added to the queue or Dispose is called
|
||||
// on the queue. An error will be returned if the queue is disposed.
|
||||
func (rb *RingBuffer) Put(item interface{}) error {
|
||||
_, err := rb.put(item, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// Offer adds the provided item to the queue if there is space. If the queue
|
||||
// is full, this call will return false. An error will be returned if the
|
||||
// queue is disposed.
|
||||
func (rb *RingBuffer) Offer(item interface{}) (bool, error) {
|
||||
return rb.put(item, true)
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) put(item interface{}, offer bool) (bool, error) {
|
||||
var n *node
|
||||
pos := atomic.LoadUint64(&rb.queue)
|
||||
L:
|
||||
for {
|
||||
if atomic.LoadUint64(&rb.disposed) == 1 {
|
||||
return false, ErrDisposed
|
||||
}
|
||||
|
||||
n = &rb.nodes[pos&rb.mask]
|
||||
seq := atomic.LoadUint64(&n.position)
|
||||
switch dif := seq - pos; {
|
||||
case dif == 0:
|
||||
if atomic.CompareAndSwapUint64(&rb.queue, pos, pos+1) {
|
||||
break L
|
||||
}
|
||||
case dif < 0:
|
||||
panic(`Ring buffer in a compromised state during a put operation.`)
|
||||
default:
|
||||
pos = atomic.LoadUint64(&rb.queue)
|
||||
}
|
||||
|
||||
if offer {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
runtime.Gosched() // free up the cpu before the next iteration
|
||||
}
|
||||
|
||||
n.data = item
|
||||
atomic.StoreUint64(&n.position, pos+1)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Get will return the next item in the queue. This call will block
|
||||
// if the queue is empty. This call will unblock when an item is added
|
||||
// to the queue or Dispose is called on the queue. An error will be returned
|
||||
// if the queue is disposed.
|
||||
func (rb *RingBuffer) Get() (interface{}, error) {
|
||||
return rb.Poll(0)
|
||||
}
|
||||
|
||||
// Poll will return the next item in the queue. This call will block
|
||||
// if the queue is empty. This call will unblock when an item is added
|
||||
// to the queue, Dispose is called on the queue, or the timeout is reached. An
|
||||
// error will be returned if the queue is disposed or a timeout occurs. A
|
||||
// non-positive timeout will block indefinitely.
|
||||
func (rb *RingBuffer) Poll(timeout time.Duration) (interface{}, error) {
|
||||
var (
|
||||
n *node
|
||||
pos = atomic.LoadUint64(&rb.dequeue)
|
||||
start time.Time
|
||||
)
|
||||
if timeout > 0 {
|
||||
start = time.Now()
|
||||
}
|
||||
L:
|
||||
for {
|
||||
if atomic.LoadUint64(&rb.disposed) == 1 {
|
||||
return nil, ErrDisposed
|
||||
}
|
||||
|
||||
n = &rb.nodes[pos&rb.mask]
|
||||
seq := atomic.LoadUint64(&n.position)
|
||||
switch dif := seq - (pos + 1); {
|
||||
case dif == 0:
|
||||
if atomic.CompareAndSwapUint64(&rb.dequeue, pos, pos+1) {
|
||||
break L
|
||||
}
|
||||
case dif < 0:
|
||||
panic(`Ring buffer in compromised state during a get operation.`)
|
||||
default:
|
||||
pos = atomic.LoadUint64(&rb.dequeue)
|
||||
}
|
||||
|
||||
if timeout > 0 && time.Since(start) >= timeout {
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
if timeout < 0 {
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
runtime.Gosched() // free up the cpu before the next iteration
|
||||
}
|
||||
data := n.data
|
||||
n.data = nil
|
||||
atomic.StoreUint64(&n.position, pos+rb.mask+1)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Len returns the number of items in the queue.
|
||||
func (rb *RingBuffer) Len() uint64 {
|
||||
return atomic.LoadUint64(&rb.queue) - atomic.LoadUint64(&rb.dequeue)
|
||||
}
|
||||
|
||||
// Cap returns the capacity of this ring buffer.
|
||||
func (rb *RingBuffer) Cap() uint64 {
|
||||
return uint64(len(rb.nodes))
|
||||
}
|
||||
|
||||
// Dispose will dispose of this queue and free any blocked threads
|
||||
// in the Put and/or Get methods. Calling those methods on a disposed
|
||||
// queue will return an error.
|
||||
func (rb *RingBuffer) Dispose() {
|
||||
atomic.CompareAndSwapUint64(&rb.disposed, 0, 1)
|
||||
}
|
||||
|
||||
// IsDisposed will return a bool indicating if this queue has been
|
||||
// disposed.
|
||||
func (rb *RingBuffer) IsDisposed() bool {
|
||||
return atomic.LoadUint64(&rb.disposed) == 1
|
||||
}
|
||||
|
||||
// NewRingBuffer will allocate, initialize, and return a ring buffer
|
||||
// with the specified size.
|
||||
func NewRingBuffer(size uint64) *RingBuffer {
|
||||
rb := &RingBuffer{}
|
||||
rb.init(size)
|
||||
return rb
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package simpletime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var Now time.Time
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for {
|
||||
// Accurate enough
|
||||
Now = time.Now()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -4,7 +4,6 @@ parsing, reassembling tcp packets, handling communication with engine listeners(
|
||||
and reporting errors and statistics of packets.
|
||||
the packets are parsed by following TCP way(https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_segment_structure).
|
||||
|
||||
|
||||
example:
|
||||
|
||||
import "github.com/buger/goreplay/tcp"
|
||||
@@ -15,13 +14,12 @@ maxSize := 5 << 20
|
||||
debugger := func(debugLevel int, data ...interface{}){} // debugger can also be nil
|
||||
messageHandler := func(mssg *tcp.Message){}
|
||||
|
||||
mssgPool := tcp.NewMessagePool(maxMessageSize, messageExpire, debugger, messageHandler)
|
||||
mssgPool := tcp.NewMessageParser(maxMessageSize, messageExpire, debugger, messageHandler)
|
||||
listener.Listen(ctx, mssgPool.Handler)
|
||||
|
||||
you can use pool.End or/and pool.Start to set custom session behaviors
|
||||
|
||||
debugLevel in debugger function indicates the priority of the logs, the bigger the number the lower
|
||||
the priority. errors are signified by debug level 4 for errors, 5 for discarded packets, and 6 for received packets.
|
||||
|
||||
*/
|
||||
package tcp // import github.com/buger/goreplay/tcp
|
||||
@@ -0,0 +1,443 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// TCPProtocol is a number to indicate type of protocol
|
||||
type TCPProtocol uint8
|
||||
|
||||
const (
|
||||
// ProtocolHTTP ...
|
||||
ProtocolHTTP TCPProtocol = iota
|
||||
// ProtocolBinary ...
|
||||
ProtocolBinary
|
||||
)
|
||||
|
||||
// Set is here so that TCPProtocol can implement flag.Var
|
||||
func (protocol *TCPProtocol) Set(v string) error {
|
||||
switch v {
|
||||
case "", "http":
|
||||
*protocol = ProtocolHTTP
|
||||
case "binary":
|
||||
*protocol = ProtocolBinary
|
||||
default:
|
||||
return fmt.Errorf("unsupported protocol %s", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (protocol *TCPProtocol) String() string {
|
||||
switch *protocol {
|
||||
case ProtocolBinary:
|
||||
return "binary"
|
||||
case ProtocolHTTP:
|
||||
return "http"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Stats every message carry its own stats object
|
||||
type Stats struct {
|
||||
LostData int
|
||||
Length int // length of the data
|
||||
Start time.Time // first packet's timestamp
|
||||
End time.Time // last packet's timestamp
|
||||
SrcAddr string
|
||||
DstAddr string
|
||||
Direction Dir
|
||||
TimedOut bool // timeout before getting the whole message
|
||||
Truncated bool // last packet truncated due to max message size
|
||||
IPversion byte
|
||||
}
|
||||
|
||||
// Message is the representation of a tcp message
|
||||
type Message struct {
|
||||
packets []*Packet
|
||||
parser *MessageParser
|
||||
feedback interface{}
|
||||
continueAdjusted bool
|
||||
Stats
|
||||
}
|
||||
|
||||
// UUID returns the UUID of a TCP request and its response.
|
||||
func (m *Message) UUID() []byte {
|
||||
var streamID uint64
|
||||
pckt := m.packets[0]
|
||||
|
||||
// check if response or request have generated the ID before.
|
||||
if m.Direction == DirIncoming {
|
||||
streamID = uint64(pckt.SrcPort)<<48 | uint64(pckt.DstPort)<<32 |
|
||||
uint64(ip2int(pckt.SrcIP))
|
||||
} else {
|
||||
streamID = uint64(pckt.DstPort)<<48 | uint64(pckt.SrcPort)<<32 |
|
||||
uint64(ip2int(pckt.DstIP))
|
||||
}
|
||||
|
||||
id := make([]byte, 12)
|
||||
binary.BigEndian.PutUint64(id, streamID)
|
||||
|
||||
if m.Direction == DirIncoming {
|
||||
binary.BigEndian.PutUint32(id[8:], pckt.Ack)
|
||||
} else {
|
||||
binary.BigEndian.PutUint32(id[8:], pckt.Seq)
|
||||
}
|
||||
|
||||
uuidHex := make([]byte, 24)
|
||||
hex.Encode(uuidHex[:], id[:])
|
||||
|
||||
return uuidHex
|
||||
}
|
||||
|
||||
func (m *Message) add(packet *Packet) bool {
|
||||
// Skip duplicates
|
||||
for _, p := range m.packets {
|
||||
if p.Seq == packet.Seq {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Packets not always captured in same Seq order, and sometimes we need to prepend
|
||||
if len(m.packets) == 0 || packet.Seq > m.packets[len(m.packets)-1].Seq {
|
||||
m.packets = append(m.packets, packet)
|
||||
} else if packet.Seq < m.packets[0].Seq {
|
||||
m.packets = append([]*Packet{packet}, m.packets...)
|
||||
} else { // insert somewhere in the middle...
|
||||
for i, p := range m.packets {
|
||||
if packet.Seq < p.Seq {
|
||||
m.packets = append(m.packets[:i], append([]*Packet{packet}, m.packets[i:]...)...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.Length += len(packet.Payload)
|
||||
m.LostData += int(packet.Lost)
|
||||
|
||||
if packet.Timestamp.After(m.End) || m.End.IsZero() {
|
||||
m.End = packet.Timestamp
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Packets returns packets of the message
|
||||
func (m *Message) Packets() []*Packet {
|
||||
return m.packets
|
||||
}
|
||||
|
||||
func (m *Message) MissingChunk() bool {
|
||||
nextSeq := m.packets[0].Seq
|
||||
|
||||
for _, p := range m.packets {
|
||||
if p.Seq != nextSeq {
|
||||
return true
|
||||
}
|
||||
|
||||
nextSeq += uint32(len(p.Payload))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Message) PacketData() [][]byte {
|
||||
tmp := make([][]byte, len(m.packets))
|
||||
|
||||
for i, p := range m.packets {
|
||||
tmp[i] = p.Payload
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
// Data returns data in this message
|
||||
func (m *Message) Data() []byte {
|
||||
packetData := m.PacketData()
|
||||
tmp := packetData[0]
|
||||
|
||||
if len(packetData) > 0 {
|
||||
tmp, _ = copySlice(tmp, len(packetData[0]), packetData[1:]...)
|
||||
}
|
||||
|
||||
// Remove Expect header, since its replay not fully supported
|
||||
if state, ok := m.feedback.(*proto.HTTPState); ok {
|
||||
if state.Continue100 {
|
||||
tmp = proto.DeleteHeader(tmp, []byte("Expect"))
|
||||
}
|
||||
}
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
// SetProtocolState set feedback/data that can be used later, e.g with End or Start hint
|
||||
func (m *Message) SetProtocolState(feedback interface{}) {
|
||||
m.feedback = feedback
|
||||
}
|
||||
|
||||
// ProtocolState returns feedback associated to this message
|
||||
func (m *Message) ProtocolState() interface{} {
|
||||
return m.feedback
|
||||
}
|
||||
|
||||
// Sort a helper to sort packets
|
||||
func (m *Message) Sort() {
|
||||
sort.SliceStable(m.packets, func(i, j int) bool { return m.packets[i].Seq < m.packets[j].Seq })
|
||||
}
|
||||
|
||||
// Emitter message handler
|
||||
type Emitter func(*Message)
|
||||
|
||||
// HintEnd hints the parser to stop the session, see MessageParser.End
|
||||
// when set, it will be executed before checking FIN or RST flag
|
||||
type HintEnd func(*Message) bool
|
||||
|
||||
// HintStart hints the parser to start the reassembling the message, see MessageParser.Start
|
||||
// when set, it will be called after checking SYN flag
|
||||
type HintStart func(*Packet) (IsRequest, IsOutgoing bool)
|
||||
|
||||
// MessageParser holds data of all tcp messages in progress(still receiving/sending packets).
|
||||
// message is identified by its source port and dst port, and last 4bytes of src IP.
|
||||
type MessageParser struct {
|
||||
m map[uint64]*Message
|
||||
|
||||
messageExpire time.Duration // the maximum time to wait for the final packet, minimum is 100ms
|
||||
allowIncompete bool
|
||||
End HintEnd
|
||||
Start HintStart
|
||||
ticker *time.Ticker
|
||||
messages chan *Message
|
||||
packets chan *PcapPacket
|
||||
close chan struct{} // to signal that we are able to close
|
||||
ports []uint16
|
||||
ips []net.IP
|
||||
}
|
||||
|
||||
// NewMessageParser returns a new instance of message parser
|
||||
func NewMessageParser(messages chan *Message, ports []uint16, ips []net.IP, messageExpire time.Duration, allowIncompete bool) (parser *MessageParser) {
|
||||
parser = new(MessageParser)
|
||||
|
||||
parser.messageExpire = messageExpire
|
||||
if parser.messageExpire == 0 {
|
||||
parser.messageExpire = time.Millisecond * 1000
|
||||
}
|
||||
|
||||
parser.allowIncompete = allowIncompete
|
||||
|
||||
parser.packets = make(chan *PcapPacket, 10000)
|
||||
|
||||
if messages == nil {
|
||||
messages = make(chan *Message, 1000)
|
||||
}
|
||||
parser.messages = messages
|
||||
|
||||
parser.m = make(map[uint64]*Message)
|
||||
parser.ticker = time.NewTicker(time.Millisecond * 100)
|
||||
parser.close = make(chan struct{}, 1)
|
||||
|
||||
parser.ports = ports
|
||||
parser.ips = ips
|
||||
|
||||
go parser.wait()
|
||||
return parser
|
||||
}
|
||||
|
||||
var packetLen int
|
||||
|
||||
// Packet returns packet handler
|
||||
func (parser *MessageParser) PacketHandler(packet *PcapPacket) {
|
||||
packetLen++
|
||||
parser.packets <- packet
|
||||
}
|
||||
|
||||
func (parser *MessageParser) wait() {
|
||||
var (
|
||||
now time.Time
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case pckt := <-parser.packets:
|
||||
parser.processPacket(parser.parsePacket(pckt))
|
||||
case now = <-parser.ticker.C:
|
||||
parser.timer(now)
|
||||
case <-parser.close:
|
||||
parser.ticker.Stop()
|
||||
// parser.Close should wait for this function to return
|
||||
parser.close <- struct{}{}
|
||||
return
|
||||
// default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (parser *MessageParser) parsePacket(pcapPkt *PcapPacket) *Packet {
|
||||
pckt, err := ParsePacket(pcapPkt.Data, pcapPkt.LType, pcapPkt.LTypeLen, pcapPkt.Ci, false)
|
||||
if err != nil {
|
||||
if _, empty := err.(EmptyPacket); !empty {
|
||||
stats.Add("packet_error", 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, p := range parser.ports {
|
||||
if pckt.DstPort == p && containsOrEmpty(pckt.DstIP, parser.ips) {
|
||||
pckt.Direction = DirIncoming
|
||||
break
|
||||
} else if pckt.SrcPort == p && containsOrEmpty(pckt.SrcIP, parser.ips) {
|
||||
pckt.Direction = DirOutcoming
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return pckt
|
||||
}
|
||||
|
||||
func containsOrEmpty(element net.IP, ipList []net.IP) bool {
|
||||
if len(ipList) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, ip := range ipList {
|
||||
if ip.Equal(element) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (parser *MessageParser) processPacket(pckt *Packet) {
|
||||
if pckt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Trying to build unique hash, but there is small chance of collision
|
||||
// No matter if it is request or response, all packets in the same message have same
|
||||
m, ok := parser.m[pckt.MessageID()]
|
||||
switch {
|
||||
case ok:
|
||||
if m.Direction == DirUnknown {
|
||||
if in, out := parser.Start(pckt); in || out {
|
||||
if in {
|
||||
m.Direction = DirIncoming
|
||||
} else {
|
||||
m.Direction = DirOutcoming
|
||||
}
|
||||
}
|
||||
}
|
||||
parser.addPacket(m, pckt)
|
||||
return
|
||||
case pckt.Direction == DirUnknown && parser.Start != nil:
|
||||
if in, out := parser.Start(pckt); in || out {
|
||||
if in {
|
||||
pckt.Direction = DirIncoming
|
||||
} else {
|
||||
pckt.Direction = DirOutcoming
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = new(Message)
|
||||
m.Direction = pckt.Direction
|
||||
m.SrcAddr = pckt.SrcIP.String()
|
||||
m.DstAddr = pckt.DstIP.String()
|
||||
|
||||
parser.m[pckt.MessageID()] = m
|
||||
|
||||
m.Start = pckt.Timestamp
|
||||
m.parser = parser
|
||||
parser.addPacket(m, pckt)
|
||||
}
|
||||
|
||||
func (parser *MessageParser) addPacket(m *Message, pckt *Packet) bool {
|
||||
if !m.add(pckt) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we are using protocol parsing, like HTTP, depend on its parsing func.
|
||||
// For the binary procols wait for message to expire
|
||||
if parser.End != nil {
|
||||
if parser.End(m) {
|
||||
parser.Emit(m)
|
||||
return true
|
||||
}
|
||||
|
||||
parser.Fix100Continue(m)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (parser *MessageParser) Fix100Continue(m *Message) {
|
||||
// Only adjust a message once
|
||||
if state, ok := m.feedback.(*proto.HTTPState); ok && state.Continue100 && !m.continueAdjusted {
|
||||
// Shift Ack by given offset
|
||||
// Size of "HTTP/1.1 100 Continue\r\n\r\n" message
|
||||
for _, p := range m.packets {
|
||||
p.messageID = 0
|
||||
p.Ack += 25
|
||||
}
|
||||
|
||||
// If next section was aready approved and received, merge messages
|
||||
if next, found := parser.m[m.packets[0].MessageID()]; found {
|
||||
for _, p := range next.packets {
|
||||
parser.addPacket(m, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-add (or override) again with new message and ID
|
||||
parser.m[m.packets[0].MessageID()] = m
|
||||
m.continueAdjusted = true
|
||||
}
|
||||
}
|
||||
|
||||
func (parser *MessageParser) Read() *Message {
|
||||
m := <-parser.messages
|
||||
return m
|
||||
}
|
||||
|
||||
func (parser *MessageParser) Emit(m *Message) {
|
||||
stats.Add("message_count", 1)
|
||||
|
||||
delete(parser.m, m.packets[0].MessageID())
|
||||
|
||||
parser.messages <- m
|
||||
}
|
||||
|
||||
func GetUnexportedField(field reflect.Value) interface{} {
|
||||
return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface()
|
||||
}
|
||||
|
||||
var failMsg int
|
||||
|
||||
func (parser *MessageParser) timer(now time.Time) {
|
||||
packetLen = 0
|
||||
|
||||
packetQueueLen.Set(int64(len(parser.packets)))
|
||||
messageQueueLen.Set(int64(len(parser.m)))
|
||||
|
||||
for _, m := range parser.m {
|
||||
if now.Sub(m.End) > parser.messageExpire {
|
||||
m.TimedOut = true
|
||||
stats.Add("message_timeout_count", 1)
|
||||
failMsg++
|
||||
if parser.End == nil || parser.allowIncompete {
|
||||
parser.Emit(m)
|
||||
}
|
||||
|
||||
delete(parser.m, m.packets[0].MessageID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (parser *MessageParser) Close() error {
|
||||
parser.close <- struct{}{}
|
||||
<-parser.close // wait for timer to be closed!
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
)
|
||||
|
||||
func copySlice(to []byte, skip int, from ...[]byte) ([]byte, int) {
|
||||
var totalLen int
|
||||
for _, s := range from {
|
||||
totalLen += len(s)
|
||||
}
|
||||
totalLen += skip
|
||||
|
||||
if len(to) < totalLen {
|
||||
diff := totalLen - len(to)
|
||||
to = append(to, make([]byte, diff)...)
|
||||
}
|
||||
|
||||
for _, s := range from {
|
||||
skip += copy(to[skip:], s)
|
||||
}
|
||||
|
||||
return to, skip
|
||||
}
|
||||
|
||||
var stats *expvar.Map
|
||||
var packetQueueLen, messageQueueLen *expvar.Int
|
||||
|
||||
func init() {
|
||||
packetQueueLen = new(expvar.Int)
|
||||
messageQueueLen = new(expvar.Int)
|
||||
|
||||
stats = expvar.NewMap("tcp")
|
||||
stats.Init()
|
||||
stats.Set("packet_queue", packetQueueLen)
|
||||
stats.Set("message_queue", messageQueueLen)
|
||||
}
|
||||
|
||||
type Dir int
|
||||
|
||||
const (
|
||||
DirUnknown = iota
|
||||
DirIncoming
|
||||
DirOutcoming
|
||||
)
|
||||
|
||||
/*
|
||||
Packet represent data and layers of packet.
|
||||
parser extracts information from pcap Packet. functions of *Packet doesn't validate if packet is nil,
|
||||
calllers must make sure that ParsePacket has'nt returned any error before calling any other
|
||||
function.
|
||||
*/
|
||||
type Packet struct {
|
||||
Direction Dir
|
||||
messageID uint64
|
||||
SrcIP, DstIP net.IP
|
||||
Version uint8
|
||||
SrcPort, DstPort uint16
|
||||
Ack, Seq uint32
|
||||
ACK, SYN, FIN, RST bool
|
||||
Lost uint32
|
||||
Retry int
|
||||
CaptureLength int
|
||||
Timestamp time.Time
|
||||
Payload []byte
|
||||
buf []byte
|
||||
|
||||
created time.Time
|
||||
gc bool
|
||||
}
|
||||
|
||||
type PcapPacket struct {
|
||||
Data []byte
|
||||
LType int
|
||||
LTypeLen int
|
||||
Ci *gopacket.CaptureInfo
|
||||
}
|
||||
|
||||
// ParsePacket parse raw packets
|
||||
func ParsePacket(data []byte, lType, lTypeLen int, ci *gopacket.CaptureInfo, allowEmpty bool) (pckt *Packet, err error) {
|
||||
pckt = new(Packet)
|
||||
if err := pckt.parse(data, lType, lTypeLen, ci, allowEmpty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pckt, nil
|
||||
}
|
||||
|
||||
func (pckt *Packet) parse(data []byte, lType, lTypeLen int, cp *gopacket.CaptureInfo, allowEmpty bool) error {
|
||||
pckt.Retry = 0
|
||||
pckt.messageID = 0
|
||||
pckt.buf = pckt.buf[:]
|
||||
|
||||
// TODO: check resolution
|
||||
pckt.Timestamp = cp.Timestamp
|
||||
|
||||
if len(data) < lTypeLen {
|
||||
return ErrHdrLength("Link")
|
||||
}
|
||||
if len(data) <= lTypeLen {
|
||||
return ErrHdrMissing("IPv4 or IPv6")
|
||||
}
|
||||
|
||||
ldata := data[lTypeLen:]
|
||||
var proto byte
|
||||
var netLayer, transLayer []byte
|
||||
|
||||
if ldata[0]>>4 == 4 {
|
||||
// IPv4 header
|
||||
if len(ldata) < 20 {
|
||||
return ErrHdrLength("IPv4")
|
||||
}
|
||||
proto = ldata[9]
|
||||
ihl := int(ldata[0]&0x0F) * 4
|
||||
if ihl < 20 {
|
||||
return ErrHdrInvalid("IPv4's IHL")
|
||||
}
|
||||
if len(ldata) < ihl {
|
||||
return ErrHdrLength("IPv4 opts")
|
||||
}
|
||||
netLayer = ldata[:ihl]
|
||||
} else if ldata[0]>>4 == 6 {
|
||||
if len(ldata) < 40 {
|
||||
return ErrHdrLength("IPv6")
|
||||
}
|
||||
proto = ldata[6]
|
||||
totalLen := 40
|
||||
for ipv6ExtensionHdr(proto) {
|
||||
hdr := len(ldata) - totalLen
|
||||
if hdr < 8 {
|
||||
return ErrHdrExpected("IPv6 opts")
|
||||
}
|
||||
extLen := 8
|
||||
if proto != 44 {
|
||||
extLen = int(ldata[totalLen+1]+1) * 8
|
||||
}
|
||||
if hdr < extLen {
|
||||
return ErrHdrLength("IPv6 opts")
|
||||
}
|
||||
proto = ldata[totalLen]
|
||||
totalLen += extLen
|
||||
}
|
||||
netLayer = ldata[:totalLen]
|
||||
} else {
|
||||
return ErrHdrExpected("IPv4 or IPv6")
|
||||
}
|
||||
if proto != 6 {
|
||||
return ErrHdrExpected("TCP")
|
||||
}
|
||||
if len(data) <= len(netLayer) {
|
||||
return ErrHdrMissing("TCP")
|
||||
}
|
||||
ndata := ldata[len(netLayer):]
|
||||
// TCP header
|
||||
if len(ndata) < 20 {
|
||||
return ErrHdrLength("TCP")
|
||||
}
|
||||
dOf := int(ndata[12]>>4) * 4
|
||||
if dOf < 20 {
|
||||
return ErrHdrInvalid("TCP's ndata offset")
|
||||
}
|
||||
if len(ndata) < dOf {
|
||||
return ErrHdrLength("TCP opts")
|
||||
}
|
||||
|
||||
// There are case when packet have padding but dOf shows its not
|
||||
empty := true
|
||||
for i := 0; i < len(ndata[dOf:]); i++ {
|
||||
if ndata[dOf:][i] != 0 {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowEmpty && empty {
|
||||
return EmptyPacket("")
|
||||
}
|
||||
|
||||
if (netLayer[0] >> 4) == 4 {
|
||||
// IPv4 header
|
||||
pckt.Version = 4
|
||||
pckt.SrcIP = netLayer[12:16]
|
||||
pckt.DstIP = netLayer[16:20]
|
||||
} else {
|
||||
// IPv6 header
|
||||
pckt.Version = 6
|
||||
pckt.SrcIP = netLayer[8:24]
|
||||
pckt.DstIP = netLayer[24:40]
|
||||
}
|
||||
|
||||
transLayer = ndata[:dOf]
|
||||
|
||||
pckt.CaptureLength = cp.CaptureLength
|
||||
pckt.SrcPort = binary.BigEndian.Uint16(transLayer[0:2])
|
||||
pckt.DstPort = binary.BigEndian.Uint16(transLayer[2:4])
|
||||
pckt.Seq = binary.BigEndian.Uint32(transLayer[4:8])
|
||||
pckt.Ack = binary.BigEndian.Uint32(transLayer[8:12])
|
||||
pckt.FIN = transLayer[13]&0x01 != 0
|
||||
pckt.SYN = transLayer[13]&0x02 != 0
|
||||
pckt.RST = transLayer[13]&0x04 != 0
|
||||
pckt.ACK = transLayer[13]&0x10 != 0
|
||||
pckt.Lost = uint32(cp.Length - cp.CaptureLength)
|
||||
|
||||
pckt.Payload = ndata[dOf:]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pckt *Packet) MessageID() uint64 {
|
||||
if pckt.messageID == 0 {
|
||||
// All packets in the same message will share the same ID
|
||||
pckt.messageID = uint64(pckt.SrcPort)<<48 | uint64(pckt.DstPort)<<32 |
|
||||
(uint64(ip2int(pckt.SrcIP)) + uint64(ip2int(pckt.DstIP)) + uint64(pckt.Ack))
|
||||
}
|
||||
|
||||
return pckt.messageID
|
||||
}
|
||||
|
||||
// Src returns the source socket of a packet
|
||||
func (pckt *Packet) Src() string {
|
||||
return fmt.Sprintf("%s:%d", pckt.SrcIP, pckt.SrcPort)
|
||||
}
|
||||
|
||||
// Dst returns destination socket
|
||||
func (pckt *Packet) Dst() string {
|
||||
return fmt.Sprintf("%s:%d", pckt.DstIP, pckt.DstPort)
|
||||
}
|
||||
|
||||
type EmptyPacket string
|
||||
|
||||
func (err EmptyPacket) Error() string {
|
||||
return "Empty packet"
|
||||
}
|
||||
|
||||
// ErrHdrLength returned on short header length
|
||||
type ErrHdrLength string
|
||||
|
||||
func (err ErrHdrLength) Error() string {
|
||||
return "short " + string(err) + " length"
|
||||
}
|
||||
|
||||
// ErrHdrMissing returned on missing header(s)
|
||||
type ErrHdrMissing string
|
||||
|
||||
func (err ErrHdrMissing) Error() string {
|
||||
return "missing " + string(err) + " header(s)"
|
||||
}
|
||||
|
||||
// ErrHdrExpected returned when header(s) are different from the one expected
|
||||
type ErrHdrExpected string
|
||||
|
||||
func (err ErrHdrExpected) Error() string {
|
||||
return "expected " + string(err) + " header(s)"
|
||||
}
|
||||
|
||||
// ErrHdrInvalid returned when header(s) are different from the one expected
|
||||
type ErrHdrInvalid string
|
||||
|
||||
func (err ErrHdrInvalid) Error() string {
|
||||
return "invalid " + string(err) + " value"
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/IPv6_packet#Extension_headers
|
||||
func ipv6ExtensionHdr(b byte) bool {
|
||||
// TODO: support all extension headers
|
||||
return b == 0 || b == 43 || b == 44
|
||||
}
|
||||
|
||||
func ip2int(ip net.IP) uint32 {
|
||||
if len(ip) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
if len(ip) == 16 {
|
||||
return binary.BigEndian.Uint32(ip[12:16])
|
||||
}
|
||||
return binary.BigEndian.Uint32(ip)
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"github.com/buger/goreplay/proto"
|
||||
|
||||
// "runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
func generateHeader(request bool, seq uint32, length uint16) []byte {
|
||||
hdr := make([]byte, 4+24+24)
|
||||
binary.BigEndian.PutUint32(hdr, uint32(layers.ProtocolFamilyIPv4))
|
||||
|
||||
ip := hdr[4:]
|
||||
ip[0] = 4<<4 | 6
|
||||
binary.BigEndian.PutUint16(ip[2:4], length+24+24)
|
||||
ip[9] = uint8(layers.IPProtocolTCP)
|
||||
copy(ip[12:16], []byte{127, 0, 0, 1})
|
||||
copy(ip[16:], []byte{127, 0, 0, 1})
|
||||
|
||||
// set tcp header
|
||||
tcp := ip[24:]
|
||||
tcp[12] = 6 << 4
|
||||
|
||||
if request {
|
||||
binary.BigEndian.PutUint16(tcp, 5535)
|
||||
binary.BigEndian.PutUint16(tcp[2:], 8000)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(tcp, 8000)
|
||||
binary.BigEndian.PutUint16(tcp[2:], 5535)
|
||||
}
|
||||
binary.BigEndian.PutUint32(tcp[4:], seq)
|
||||
return hdr
|
||||
}
|
||||
|
||||
func GetPackets(request bool, start uint32, _len int, payload []byte) []*Packet {
|
||||
var packets = make([]*Packet, _len)
|
||||
var err error
|
||||
for i := start; i < start+uint32(_len); i++ {
|
||||
d := append(generateHeader(request, i, uint16(len(payload))), payload...)
|
||||
ci := &gopacket.CaptureInfo{Length: len(d), CaptureLength: len(d), Timestamp: time.Now()}
|
||||
|
||||
packets[i-start], err = ParsePacket(d, int(layers.LinkTypeLoop), 4, ci, true)
|
||||
if request {
|
||||
packets[i-start].Direction = DirIncoming
|
||||
} else {
|
||||
packets[i-start].Direction = DirOutcoming
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return packets
|
||||
}
|
||||
|
||||
func TestRequestResponseMapping(t *testing.T) {
|
||||
packets := []*Packet{
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 1, Seq: 1, Direction: DirIncoming, Timestamp: time.Unix(1, 0), Payload: []byte("GET / HTTP/1.1\r\n")},
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 1, Seq: 17, Direction: DirIncoming, Timestamp: time.Unix(2, 0), Payload: []byte("Host: localhost\r\n\r\n")},
|
||||
|
||||
// Seq of first response packet match Ack of first request packet
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 36, Seq: 1, Direction: DirOutcoming, Timestamp: time.Unix(3, 0), Payload: []byte("HTTP/1.1 200 OK\r\n")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 36, Seq: 18, Direction: DirOutcoming, Timestamp: time.Unix(4, 0), Payload: []byte("Content-Length: 0\r\n\r\n")},
|
||||
|
||||
// Same TCP stream
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 39, Seq: 36, Direction: DirIncoming, Timestamp: time.Unix(5, 0), Payload: []byte("GET / HTTP/1.1\r\n")},
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 39, Seq: 52, Direction: DirIncoming, Timestamp: time.Unix(6, 0), Payload: []byte("Host: localhost\r\n\r\n")},
|
||||
|
||||
// Seq of first response packet match Ack of first request packet
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 71, Seq: 39, Direction: DirOutcoming, Timestamp: time.Unix(7, 0), Payload: []byte("HTTP/1.1 200 OK\r\n")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 71, Seq: 56, Direction: DirOutcoming, Timestamp: time.Unix(8, 0), Payload: []byte("Content-Length: 0\r\n\r\n")},
|
||||
}
|
||||
|
||||
parser := NewMessageParser(nil, nil, nil, time.Second, false)
|
||||
parser.Start = func(pckt *Packet) (bool, bool) {
|
||||
return proto.HasRequestTitle(pckt.Payload), proto.HasResponseTitle(pckt.Payload)
|
||||
}
|
||||
parser.End = func(m *Message) bool {
|
||||
return proto.HasFullPayload(m, m.PacketData()...)
|
||||
}
|
||||
|
||||
for _, packet := range packets {
|
||||
parser.processPacket(packet)
|
||||
}
|
||||
|
||||
messages := []*Message{}
|
||||
for i := 0; i < 4; i++ {
|
||||
m := parser.Read()
|
||||
messages = append(messages, m)
|
||||
}
|
||||
|
||||
assert.Equal(t, int(messages[0].Direction), int(DirIncoming))
|
||||
assert.Equal(t, int(messages[1].Direction), int(DirOutcoming))
|
||||
assert.Equal(t, int(messages[2].Direction), int(DirIncoming))
|
||||
assert.Equal(t, int(messages[3].Direction), int(DirOutcoming))
|
||||
|
||||
assert.Equal(t, messages[0].UUID(), messages[1].UUID())
|
||||
assert.Equal(t, messages[2].UUID(), messages[3].UUID())
|
||||
|
||||
assert.NotEqual(t, messages[0].UUID(), messages[2].UUID())
|
||||
}
|
||||
|
||||
func TestMessageParserWithHint(t *testing.T) {
|
||||
parser := NewMessageParser(nil, nil, nil, time.Second, false)
|
||||
parser.Start = func(pckt *Packet) (bool, bool) {
|
||||
return proto.HasRequestTitle(pckt.Payload), proto.HasResponseTitle(pckt.Payload)
|
||||
}
|
||||
parser.End = func(m *Message) bool {
|
||||
return proto.HasFullPayload(m, m.PacketData()...)
|
||||
}
|
||||
|
||||
packets := []*Packet{
|
||||
// Seq of first response packet match Ack of first request packet
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 1, Direction: DirOutcoming, Timestamp: time.Unix(1, 0), Payload: []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n7\r\n")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 18, Direction: DirOutcoming, Timestamp: time.Unix(2, 0), Payload: []byte("\r\nMozilla\r\n9\r\nDeveloper\r")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 42, Direction: DirOutcoming, Timestamp: time.Unix(3, 0), Payload: []byte("\n7\r\nNetwork\r\n0\r\n\r\n")},
|
||||
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 1, Direction: DirIncoming, Timestamp: time.Unix(4, 0), Payload: []byte("POST / HTTP/1.1\r\nContent-Type: text/plain\r\nContent-Length: 23\r\n\r\n")},
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 66, Direction: DirIncoming, Timestamp: time.Unix(5, 0), Payload: []byte("MozillaDeveloper")},
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 82, Direction: DirIncoming, Timestamp: time.Unix(6, 0), Payload: []byte("Network")},
|
||||
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 89, Seq: 1, Direction: DirOutcoming, Timestamp: time.Unix(7, 0), Payload: []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n")},
|
||||
}
|
||||
|
||||
for _, p := range packets {
|
||||
parser.processPacket(p)
|
||||
}
|
||||
|
||||
messages := []*Message{}
|
||||
for i := 0; i < 3; i++ {
|
||||
m := parser.Read()
|
||||
messages = append(messages, m)
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(messages[0].Data(), []byte("\n7\r\nNetwork\r\n0\r\n\r\n")) {
|
||||
t.Errorf("expected to %q to have suffix %q", messages[0].Data(), []byte("\n7\r\nNetwork\r\n0\r\n\r\n"))
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(messages[1].Data(), []byte("Network")) {
|
||||
t.Errorf("expected to %q to have suffix %q", messages[1].Data(), []byte("Network"))
|
||||
}
|
||||
|
||||
if !bytes.HasSuffix(messages[2].Data(), []byte("Content-Length: 0\r\n\r\n")) {
|
||||
t.Errorf("expected to %q to have suffix %q", messages[2].Data(), []byte("Content-Length: 0\r\n\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageParserWrongOrder(t *testing.T) {
|
||||
parser := NewMessageParser(nil, nil, nil, time.Second, false)
|
||||
parser.Start = func(pckt *Packet) (bool, bool) {
|
||||
return proto.HasRequestTitle(pckt.Payload), proto.HasResponseTitle(pckt.Payload)
|
||||
}
|
||||
parser.End = func(m *Message) bool {
|
||||
return proto.HasFullPayload(m, m.PacketData()...)
|
||||
}
|
||||
packets := []*Packet{
|
||||
// Seq of first response packet match Ack of first request packet
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 66, Direction: DirIncoming, Timestamp: time.Unix(5, 0), Payload: []byte("MozillaDeveloper")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 1, Direction: DirOutcoming, Timestamp: time.Unix(1, 0), Payload: []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n7\r\n")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 42, Direction: DirOutcoming, Timestamp: time.Unix(3, 0), Payload: []byte("\n7\r\nNetwork\r\n0\r\n\r\n")},
|
||||
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 1, Direction: DirIncoming, Timestamp: time.Unix(4, 0), Payload: []byte("POST / HTTP/1.1\r\nContent-Type: text/plain\r\nContent-Length: 23\r\n\r\n")},
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 1, Seq: 18, Direction: DirOutcoming, Timestamp: time.Unix(2, 0), Payload: []byte("\r\nMozilla\r\n9\r\nDeveloper\r")},
|
||||
|
||||
{SrcPort: 80, DstPort: 60000, Ack: 89, Seq: 1, Direction: DirOutcoming, Timestamp: time.Unix(7, 0), Payload: []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 0\r\n\r\n")},
|
||||
{SrcPort: 60000, DstPort: 80, Ack: 60, Seq: 82, Direction: DirIncoming, Timestamp: time.Unix(6, 0), Payload: []byte("Network")},
|
||||
}
|
||||
|
||||
for _, p := range packets {
|
||||
parser.processPacket(p)
|
||||
}
|
||||
|
||||
m := parser.Read()
|
||||
|
||||
if !bytes.HasSuffix(m.Data(), []byte("\n7\r\nNetwork\r\n0\r\n\r\n")) {
|
||||
t.Errorf("expected to %q to have suffix %q", m.Data(), []byte("\n7\r\nNetwork\r\n0\r\n\r\n"))
|
||||
}
|
||||
|
||||
m = parser.Read()
|
||||
|
||||
if !bytes.HasSuffix(m.Data(), []byte("Content-Length: 0\r\n\r\n")) {
|
||||
t.Errorf("expected to %q to have suffix %q", m.Data(), []byte("Content-Length: 0\r\n\r\n"))
|
||||
}
|
||||
|
||||
m = parser.Read()
|
||||
|
||||
if !bytes.HasSuffix(m.Data(), []byte("Network")) {
|
||||
t.Errorf("expected to %q to have suffix %q", m.Data(), []byte("Network"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageParserWithoutHint(t *testing.T) {
|
||||
var data [63 << 10]byte
|
||||
packets := GetPackets(true, 1, 10, data[:])
|
||||
|
||||
p := NewMessageParser(nil, nil, nil, time.Second, false)
|
||||
for _, v := range packets {
|
||||
p.processPacket(v)
|
||||
}
|
||||
m := p.Read()
|
||||
|
||||
if m.Length != 63<<10*10 {
|
||||
t.Errorf("expected %d to equal %d", m.Length, 63<<10*10)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageTimeoutReached(t *testing.T) {
|
||||
const size = 63 << 11
|
||||
var data [size >> 1]byte
|
||||
packets := GetPackets(true, 1, 2, data[:])
|
||||
p := NewMessageParser(nil, nil, nil, 100*time.Millisecond, true)
|
||||
p.processPacket(packets[0])
|
||||
|
||||
time.Sleep(time.Millisecond * 20)
|
||||
|
||||
p.processPacket(packets[1])
|
||||
m := p.Read()
|
||||
if m.Length != size {
|
||||
t.Errorf("expected %d to equal %d", m.Length, size)
|
||||
}
|
||||
if !m.TimedOut {
|
||||
t.Error("expected message to be timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMessageUUID(b *testing.B) {
|
||||
packets := GetPackets(true, 1, 5, nil)
|
||||
|
||||
var uuid []byte
|
||||
parser := NewMessageParser(nil, nil, nil, 10*time.Millisecond, true)
|
||||
for _, p := range packets {
|
||||
parser.processPacket(p)
|
||||
}
|
||||
|
||||
msg := parser.Read()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
uuid = msg.UUID()
|
||||
}
|
||||
_ = uuid
|
||||
}
|
||||
|
||||
func BenchmarkPacketParseAndSort(b *testing.B) {
|
||||
m := new(Message)
|
||||
m.packets = make([]*Packet, 100)
|
||||
for i, v := range GetPackets(true, 1, 100, nil) {
|
||||
m.packets[i] = v
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Sort()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMessageParserWithoutHint(b *testing.B) {
|
||||
var chunk = []byte("111111111111111111111111111111")
|
||||
packets := GetPackets(true, 1, 1000, chunk)
|
||||
p := NewMessageParser(nil, nil, nil, 2*time.Second, false)
|
||||
b.ResetTimer()
|
||||
b.ReportMetric(float64(1000), "packets/op")
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range packets {
|
||||
p.processPacket(v)
|
||||
}
|
||||
p.Read()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMessageParserWithHint(b *testing.B) {
|
||||
var buf [1002][]byte
|
||||
var chunk = []byte("1e\r\n111111111111111111111111111111\r\n")
|
||||
buf[0] = []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n")
|
||||
for i := 1; i < 1000; i++ {
|
||||
buf[i] = chunk
|
||||
}
|
||||
buf[1001] = []byte("0\r\n\r\n")
|
||||
packets := make([]*Packet, len(buf))
|
||||
for i := 0; i < len(buf); i++ {
|
||||
packets[i] = GetPackets(false, 1, 1, buf[i])[0]
|
||||
}
|
||||
|
||||
parser := NewMessageParser(nil, nil, nil, 2*time.Second, false)
|
||||
parser.Start = func(pckt *Packet) (bool, bool) {
|
||||
return false, proto.HasResponseTitle(pckt.Payload)
|
||||
}
|
||||
parser.End = func(m *Message) bool {
|
||||
return proto.HasFullPayload(m, m.PacketData()...)
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportMetric(float64(len(packets)), "packets/op")
|
||||
b.ReportMetric(float64(1000), "chunks/op")
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := range packets {
|
||||
parser.processPacket(packets[j])
|
||||
}
|
||||
parser.Read()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewAndParsePacket(b *testing.B) {
|
||||
data := append(generateHeader(true, 1024, 10), make([]byte, 10)...)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParsePacket(data, int(layers.LinkTypeLoop), 4, &gopacket.CaptureInfo{}, true)
|
||||
}
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Native k8s integration
|
||||
|
||||
After following steps below, you will be able to capture traffic inside k8s like this:
|
||||
|
||||
```
|
||||
gor --input-raw k8s://namespace/deployment/app:80 --output-http http://replay.com
|
||||
```
|
||||
|
||||
GoReplay will running as a daemonset (e.g. on each phisical k8s node.
|
||||
It will also require giving required permission to have read access to K8s APIs, so it can dynamically filter traffic for a specific pods.
|
||||
|
||||
Supported format for filtering required pods:
|
||||
|
||||
```
|
||||
k8s://[namespace/]pod/[pod_name] - k8s://default/pod/nginx-7848d4b86f-5nxz8
|
||||
k8s://[namespace/]deployment/[deployment_name] - k8s://default/deployment/nginx
|
||||
k8s://[namespace/]daemonset/[daemonset_name] - k8s://default/daemonset/nginx
|
||||
k8s://[namespace/]labelSelector/[selector] - k8s://default/labelSelector/app=nginx
|
||||
k8s://[namespace/]fieldSelector/[selector] - k8s://default/fieldSelector/metadata.name=nginx-7848d4b86f-5nxz8
|
||||
```
|
||||
`namespace` is optional, omit to use all namespaces: `k8s://labelSelector/app=replay`
|
||||
|
||||
## 1. Create a namespace
|
||||
`kubectl create namespace goreplay`
|
||||
|
||||
## 2. Create the Kubernetes service account in the namespace:
|
||||
|
||||
`kubectl create serviceaccount goreplay --namespace goreplay`
|
||||
|
||||
## 3. Create Cluster Role which gives read-only access to the pods:
|
||||
`kubectl -n goreplay -f clusterrole.yaml apply`
|
||||
|
||||
```yaml
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: pod-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["daemonset"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
```
|
||||
|
||||
## 4. Attach role to goreplay service account
|
||||
`kubectl -n goreplay -f rolebinding.yaml apply`
|
||||
|
||||
```yaml
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: goreplay-reader-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: goreplay
|
||||
namespace: goreplay
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-reader
|
||||
apiGroup: ""
|
||||
```
|
||||
|
||||
## 5. Start goreplay daemonset
|
||||
|
||||
`kubectl -n goreplay -f goreplay.yaml apply`
|
||||
|
||||
In arguments, specify which service you want to capture.
|
||||
Following format supported:
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: goreplay-daemon
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
hostNetwork: true
|
||||
serviceAccountName: goreplay
|
||||
containers:
|
||||
- name: goreplay
|
||||
image: buger/goreplay:2.0.0-rc2
|
||||
command:
|
||||
- "--input-raw k8s://deployments/nginx:80"
|
||||
- "--output-stdout"
|
||||
```
|
||||
|
||||
## 6. Create a simple http service (Optionally)
|
||||
|
||||
`kubectl -n default -f nginx.yaml apply`
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ngnix-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: NodePort
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
```
|
||||
|
||||
|
||||
## 7. Verify installation
|
||||
|
||||
Find url for your service using `kubectl get svc` or `minikube service --url ngnix-service -n http`, and make a call to it.
|
||||
|
||||
Get GoReplay logs, and check if it capture traffic of your service.
|
||||
`kubectl logs -n goreplay -l name=goreplay-daemon --all-containers`
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: pod-reader
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["daemonsets"]
|
||||
verbs: ["get", "watch", "list"]
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: goreplay-daemon
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
hostNetwork: true
|
||||
serviceAccountName: goreplay
|
||||
containers:
|
||||
- name: goreplay
|
||||
image: buger/goreplay:2.0.0-rc2
|
||||
command:
|
||||
- "--input-raw k8s://deployments/nginx:80"
|
||||
- "--output-stdout"
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: ngnix-service
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: NodePort
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: goreplay-reader-binding
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: goreplay
|
||||
namespace: goreplay
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: pod-reader
|
||||
apiGroup: ""
|
||||
@@ -1,35 +1,46 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"github.com/xdg-go/scram"
|
||||
)
|
||||
|
||||
// SASLKafkaConfig SASL configuration
|
||||
type SASLKafkaConfig struct {
|
||||
UseSASL bool `json:"input-kafka-use-sasl"`
|
||||
Mechanism string `json:"input-kafka-mechanism"`
|
||||
Username string `json:"input-kafka-username"`
|
||||
Password string `json:"input-kafka-password"`
|
||||
}
|
||||
|
||||
// InputKafkaConfig should contains required information to
|
||||
// build producers.
|
||||
type InputKafkaConfig struct {
|
||||
producer sarama.AsyncProducer
|
||||
consumer sarama.Consumer
|
||||
Host string `json:"input-kafka-host"`
|
||||
Topic string `json:"input-kafka-topic"`
|
||||
UseJSON bool `json:"input-kafka-json-format"`
|
||||
consumer sarama.Consumer
|
||||
Host string `json:"input-kafka-host"`
|
||||
Topic string `json:"input-kafka-topic"`
|
||||
UseJSON bool `json:"input-kafka-json-format"`
|
||||
SASLConfig SASLKafkaConfig
|
||||
}
|
||||
|
||||
// OutputKafkaConfig is the representation of kfka output configuration
|
||||
type OutputKafkaConfig struct {
|
||||
producer sarama.AsyncProducer
|
||||
consumer sarama.Consumer
|
||||
Host string `json:"output-kafka-host"`
|
||||
Topic string `json:"output-kafka-topic"`
|
||||
UseJSON bool `json:"output-kafka-json-format"`
|
||||
producer sarama.AsyncProducer
|
||||
Host string `json:"output-kafka-host"`
|
||||
Topic string `json:"output-kafka-topic"`
|
||||
UseJSON bool `json:"output-kafka-json-format"`
|
||||
SASLConfig SASLKafkaConfig
|
||||
}
|
||||
|
||||
// KafkaTLSConfig should contains TLS certificates for connecting to secured Kafka clusters
|
||||
@@ -51,7 +62,6 @@ type KafkaMessage struct {
|
||||
ReqHeaders map[string]string `json:"Req_Headers,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
// NewTLSConfig loads TLS certificates
|
||||
func NewTLSConfig(clientCertFile, clientKeyFile, caCertFile string) (*tls.Config, error) {
|
||||
tlsConfig := tls.Config{}
|
||||
@@ -63,7 +73,7 @@ func NewTLSConfig(clientCertFile, clientKeyFile, caCertFile string) (*tls.Config
|
||||
return &tlsConfig, errors.New("missing TLS client certificate in kafka")
|
||||
}
|
||||
// Load client cert
|
||||
if (clientCertFile != "") && (clientKeyFile != "") {
|
||||
if (clientCertFile != "") && (clientKeyFile != "") {
|
||||
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
|
||||
if err != nil {
|
||||
return &tlsConfig, err
|
||||
@@ -84,7 +94,7 @@ func NewTLSConfig(clientCertFile, clientKeyFile, caCertFile string) (*tls.Config
|
||||
}
|
||||
|
||||
// NewKafkaConfig returns Kafka config with or without TLS
|
||||
func NewKafkaConfig(tlsConfig *KafkaTLSConfig) *sarama.Config {
|
||||
func NewKafkaConfig(saslConfig *SASLKafkaConfig, tlsConfig *KafkaTLSConfig) *sarama.Config {
|
||||
config := sarama.NewConfig()
|
||||
// Configuration options go here
|
||||
if tlsConfig != nil && (tlsConfig.ClientCert != "" || tlsConfig.CACert != "") {
|
||||
@@ -95,6 +105,18 @@ func NewKafkaConfig(tlsConfig *KafkaTLSConfig) *sarama.Config {
|
||||
}
|
||||
config.Net.TLS.Config = tlsConfig
|
||||
}
|
||||
if saslConfig.UseSASL {
|
||||
mechanism := sarama.SASLMechanism(saslConfig.Mechanism)
|
||||
config.Net.SASL.Enable = saslConfig.UseSASL
|
||||
config.Net.SASL.Mechanism = mechanism
|
||||
config.Net.SASL.User = saslConfig.Username
|
||||
config.Net.SASL.Password = saslConfig.Password
|
||||
if mechanism == sarama.SASLTypeSCRAMSHA256 {
|
||||
config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA256} }
|
||||
} else if mechanism == sarama.SASLTypeSCRAMSHA512 {
|
||||
config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA512} }
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -116,3 +138,38 @@ func (m KafkaMessage) Dump() ([]byte, error) {
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
var (
|
||||
// SHA256 SASLMechanism
|
||||
SHA256 scram.HashGeneratorFcn = sha256.New
|
||||
// SHA512 SASLMechanism
|
||||
SHA512 scram.HashGeneratorFcn = sha512.New
|
||||
)
|
||||
|
||||
// XDGSCRAMClient for SASL-Protocol
|
||||
type XDGSCRAMClient struct {
|
||||
*scram.Client
|
||||
*scram.ClientConversation
|
||||
scram.HashGeneratorFcn
|
||||
}
|
||||
|
||||
// Begin of XDGSCRAMClient
|
||||
func (x *XDGSCRAMClient) Begin(userName, password, authzID string) (err error) {
|
||||
x.Client, err = x.HashGeneratorFcn.NewClient(userName, password, authzID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
x.ClientConversation = x.Client.NewConversation()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Step of XDGSCRAMClient
|
||||
func (x *XDGSCRAMClient) Step(challenge string) (response string, err error) {
|
||||
response, err = x.ClientConversation.Step(challenge)
|
||||
return
|
||||
}
|
||||
|
||||
// Done of XDGSCRAMClient
|
||||
func (x *XDGSCRAMClient) Done() bool {
|
||||
return x.ClientConversation.Done()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
// +build !race
|
||||
//go:build !race
|
||||
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
+65
-67
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/goreplay/proto"
|
||||
)
|
||||
|
||||
const echoSh = "./examples/middleware/echo.sh"
|
||||
const tokenModifier = "go run ./examples/middleware/token_modifier.go"
|
||||
|
||||
var noDebug = append(syscall.Environ(), "GOR_TEST=1")
|
||||
var withDebug = append(syscall.Environ(), "GOR_TEST=1")
|
||||
|
||||
func initMiddleware(cmd *exec.Cmd, cancl context.CancelFunc, l PluginReader, c func(error)) *Middleware {
|
||||
var m Middleware
|
||||
@@ -52,7 +50,7 @@ func initCmd(command string, env []string) (*exec.Cmd, context.CancelFunc) {
|
||||
func TestMiddlewareEarlyClose(t *testing.T) {
|
||||
quit := make(chan struct{})
|
||||
in := NewTestInput()
|
||||
cmd, cancl := initCmd(echoSh, noDebug)
|
||||
cmd, cancl := initCmd(echoSh, withDebug)
|
||||
midd := initMiddleware(cmd, cancl, in, func(err error) {
|
||||
if err != nil {
|
||||
if e, ok := err.(*exec.ExitError); ok {
|
||||
@@ -89,66 +87,66 @@ func TestMiddlewareEarlyClose(t *testing.T) {
|
||||
<-quit
|
||||
}
|
||||
|
||||
func TestTokenMiddleware(t *testing.T) {
|
||||
quit := make(chan struct{})
|
||||
in := NewTestInput()
|
||||
in.skipHeader = true
|
||||
cmd, cancl := initCmd(tokenModifier, noDebug)
|
||||
midd := initMiddleware(cmd, cancl, in, func(err error) {})
|
||||
req := []byte("1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nGET /token HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
||||
res := []byte("2 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nHTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n17d823647c")
|
||||
rep := []byte("3 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nHTTP/1.1 200 OK\r\nContent-Length: 15\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n932079936fa4306")
|
||||
count := uint32(0)
|
||||
out := NewTestOutput(func(msg *Message) {
|
||||
if msg.Meta[0] == '1' && !bytes.Equal(payloadID(msg.Meta), payloadID(req)) {
|
||||
token, _, _ := proto.PathParam(msg.Data, []byte("token"))
|
||||
if !bytes.Equal(token, proto.Body(rep)) {
|
||||
t.Error("expected the token to be equal to the replayed responses's token")
|
||||
}
|
||||
}
|
||||
atomic.AddUint32(&count, 1)
|
||||
if atomic.LoadUint32(&count) == 2 {
|
||||
quit <- struct{}{}
|
||||
}
|
||||
})
|
||||
pl := &InOutPlugins{}
|
||||
pl.Inputs = []PluginReader{midd, in}
|
||||
pl.Outputs = []PluginWriter{out}
|
||||
pl.All = []interface{}{midd, out, in}
|
||||
e := NewEmitter()
|
||||
go e.Start(pl, "")
|
||||
in.EmitBytes(req) // emit original request
|
||||
in.EmitBytes(res) // emit its response
|
||||
in.EmitBytes(rep) // emit replayed response
|
||||
// emit the request which should have modified token
|
||||
token := []byte("1 8e091765ae902fef8a2b7d9dd96 14398188235873 100\nGET /?token=17d823647c HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
||||
in.EmitBytes(token)
|
||||
<-quit
|
||||
midd.Close()
|
||||
}
|
||||
//func TestTokenMiddleware(t *testing.T) {
|
||||
// quit := make(chan struct{})
|
||||
// in := NewTestInput()
|
||||
// in.skipHeader = true
|
||||
// cmd, cancl := initCmd(tokenModifier, withDebug)
|
||||
// midd := initMiddleware(cmd, cancl, in, func(err error) {})
|
||||
// req := []byte("1 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nGET /token HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
||||
// res := []byte("2 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nHTTP/1.1 200 OK\r\nContent-Length: 10\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n17d823647c")
|
||||
// rep := []byte("3 932079936fa4306fc308d67588178d17d823647c 1439818823587396305 200\nHTTP/1.1 200 OK\r\nContent-Length: 15\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n932079936fa4306")
|
||||
// count := uint32(0)
|
||||
// out := NewTestOutput(func(msg *Message) {
|
||||
// if msg.Meta[0] == '1' && !bytes.Equal(payloadID(msg.Meta), payloadID(req)) {
|
||||
// token, _, _ := proto.PathParam(msg.Data, []byte("token"))
|
||||
// if !bytes.Equal(token, proto.Body(rep)) {
|
||||
// t.Errorf("expected the token %s to be equal to the replayed response's token %s", token, proto.Body(rep))
|
||||
// }
|
||||
// }
|
||||
// atomic.AddUint32(&count, 1)
|
||||
// if atomic.LoadUint32(&count) == 2 {
|
||||
// quit <- struct{}{}
|
||||
// }
|
||||
// })
|
||||
// pl := &InOutPlugins{}
|
||||
// pl.Inputs = []PluginReader{midd, in}
|
||||
// pl.Outputs = []PluginWriter{out}
|
||||
// pl.All = []interface{}{midd, out, in}
|
||||
// e := NewEmitter()
|
||||
// go e.Start(pl, "")
|
||||
// in.EmitBytes(req) // emit original request
|
||||
// in.EmitBytes(res) // emit its response
|
||||
// in.EmitBytes(rep) // emit replayed response
|
||||
// // emit the request which should have modified token
|
||||
// token := []byte("1 8e091765ae902fef8a2b7d9dd96 14398188235873 100\nGET /?token=17d823647c HTTP/1.1\r\nHost: example.org\r\n\r\n")
|
||||
// in.EmitBytes(token)
|
||||
// <-quit
|
||||
// midd.Close()
|
||||
//}
|
||||
|
||||
func TestMiddlewareWithPrettify(t *testing.T) {
|
||||
Settings.PrettifyHTTP = true
|
||||
quit := make(chan struct{})
|
||||
in := NewTestInput()
|
||||
cmd, cancl := initCmd(echoSh, noDebug)
|
||||
midd := initMiddleware(cmd, cancl, in, func(err error) {})
|
||||
var b1 = []byte("POST / HTTP/1.1\r\nHost: example.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
var b2 = []byte("POST / HTTP/1.1\r\nHost: example.org\r\nContent-Length: 25\r\n\r\nWikipedia in\r\n\r\nchunks.")
|
||||
out := NewTestOutput(func(msg *Message) {
|
||||
if !bytes.Equal(proto.Body(b2), proto.Body(msg.Data)) {
|
||||
t.Errorf("expected %q body to equal %q body", b2, msg.Data)
|
||||
}
|
||||
quit <- struct{}{}
|
||||
})
|
||||
pl := &InOutPlugins{}
|
||||
pl.Inputs = []PluginReader{midd, in}
|
||||
pl.Outputs = []PluginWriter{out}
|
||||
pl.All = []interface{}{midd, out, in}
|
||||
e := NewEmitter()
|
||||
go e.Start(pl, "")
|
||||
in.EmitBytes(b1)
|
||||
<-quit
|
||||
midd.Close()
|
||||
Settings.PrettifyHTTP = false
|
||||
}
|
||||
//func TestMiddlewareWithPrettify(t *testing.T) {
|
||||
// Settings.PrettifyHTTP = true
|
||||
// quit := make(chan struct{})
|
||||
// in := NewTestInput()
|
||||
// cmd, cancl := initCmd(echoSh, withDebug)
|
||||
// midd := initMiddleware(cmd, cancl, in, func(err error) {})
|
||||
// var b1 = []byte("POST / HTTP/1.1\r\nHost: example.org\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n")
|
||||
// var b2 = []byte("POST / HTTP/1.1\r\nHost: example.org\r\nContent-Length: 25\r\n\r\nWikipedia in\r\n\r\nchunks.")
|
||||
// out := NewTestOutput(func(msg *Message) {
|
||||
// if !bytes.Equal(proto.Body(b2), proto.Body(msg.Data)) {
|
||||
// t.Errorf("expected %q body to equal %q body", b2, msg.Data)
|
||||
// }
|
||||
// quit <- struct{}{}
|
||||
// })
|
||||
// pl := &InOutPlugins{}
|
||||
// pl.Inputs = []PluginReader{midd, in}
|
||||
// pl.Outputs = []PluginWriter{out}
|
||||
// pl.All = []interface{}{midd, out, in}
|
||||
// e := NewEmitter()
|
||||
// go e.Start(pl, "")
|
||||
// in.EmitBytes(b1)
|
||||
// <-quit
|
||||
// midd.Close()
|
||||
// Settings.PrettifyHTTP = false
|
||||
//}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# nfpm example config file
|
||||
#
|
||||
# check https://nfpm.goreleaser.com/configuration for detailed usage
|
||||
#
|
||||
name: "GoReplay"
|
||||
arch: ${PLATFORM}
|
||||
platform: "linux"
|
||||
version: ${VERSION}
|
||||
section: "default"
|
||||
priority: "extra"
|
||||
provides:
|
||||
- goreplay
|
||||
maintainer: "Leonid Bugaev <hello@goreplay.org>"
|
||||
description: |
|
||||
GoReplay is the simplest and safest way to test your app using real traffic before you put it into production.
|
||||
vendor: "GoReplay"
|
||||
homepage: "https://goreplay.org"
|
||||
license: "AGPL"
|
||||
contents:
|
||||
- src: ./${BIN_NAME}
|
||||
dst: /usr/local/bin
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/size"
|
||||
)
|
||||
|
||||
// BinaryOutputConfig struct for holding binary output configuration
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -24,6 +24,7 @@ func (i *DummyOutput) PluginWrite(msg *Message) (int, error) {
|
||||
n += nn
|
||||
nn, err = os.Stdout.Write(payloadSeparatorAsBytes)
|
||||
n += nn
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
|
||||
+35
-18
@@ -1,12 +1,14 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
@@ -15,10 +17,23 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/size"
|
||||
)
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
var instanceID string
|
||||
|
||||
func init() {
|
||||
instanceID = randSeq(8)
|
||||
}
|
||||
|
||||
func randSeq(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var dateFileNameFuncs = map[string]func(*FileOutput) string{
|
||||
"%Y": func(o *FileOutput) string { return time.Now().Format("2006") },
|
||||
"%m": func(o *FileOutput) string { return time.Now().Format("01") },
|
||||
@@ -29,6 +44,7 @@ var dateFileNameFuncs = map[string]func(*FileOutput) string{
|
||||
"%NS": func(o *FileOutput) string { return fmt.Sprint(time.Now().Nanosecond()) },
|
||||
"%r": func(o *FileOutput) string { return string(o.currentID) },
|
||||
"%t": func(o *FileOutput) string { return string(o.payloadType) },
|
||||
"%i": func(o *FileOutput) string { return instanceID },
|
||||
}
|
||||
|
||||
// FileOutputConfig ...
|
||||
@@ -45,17 +61,17 @@ type FileOutputConfig struct {
|
||||
// FileOutput output plugin
|
||||
type FileOutput struct {
|
||||
sync.RWMutex
|
||||
pathTemplate string
|
||||
currentName string
|
||||
file *os.File
|
||||
QueueLength int
|
||||
chunkSize int
|
||||
writer io.Writer
|
||||
requestPerFile bool
|
||||
currentID []byte
|
||||
payloadType []byte
|
||||
closed bool
|
||||
totalFileSize size.Size
|
||||
pathTemplate string
|
||||
currentName string
|
||||
file *os.File
|
||||
QueueLength int
|
||||
writer io.Writer
|
||||
requestPerFile bool
|
||||
currentID []byte
|
||||
payloadType []byte
|
||||
closed bool
|
||||
currentFileSize int
|
||||
totalFileSize size.Size
|
||||
|
||||
config *FileOutputConfig
|
||||
}
|
||||
@@ -65,7 +81,6 @@ func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput {
|
||||
o := new(FileOutput)
|
||||
o.pathTemplate = pathTemplate
|
||||
o.config = config
|
||||
o.updateName()
|
||||
|
||||
if strings.Contains(pathTemplate, "%r") {
|
||||
o.requestPerFile = true
|
||||
@@ -81,7 +96,6 @@ func NewFileOutput(pathTemplate string, config *FileOutputConfig) *FileOutput {
|
||||
if o.IsClosed() {
|
||||
break
|
||||
}
|
||||
o.updateName()
|
||||
o.flush()
|
||||
}
|
||||
}()
|
||||
@@ -157,7 +171,7 @@ func (o *FileOutput) filename() string {
|
||||
|
||||
if o.currentName == "" ||
|
||||
((o.config.QueueLimit > 0 && o.QueueLength >= o.config.QueueLimit) ||
|
||||
(o.config.SizeLimit > 0 && o.chunkSize >= int(o.config.SizeLimit))) {
|
||||
(o.config.SizeLimit > 0 && o.currentFileSize >= int(o.config.SizeLimit))) {
|
||||
nextChunk = true
|
||||
}
|
||||
|
||||
@@ -236,6 +250,7 @@ func (o *FileOutput) PluginWrite(msg *Message) (n int, err error) {
|
||||
n += nn
|
||||
|
||||
o.totalFileSize += size.Size(n)
|
||||
o.currentFileSize += n
|
||||
o.QueueLength++
|
||||
|
||||
if Settings.OutputFileConfig.OutputFileMaxSize > 0 && o.totalFileSize >= Settings.OutputFileConfig.OutputFileMaxSize {
|
||||
@@ -264,7 +279,7 @@ func (o *FileOutput) flush() {
|
||||
}
|
||||
|
||||
if stat, err := o.file.Stat(); err == nil {
|
||||
o.chunkSize = int(stat.Size())
|
||||
o.currentFileSize = int(stat.Size())
|
||||
} else {
|
||||
Debug(0, "[OUTPUT-HTTP] error accessing file size", err)
|
||||
}
|
||||
@@ -290,6 +305,8 @@ func (o *FileOutput) closeLocked() error {
|
||||
}
|
||||
|
||||
o.closed = true
|
||||
o.currentFileSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+3
-5
@@ -1,7 +1,8 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"math/rand"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -10,8 +11,6 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/size"
|
||||
)
|
||||
|
||||
func TestFileOutput(t *testing.T) {
|
||||
@@ -39,7 +38,7 @@ func TestFileOutput(t *testing.T) {
|
||||
emitter.Close()
|
||||
|
||||
var counter int64
|
||||
input2 := NewFileInput("/tmp/test_requests.gor", false)
|
||||
input2 := NewFileInput("/tmp/test_requests.gor", false, 100, 0, false)
|
||||
output2 := NewTestOutput(func(*Message) {
|
||||
atomic.AddInt64(&counter, 1)
|
||||
wg.Done()
|
||||
@@ -315,7 +314,6 @@ func TestFileOutputAppendSizeLimitOverflow(t *testing.T) {
|
||||
name2 := output.file.Name()
|
||||
|
||||
output.flush()
|
||||
output.updateName()
|
||||
|
||||
output.PluginWrite(&Message{Meta: []byte("1 1 1\r\n"), Data: []byte("test")})
|
||||
name3 := output.file.Name()
|
||||
|
||||
+50
-30
@@ -1,10 +1,11 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -12,8 +13,6 @@ import (
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/size"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -25,8 +24,8 @@ const (
|
||||
type response struct {
|
||||
payload []byte
|
||||
uuid []byte
|
||||
roundTripTime int64
|
||||
startedAt int64
|
||||
roundTripTime int64
|
||||
}
|
||||
|
||||
// HTTPOutputConfig struct for holding http output configuration
|
||||
@@ -48,6 +47,24 @@ type HTTPOutputConfig struct {
|
||||
url *url.URL
|
||||
}
|
||||
|
||||
func (hoc *HTTPOutputConfig) Copy() *HTTPOutputConfig {
|
||||
return &HTTPOutputConfig{
|
||||
TrackResponses: hoc.TrackResponses,
|
||||
Stats: hoc.Stats,
|
||||
OriginalHost: hoc.OriginalHost,
|
||||
RedirectLimit: hoc.RedirectLimit,
|
||||
WorkersMin: hoc.WorkersMin,
|
||||
WorkersMax: hoc.WorkersMax,
|
||||
StatsMs: hoc.StatsMs,
|
||||
QueueLen: hoc.QueueLen,
|
||||
ElasticSearch: hoc.ElasticSearch,
|
||||
Timeout: hoc.Timeout,
|
||||
WorkerTimeout: hoc.WorkerTimeout,
|
||||
BufferSize: hoc.BufferSize,
|
||||
SkipVerify: hoc.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPOutput plugin manage pool of workers which send request to replayed server
|
||||
// By default workers pool is dynamic and starts with 1 worker or workerMin workers
|
||||
// You can specify maximum number of workers using `--output-http-workers`
|
||||
@@ -68,42 +85,43 @@ type HTTPOutput struct {
|
||||
func NewHTTPOutput(address string, config *HTTPOutputConfig) PluginReadWriter {
|
||||
o := new(HTTPOutput)
|
||||
var err error
|
||||
config.url, err = url.Parse(address)
|
||||
newConfig := config.Copy()
|
||||
newConfig.url, err = url.Parse(address)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("[OUTPUT-HTTP] parse HTTP output URL error[%q]", err))
|
||||
}
|
||||
if config.url.Scheme == "" {
|
||||
config.url.Scheme = "http"
|
||||
if newConfig.url.Scheme == "" {
|
||||
newConfig.url.Scheme = "http"
|
||||
}
|
||||
config.rawURL = config.url.String()
|
||||
if config.Timeout < time.Millisecond*100 {
|
||||
config.Timeout = time.Second
|
||||
newConfig.rawURL = newConfig.url.String()
|
||||
if newConfig.Timeout < time.Millisecond*100 {
|
||||
newConfig.Timeout = time.Second
|
||||
}
|
||||
if config.BufferSize <= 0 {
|
||||
config.BufferSize = 100 * 1024 // 100kb
|
||||
if newConfig.BufferSize <= 0 {
|
||||
newConfig.BufferSize = 100 * 1024 // 100kb
|
||||
}
|
||||
if config.WorkersMin <= 0 {
|
||||
config.WorkersMin = 1
|
||||
if newConfig.WorkersMin <= 0 {
|
||||
newConfig.WorkersMin = 1
|
||||
}
|
||||
if config.WorkersMin > 1000 {
|
||||
config.WorkersMin = 1000
|
||||
if newConfig.WorkersMin > 1000 {
|
||||
newConfig.WorkersMin = 1000
|
||||
}
|
||||
if config.WorkersMax <= 0 {
|
||||
config.WorkersMax = math.MaxInt32 // idealy so large
|
||||
if newConfig.WorkersMax <= 0 {
|
||||
newConfig.WorkersMax = math.MaxInt32 // idealy so large
|
||||
}
|
||||
if config.WorkersMax < config.WorkersMin {
|
||||
config.WorkersMax = config.WorkersMin
|
||||
if newConfig.WorkersMax < newConfig.WorkersMin {
|
||||
newConfig.WorkersMax = newConfig.WorkersMin
|
||||
}
|
||||
if config.QueueLen <= 0 {
|
||||
config.QueueLen = 1000
|
||||
if newConfig.QueueLen <= 0 {
|
||||
newConfig.QueueLen = 1000
|
||||
}
|
||||
if config.RedirectLimit < 0 {
|
||||
config.RedirectLimit = 0
|
||||
if newConfig.RedirectLimit < 0 {
|
||||
newConfig.RedirectLimit = 0
|
||||
}
|
||||
if config.WorkerTimeout <= 0 {
|
||||
config.WorkerTimeout = time.Second * 2
|
||||
if newConfig.WorkerTimeout <= 0 {
|
||||
newConfig.WorkerTimeout = time.Second * 2
|
||||
}
|
||||
o.config = config
|
||||
o.config = newConfig
|
||||
o.stop = make(chan bool)
|
||||
if o.config.Stats {
|
||||
o.queueStats = NewGorStat("output_http", o.config.StatsMs)
|
||||
@@ -206,7 +224,7 @@ func (o *HTTPOutput) PluginRead() (*Message, error) {
|
||||
msg.Data = resp.payload
|
||||
}
|
||||
|
||||
msg.Meta = payloadHeader(ReplayedResponsePayload, resp.uuid, resp.roundTripTime, resp.startedAt)
|
||||
msg.Meta = payloadHeader(ReplayedResponsePayload, resp.uuid, resp.startedAt, resp.roundTripTime)
|
||||
|
||||
return &msg, nil
|
||||
}
|
||||
@@ -215,6 +233,7 @@ func (o *HTTPOutput) sendRequest(client *HTTPClient, msg *Message) {
|
||||
if !isRequestPayload(msg.Meta) {
|
||||
return
|
||||
}
|
||||
|
||||
uuid := payloadID(msg.Meta)
|
||||
start := time.Now()
|
||||
resp, err := client.Send(msg.Data)
|
||||
@@ -300,7 +319,7 @@ func (c *HTTPClient) Send(data []byte) ([]byte, error) {
|
||||
if !c.config.OriginalHost {
|
||||
req.Host = c.config.url.Host
|
||||
}
|
||||
|
||||
|
||||
// fix #862
|
||||
if c.config.url.Path == "" && c.config.url.RawQuery == "" {
|
||||
req.URL.Scheme = c.config.url.Scheme
|
||||
@@ -308,7 +327,7 @@ func (c *HTTPClient) Send(data []byte) ([]byte, error) {
|
||||
} else {
|
||||
req.URL = c.config.url
|
||||
}
|
||||
|
||||
|
||||
// force connection to not be closed, which can affect the global client
|
||||
req.Close = false
|
||||
// it's an error if this is not equal to empty string
|
||||
@@ -321,5 +340,6 @@ func (c *HTTPClient) Send(data []byte) ([]byte, error) {
|
||||
if c.config.TrackResponses {
|
||||
return httputil.DumpResponse(resp, true)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
+5
-6
@@ -1,14 +1,13 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/buger/goreplay/internal/byteutils"
|
||||
"github.com/buger/goreplay/proto"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/byteutils"
|
||||
"github.com/buger/goreplay/proto"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/Shopify/sarama/mocks"
|
||||
)
|
||||
@@ -23,8 +22,8 @@ type KafkaOutput struct {
|
||||
const KafkaOutputFrequency = 500
|
||||
|
||||
// NewKafkaOutput creates instance of kafka producer client with TLS config
|
||||
func NewKafkaOutput(address string, config *OutputKafkaConfig, tlsConfig *KafkaTLSConfig) PluginWriter {
|
||||
c := NewKafkaConfig(tlsConfig)
|
||||
func NewKafkaOutput(_ string, config *OutputKafkaConfig, tlsConfig *KafkaTLSConfig) PluginWriter {
|
||||
c := NewKafkaConfig(&config.SASLConfig, tlsConfig)
|
||||
|
||||
var producer sarama.AsyncProducer
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
// NullOutput used for debugging, prints nothing
|
||||
type NullOutput struct {
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
_ "bufio"
|
||||
@@ -63,7 +63,7 @@ func NewS3Output(pathTemplate string, config *FileOutputConfig) *S3Output {
|
||||
func (o *S3Output) connect() {
|
||||
if o.session == nil {
|
||||
o.session = session.Must(session.NewSession(awsConfig()))
|
||||
log.Println("[S3 Output] S3 connection succesfully initialized")
|
||||
log.Println("[S3 Output] S3 connection successfully initialized")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-15
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
|
||||
// TCPOutput used for sending raw tcp payloads
|
||||
// Currently used for internal communication between listener and replay server
|
||||
// Can be used for transfering binary payloads like protocol buffers
|
||||
// Can be used for transferring binary payloads like protocol buffers
|
||||
type TCPOutput struct {
|
||||
address string
|
||||
limit int
|
||||
@@ -18,6 +19,8 @@ type TCPOutput struct {
|
||||
bufStats *GorStat
|
||||
config *TCPOutputConfig
|
||||
workerIndex uint32
|
||||
|
||||
close bool
|
||||
}
|
||||
|
||||
// TCPOutputConfig tcp output configuration
|
||||
@@ -26,6 +29,9 @@ type TCPOutputConfig struct {
|
||||
Sticky bool `json:"output-tcp-sticky"`
|
||||
SkipVerify bool `json:"output-tcp-skip-verify"`
|
||||
Workers int `json:"output-tcp-workers"`
|
||||
|
||||
GetInitMessage func() *Message `json:"-"`
|
||||
WriteBeforeMessage func(conn net.Conn, msg *Message) error `json:"-"`
|
||||
}
|
||||
|
||||
// NewTCPOutput constructor for TCPOutput
|
||||
@@ -54,6 +60,10 @@ func (o *TCPOutput) worker(bufferIndex int) {
|
||||
retries := 0
|
||||
conn, err := o.connect(o.address)
|
||||
for {
|
||||
if o.close {
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
@@ -71,33 +81,48 @@ func (o *TCPOutput) worker(bufferIndex int) {
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if o.config.GetInitMessage != nil {
|
||||
msg := o.config.GetInitMessage()
|
||||
_ = o.writeToConnection(conn, msg)
|
||||
}
|
||||
|
||||
for {
|
||||
msg := <-o.buf[bufferIndex]
|
||||
if _, err = conn.Write(msg.Meta); err == nil {
|
||||
if _, err = conn.Write(msg.Data); err == nil {
|
||||
_, err = conn.Write(payloadSeparatorAsBytes)
|
||||
}
|
||||
}
|
||||
|
||||
err = o.writeToConnection(conn, msg)
|
||||
if err != nil {
|
||||
Debug(2, "INFO: TCP output connection closed, reconnecting")
|
||||
o.buf[bufferIndex] <- msg
|
||||
go o.worker(bufferIndex)
|
||||
o.buf[bufferIndex] <- msg
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *TCPOutput) getBufferIndex(data []byte) int {
|
||||
func (o *TCPOutput) writeToConnection(conn net.Conn, msg *Message) (err error) {
|
||||
if o.config.WriteBeforeMessage != nil {
|
||||
err = o.config.WriteBeforeMessage(conn, msg)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if _, err = conn.Write(msg.Meta); err == nil {
|
||||
if _, err = conn.Write(msg.Data); err == nil {
|
||||
_, err = conn.Write(payloadSeparatorAsBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o *TCPOutput) getBufferIndex(msg *Message) int {
|
||||
if !o.config.Sticky {
|
||||
o.workerIndex++
|
||||
return int(o.workerIndex) % o.config.Workers
|
||||
}
|
||||
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(payloadMeta(data)[1])
|
||||
hasher.Write(payloadID(msg.Meta))
|
||||
return int(hasher.Sum32()) % o.config.Workers
|
||||
|
||||
}
|
||||
|
||||
// PluginWrite writes message to this plugin
|
||||
@@ -106,7 +131,7 @@ func (o *TCPOutput) PluginWrite(msg *Message) (n int, err error) {
|
||||
return len(msg.Data), nil
|
||||
}
|
||||
|
||||
bufferIndex := o.getBufferIndex(msg.Data)
|
||||
bufferIndex := o.getBufferIndex(msg)
|
||||
o.buf[bufferIndex] <- msg
|
||||
|
||||
if Settings.OutputTCPStats {
|
||||
@@ -118,9 +143,16 @@ func (o *TCPOutput) PluginWrite(msg *Message) (n int, err error) {
|
||||
|
||||
func (o *TCPOutput) connect(address string) (conn net.Conn, err error) {
|
||||
if o.config.Secure {
|
||||
conn, err = tls.Dial("tcp", address, &tls.Config{InsecureSkipVerify: o.config.SkipVerify})
|
||||
var d tls.Dialer
|
||||
d.Config = &tls.Config{InsecureSkipVerify: o.config.SkipVerify}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
conn, err = d.DialContext(ctx, "tcp", address)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", address)
|
||||
var d net.Dialer
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
conn, err = d.DialContext(ctx, "tcp", address)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -129,3 +161,7 @@ func (o *TCPOutput) connect(address string) (conn net.Conn, err error) {
|
||||
func (o *TCPOutput) String() string {
|
||||
return fmt.Sprintf("TCP output %s, limit: %d", o.address, o.limit)
|
||||
}
|
||||
|
||||
func (o *TCPOutput) Close() {
|
||||
o.close = true
|
||||
}
|
||||
|
||||
+83
-22
@@ -1,12 +1,15 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTCPOutput(t *testing.T) {
|
||||
@@ -15,24 +18,8 @@ func TestTCPOutput(t *testing.T) {
|
||||
listener := startTCP(func(data []byte) {
|
||||
wg.Done()
|
||||
})
|
||||
input := NewTestInput()
|
||||
output := NewTCPOutput(listener.Addr().String(), &TCPOutputConfig{Workers: 10})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []PluginReader{input},
|
||||
Outputs: []PluginWriter{output},
|
||||
}
|
||||
|
||||
emitter := NewEmitter()
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
runTCPOutput(wg, output, 10, false)
|
||||
}
|
||||
|
||||
func startTCP(cb func([]byte)) net.Listener {
|
||||
@@ -125,8 +112,82 @@ func TestBufferDistribution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getTestBytes() []byte {
|
||||
reqh := payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1)
|
||||
reqb := append(reqh, []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n")...)
|
||||
return reqb
|
||||
func getTestBytes() *Message {
|
||||
return &Message{
|
||||
Meta: payloadHeader(RequestPayload, uuid(), time.Now().UnixNano(), -1),
|
||||
Data: []byte("GET / HTTP/1.1\r\nHost: www.w3.org\r\nUser-Agent: Go 1.1 package http\r\nAccept-Encoding: gzip\r\n\r\n"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPOutputGetInitMessage(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
var dataList [][]byte
|
||||
listener := startTCP(func(data []byte) {
|
||||
dataList = append(dataList, data)
|
||||
wg.Done()
|
||||
})
|
||||
getInitMessage := func() *Message {
|
||||
return &Message{
|
||||
Meta: []byte{},
|
||||
Data: []byte("test1"),
|
||||
}
|
||||
}
|
||||
output := NewTCPOutput(listener.Addr().String(), &TCPOutputConfig{Workers: 1, GetInitMessage: getInitMessage})
|
||||
|
||||
runTCPOutput(wg, output, 1, true)
|
||||
|
||||
if assert.Equal(t, 2, len(dataList)) {
|
||||
assert.Equal(t, "test1", string(dataList[0]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPOutputGetInitMessageAndWriteBeforeMessage(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
var dataList [][]byte
|
||||
listener := startTCP(func(data []byte) {
|
||||
dataList = append(dataList, data)
|
||||
wg.Done()
|
||||
})
|
||||
getInitMessage := func() *Message {
|
||||
return &Message{
|
||||
Meta: []byte{},
|
||||
Data: []byte("test2"),
|
||||
}
|
||||
}
|
||||
writeBeforeMessage := func(conn net.Conn, _ *Message) error {
|
||||
_, err := conn.Write([]byte("before"))
|
||||
return err
|
||||
}
|
||||
output := NewTCPOutput(listener.Addr().String(), &TCPOutputConfig{Workers: 1, GetInitMessage: getInitMessage, WriteBeforeMessage: writeBeforeMessage})
|
||||
|
||||
runTCPOutput(wg, output, 1, true)
|
||||
|
||||
if assert.Equal(t, 2, len(dataList)) {
|
||||
assert.Equal(t, "beforetest2", string(dataList[0]))
|
||||
assert.True(t, strings.HasPrefix(string(dataList[1]), "before"))
|
||||
}
|
||||
}
|
||||
|
||||
func runTCPOutput(wg *sync.WaitGroup, output PluginWriter, repeat int, initMessage bool) {
|
||||
input := NewTestInput()
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []PluginReader{input},
|
||||
Outputs: []PluginWriter{output},
|
||||
}
|
||||
|
||||
emitter := NewEmitter()
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
if initMessage {
|
||||
wg.Add(1)
|
||||
}
|
||||
for i := 0; i < repeat; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketOutput used for sending raw tcp payloads
|
||||
// Can be used for transferring binary payloads like protocol buffers
|
||||
type WebSocketOutput struct {
|
||||
address string
|
||||
limit int
|
||||
buf []chan *Message
|
||||
bufStats *GorStat
|
||||
config *WebSocketOutputConfig
|
||||
workerIndex uint32
|
||||
headers http.Header
|
||||
|
||||
close bool
|
||||
}
|
||||
|
||||
// WebSocketOutputConfig WebSocket output configuration
|
||||
type WebSocketOutputConfig struct {
|
||||
Sticky bool `json:"output-ws-sticky"`
|
||||
SkipVerify bool `json:"output-ws-skip-verify"`
|
||||
Workers int `json:"output-ws-workers"`
|
||||
|
||||
Headers map[string][]string `json:"output-ws-headers"`
|
||||
}
|
||||
|
||||
// NewWebSocketOutput constructor for WebSocketOutput
|
||||
// Initialize X workers which hold keep-alive connection
|
||||
func NewWebSocketOutput(address string, config *WebSocketOutputConfig) PluginWriter {
|
||||
o := new(WebSocketOutput)
|
||||
|
||||
u, err := url.Parse(address)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("[OUTPUT-WS] parse WS output URL error[%q]", err))
|
||||
}
|
||||
|
||||
o.config = config
|
||||
o.headers = http.Header{
|
||||
"Authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte(u.User.String()))},
|
||||
}
|
||||
for k, values := range config.Headers {
|
||||
for _, v := range values {
|
||||
o.headers.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
u.User = nil // must be after creating the headers
|
||||
o.address = u.String()
|
||||
|
||||
if Settings.OutputWebSocketStats {
|
||||
o.bufStats = NewGorStat("output_ws", 5000)
|
||||
}
|
||||
|
||||
// create X buffers and send the buffer index to the worker
|
||||
o.buf = make([]chan *Message, o.config.Workers)
|
||||
for i := 0; i < o.config.Workers; i++ {
|
||||
o.buf[i] = make(chan *Message, 100)
|
||||
go o.worker(i)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *WebSocketOutput) worker(bufferIndex int) {
|
||||
retries := 0
|
||||
conn, err := o.connect(o.address)
|
||||
for {
|
||||
if o.close {
|
||||
return
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
Debug(1, fmt.Sprintf("Can't connect to aggregator instance, reconnecting in 1 second. Retries:%d", retries))
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
conn, err = o.connect(o.address)
|
||||
retries++
|
||||
}
|
||||
|
||||
if retries > 0 {
|
||||
Debug(2, fmt.Sprintf("Connected to aggregator instance after %d retries", retries))
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
msg := <-o.buf[bufferIndex]
|
||||
err = conn.WriteMessage(websocket.BinaryMessage, append(msg.Meta, msg.Data...))
|
||||
if err != nil {
|
||||
Debug(2, "INFO: WebSocket output connection closed, reconnecting "+err.Error())
|
||||
go o.worker(bufferIndex)
|
||||
o.buf[bufferIndex] <- msg
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *WebSocketOutput) getBufferIndex(msg *Message) int {
|
||||
if !o.config.Sticky {
|
||||
o.workerIndex++
|
||||
return int(o.workerIndex) % o.config.Workers
|
||||
}
|
||||
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write(payloadID(msg.Meta))
|
||||
return int(hasher.Sum32()) % o.config.Workers
|
||||
}
|
||||
|
||||
// PluginWrite writes message to this plugin
|
||||
func (o *WebSocketOutput) PluginWrite(msg *Message) (n int, err error) {
|
||||
if !isOriginPayload(msg.Meta) {
|
||||
return len(msg.Data), nil
|
||||
}
|
||||
|
||||
bufferIndex := o.getBufferIndex(msg)
|
||||
o.buf[bufferIndex] <- msg
|
||||
|
||||
if Settings.OutputTCPStats {
|
||||
o.bufStats.Write(len(o.buf[bufferIndex]))
|
||||
}
|
||||
|
||||
return len(msg.Data) + len(msg.Meta), nil
|
||||
}
|
||||
|
||||
func (o *WebSocketOutput) connect(address string) (conn *websocket.Conn, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
d := websocket.DefaultDialer
|
||||
if strings.HasPrefix(address, "wss://") {
|
||||
d.TLSClientConfig = &tls.Config{InsecureSkipVerify: o.config.SkipVerify}
|
||||
}
|
||||
|
||||
conn, _, err = d.DialContext(ctx, address, o.headers)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *WebSocketOutput) String() string {
|
||||
return fmt.Sprintf("WebSocket output %s, limit: %d", o.address, o.limit)
|
||||
}
|
||||
|
||||
// Close closes the output
|
||||
func (o *WebSocketOutput) Close() {
|
||||
o.close = true
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWebSocketOutput(t *testing.T) {
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
var gotHeader http.Header
|
||||
wsAddr := startWebsocket(func(data []byte) {
|
||||
wg.Done()
|
||||
}, func(header http.Header) {
|
||||
gotHeader = header
|
||||
})
|
||||
input := NewTestInput()
|
||||
headers := map[string][]string{
|
||||
"key1": {"value1"},
|
||||
"key2": {"value2"},
|
||||
}
|
||||
output := NewWebSocketOutput(wsAddr, &WebSocketOutputConfig{Workers: 1, Headers: headers})
|
||||
|
||||
plugins := &InOutPlugins{
|
||||
Inputs: []PluginReader{input},
|
||||
Outputs: []PluginWriter{output},
|
||||
}
|
||||
|
||||
emitter := NewEmitter()
|
||||
go emitter.Start(plugins, Settings.Middleware)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
input.EmitGET()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
emitter.Close()
|
||||
|
||||
if assert.NotNil(t, gotHeader) {
|
||||
assert.Equal(t, "Basic dXNlcjE=", gotHeader.Get("Authorization"))
|
||||
for k, values := range headers {
|
||||
assert.Equal(t, 1, len(values))
|
||||
assert.Equal(t, values[0], gotHeader.Get(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startWebsocket(cb func([]byte), headercb func(http.Header)) string {
|
||||
upgrader := websocket.Upgrader{}
|
||||
|
||||
http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
headercb(r.Header)
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func(conn *websocket.Conn) {
|
||||
defer conn.Close()
|
||||
for {
|
||||
_, msg, _ := conn.ReadMessage()
|
||||
cb(msg)
|
||||
}
|
||||
}(c)
|
||||
})
|
||||
|
||||
go func() {
|
||||
err := http.ListenAndServe("localhost:8081", nil)
|
||||
if err != nil {
|
||||
log.Fatal("Can't start:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
return "ws://user1@localhost:8081/test"
|
||||
}
|
||||
+8
-5
@@ -1,11 +1,11 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Message represents data accross plugins
|
||||
// Message represents data across plugins
|
||||
type Message struct {
|
||||
Meta []byte // metadata
|
||||
Data []byte // actual data
|
||||
@@ -83,7 +83,6 @@ func (plugins *InOutPlugins) registerPlugin(constructor interface{}, options ...
|
||||
plugins.Outputs = append(plugins.Outputs, w)
|
||||
}
|
||||
plugins.All = append(plugins.All, plugin)
|
||||
|
||||
}
|
||||
|
||||
// NewPlugins specify and initialize all available plugins
|
||||
@@ -107,7 +106,7 @@ func NewPlugins() *InOutPlugins {
|
||||
}
|
||||
|
||||
for _, options := range Settings.InputRAW {
|
||||
plugins.registerPlugin(NewRAWInput, options, Settings.RAWInputConfig)
|
||||
plugins.registerPlugin(NewRAWInput, options, Settings.InputRAWConfig)
|
||||
}
|
||||
|
||||
for _, options := range Settings.InputTCP {
|
||||
@@ -118,8 +117,12 @@ func NewPlugins() *InOutPlugins {
|
||||
plugins.registerPlugin(NewTCPOutput, options, &Settings.OutputTCPConfig)
|
||||
}
|
||||
|
||||
for _, options := range Settings.OutputWebSocket {
|
||||
plugins.registerPlugin(NewWebSocketOutput, options, &Settings.OutputWebSocketConfig)
|
||||
}
|
||||
|
||||
for _, options := range Settings.InputFile {
|
||||
plugins.registerPlugin(NewFileInput, options, Settings.InputFileLoop)
|
||||
plugins.registerPlugin(NewFileInput, options, Settings.InputFileLoop, Settings.InputFileReadDepth, Settings.InputFileMaxWait, Settings.InputFileDryRun)
|
||||
}
|
||||
|
||||
for _, path := range Settings.OutputFile {
|
||||
|
||||
+5
-5
@@ -1,14 +1,14 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPluginsRegistration(t *testing.T) {
|
||||
Settings.InputDummy = MultiOption{"[]"}
|
||||
Settings.OutputDummy = MultiOption{"[]"}
|
||||
Settings.OutputHTTP = MultiOption{"www.example.com|10"}
|
||||
Settings.InputFile = MultiOption{"/dev/null"}
|
||||
Settings.InputDummy = []string{"[]"}
|
||||
Settings.OutputDummy = []string{"[]"}
|
||||
Settings.OutputHTTP = []string{"www.example.com|10"}
|
||||
Settings.InputFile = []string{"/dev/null"}
|
||||
|
||||
plugins := NewPlugins()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
// PRO this value indicates if goreplay is running in PRO mode..
|
||||
// it must not be modified explicitly in production
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// +build gofuzz
|
||||
//go:build gofuzz
|
||||
|
||||
package proto
|
||||
|
||||
|
||||
+172
-110
@@ -19,11 +19,12 @@ package proto
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"github.com/buger/goreplay/internal/byteutils"
|
||||
|
||||
_ "fmt"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
|
||||
"github.com/buger/goreplay/byteutils"
|
||||
)
|
||||
|
||||
// CRLF In HTTP newline defined by 2 bytes (for both windows and *nix support)
|
||||
@@ -58,10 +59,15 @@ func MIMEHeadersStartPos(payload []byte) int {
|
||||
// If not found, value will be blank, and headerStart will be -1
|
||||
// Do not support multi-line headers.
|
||||
func header(payload []byte, name []byte) (value []byte, headerStart, headerEnd, valueStart, valueEnd int) {
|
||||
headerStart = MIMEHeadersStartPos(payload)
|
||||
if headerStart < 0 {
|
||||
return
|
||||
if HasTitle(payload) {
|
||||
headerStart = MIMEHeadersStartPos(payload)
|
||||
if headerStart < 0 {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
headerStart = 0
|
||||
}
|
||||
|
||||
var colonIndex int
|
||||
for headerStart < len(payload) {
|
||||
headerEnd = bytes.IndexByte(payload[headerStart:], '\n')
|
||||
@@ -71,9 +77,12 @@ func header(payload []byte, name []byte) (value []byte, headerStart, headerEnd,
|
||||
headerEnd += headerStart
|
||||
colonIndex = bytes.IndexByte(payload[headerStart:headerEnd], ':')
|
||||
if colonIndex == -1 {
|
||||
break
|
||||
// Malformed header, skip, most likely packet with partial headers
|
||||
headerStart = headerEnd + 1
|
||||
continue
|
||||
}
|
||||
colonIndex += headerStart
|
||||
|
||||
if bytes.EqualFold(payload[headerStart:colonIndex], name) {
|
||||
valueStart = colonIndex + 1
|
||||
valueEnd = headerEnd - 2
|
||||
@@ -193,7 +202,7 @@ func Body(payload []byte) []byte {
|
||||
return payload[pos:]
|
||||
}
|
||||
|
||||
// Path takes payload and retuns request path: Split(firstLine, ' ')[1]
|
||||
// Path takes payload and returns request path: Split(firstLine, ' ')[1]
|
||||
func Path(payload []byte) []byte {
|
||||
if !HasRequestTitle(payload) {
|
||||
return nil
|
||||
@@ -398,140 +407,193 @@ func HasTitle(payload []byte) bool {
|
||||
return HasRequestTitle(payload) || HasResponseTitle(payload)
|
||||
}
|
||||
|
||||
// CheckChunked checks HTTP/1 chunked data integrity and return the final index
|
||||
// of chunks(index after '0\r\n\r\n') or -1 if there is missing data
|
||||
// or there is bad format
|
||||
func CheckChunked(buf []byte) (chunkEnd int) {
|
||||
var (
|
||||
ok bool
|
||||
chkLen int
|
||||
sz int
|
||||
ext int
|
||||
)
|
||||
for {
|
||||
sz = bytes.IndexByte(buf[chunkEnd:], '\r')
|
||||
// CheckChunked checks HTTP/1 chunked data integrity(https://tools.ietf.org/html/rfc7230#section-4.1)
|
||||
// and returns the length of total valid scanned chunks(including chunk size, extensions and CRLFs) and
|
||||
// full is true if all chunks was scanned.
|
||||
func CheckChunked(bufs ...[]byte) (chunkEnd int, full bool) {
|
||||
var buf []byte
|
||||
if len(bufs) > 0 {
|
||||
buf = bufs[0]
|
||||
}
|
||||
for chunkEnd < len(buf) {
|
||||
sz := bytes.IndexByte(buf[chunkEnd:], '\r')
|
||||
if sz < 1 {
|
||||
return -1
|
||||
break
|
||||
}
|
||||
// ignoring chunks extensions https://github.com/golang/go/issues/13135
|
||||
// but chunks extensions are no longer a thing
|
||||
ext = bytes.IndexByte(buf[chunkEnd:chunkEnd+sz], ';')
|
||||
if ext < 0 {
|
||||
ext = sz
|
||||
// don't parse chunk extensions https://github.com/golang/go/issues/13135.
|
||||
// chunks extensions are no longer a thing, but we do check if the byte
|
||||
// following the parsed hex number is ';'
|
||||
sz += chunkEnd
|
||||
chkLen, ok := atoI(buf[chunkEnd:sz], 16)
|
||||
if !ok && bytes.IndexByte(buf[chunkEnd:sz], ';') < 1 {
|
||||
break
|
||||
}
|
||||
chkLen, ok = atoI(buf[chunkEnd:chunkEnd+ext], 16)
|
||||
if !ok {
|
||||
return -1
|
||||
sz++ // + '\n'
|
||||
// total length = SIZE + CRLF + OCTETS + CRLF
|
||||
allChunk := sz + chkLen + 2
|
||||
if allChunk >= len(buf) ||
|
||||
buf[sz]&buf[allChunk] != '\n' ||
|
||||
buf[allChunk-1] != '\r' {
|
||||
break
|
||||
}
|
||||
chunkEnd += (sz + 2)
|
||||
chunkEnd = allChunk + 1
|
||||
if chkLen == 0 {
|
||||
if !bytes.Equal(buf[chunkEnd:chunkEnd+2], CRLF) {
|
||||
return -1
|
||||
full = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProtocolStateSetter is an interface used to provide protocol state for future use
|
||||
type ProtocolStateSetter interface {
|
||||
SetProtocolState(interface{})
|
||||
ProtocolState() interface{}
|
||||
}
|
||||
|
||||
type HTTPState struct {
|
||||
Body int // body index
|
||||
HeaderStart int
|
||||
HeaderEnd int
|
||||
HeaderParsed bool // we checked necessary headers
|
||||
HasFullPayload bool // all chunks has been parsed
|
||||
IsChunked bool // Transfer-Encoding: chunked
|
||||
BodyLen int // Content-Length's value
|
||||
HasTrailer bool // Trailer header?
|
||||
Continue100 bool
|
||||
}
|
||||
|
||||
// HasFullPayload checks if this message has full or valid payloads and returns true.
|
||||
// Message param is optional but recommended on cases where 'data' is storing
|
||||
// partial-to-full stream of bytes(packets).
|
||||
func HasFullPayload(m ProtocolStateSetter, payloads ...[]byte) bool {
|
||||
var state *HTTPState
|
||||
if m != nil {
|
||||
state, _ = m.ProtocolState().(*HTTPState)
|
||||
}
|
||||
if state == nil {
|
||||
state = new(HTTPState)
|
||||
if m != nil {
|
||||
m.SetProtocolState(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Http Packets can only start with a few things, check if this is one of them
|
||||
if len(payloads) == 0 {
|
||||
return false
|
||||
}
|
||||
if !HasRequestTitle(payloads[0]) && !HasResponseTitle(payloads[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
if state.HeaderStart < 1 {
|
||||
for _, data := range payloads {
|
||||
state.HeaderStart = MIMEHeadersStartPos(data)
|
||||
if state.HeaderStart < 0 {
|
||||
return false
|
||||
} else {
|
||||
break
|
||||
}
|
||||
return chunkEnd + 2
|
||||
}
|
||||
// ideally chunck length and at least len("\r\n0\r\n\r\n")
|
||||
if len(buf[chunkEnd:]) < chkLen+7 {
|
||||
return -1
|
||||
}
|
||||
chunkEnd += chkLen
|
||||
// chunks must end with CRLF
|
||||
if !bytes.Equal(buf[chunkEnd:chunkEnd+2], CRLF) {
|
||||
return -1
|
||||
}
|
||||
chunkEnd += 2
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback is an interface used to provide feedback or store dummy data for future use
|
||||
type Feedback interface {
|
||||
SetFeedback(interface{})
|
||||
Feedback() interface{}
|
||||
}
|
||||
|
||||
type feedback struct {
|
||||
body int // body index
|
||||
hdrStart int
|
||||
headers textproto.MIMEHeader
|
||||
}
|
||||
|
||||
// HasFullPayload reports if this http has full payloads
|
||||
func HasFullPayload(data []byte, f Feedback) bool {
|
||||
var feed *feedback
|
||||
var ok bool
|
||||
var body []byte
|
||||
if f != nil {
|
||||
feed, ok = f.Feedback().(*feedback)
|
||||
}
|
||||
if !ok {
|
||||
feed = new(feedback)
|
||||
}
|
||||
if f != nil {
|
||||
f.SetFeedback(feed)
|
||||
}
|
||||
if feed.hdrStart < 1 {
|
||||
feed.hdrStart = MIMEHeadersStartPos(data)
|
||||
if feed.hdrStart < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if feed.body < 1 {
|
||||
feed.body = MIMEHeadersEndPos(data)
|
||||
if feed.body < 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if feed.headers == nil {
|
||||
feed.headers = GetHeaders(data[feed.hdrStart:feed.body])
|
||||
if feed.headers == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(data) > feed.body {
|
||||
body = data[feed.body:]
|
||||
}
|
||||
|
||||
if feed.headers.Get("Transfer-Encoding") == "chunked" {
|
||||
if state.Body < 1 || state.HeaderEnd < 1 {
|
||||
var pos int
|
||||
for _, data := range payloads {
|
||||
endPos := MIMEHeadersEndPos(data)
|
||||
if endPos < 0 {
|
||||
pos += len(data)
|
||||
} else {
|
||||
pos += endPos
|
||||
state.HeaderEnd = pos
|
||||
}
|
||||
|
||||
if endPos > 0 {
|
||||
state.Body = pos
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.HeaderEnd < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if !state.HeaderParsed {
|
||||
var pos int
|
||||
for _, data := range payloads {
|
||||
chunked := Header(data, []byte("Transfer-Encoding"))
|
||||
|
||||
if len(chunked) > 0 && bytes.Index(data, []byte("chunked")) > 0 {
|
||||
state.IsChunked = true
|
||||
// trailers are generally not allowed in non-chunks body
|
||||
state.HasTrailer = len(Header(data, []byte("Trailer"))) > 0
|
||||
} else {
|
||||
contentLen := Header(data, []byte("Content-Length"))
|
||||
state.BodyLen, _ = atoI(contentLen, 10)
|
||||
}
|
||||
|
||||
pos += len(data)
|
||||
|
||||
if string(Header(data, []byte("Expect"))) == "100-continue" {
|
||||
state.Continue100 = true
|
||||
}
|
||||
|
||||
if state.BodyLen > 0 || pos >= state.Body {
|
||||
state.HeaderParsed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bodyLen := 0
|
||||
for _, data := range payloads {
|
||||
bodyLen += len(data)
|
||||
}
|
||||
bodyLen -= state.Body
|
||||
|
||||
if state.IsChunked {
|
||||
// check chunks
|
||||
if len(body) < 1 {
|
||||
return false
|
||||
}
|
||||
var chunkEnd int
|
||||
if chunkEnd = CheckChunked(body); chunkEnd < 1 {
|
||||
if bodyLen < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// check trailer headers
|
||||
if feed.headers.Get("Trailer") == "" {
|
||||
return true
|
||||
if state.HasTrailer {
|
||||
if bytes.HasSuffix(payloads[len(payloads)-1], []byte("\r\n\r\n")) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if bytes.HasSuffix(payloads[len(payloads)-1], []byte("0\r\n\r\n")) {
|
||||
state.HasFullPayload = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
// trailer headers(whether chunked or plain) should end with empty line
|
||||
return len(body) > chunkEnd && MIMEHeadersEndPos(body[chunkEnd:]) != -1
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// check for content-length header
|
||||
if header := feed.headers.Get("Content-Length"); header != "" {
|
||||
num, ok := atoI([]byte(header), 10)
|
||||
// trailers are generally not allowed in non-chunks body
|
||||
return ok && num == len(body)
|
||||
}
|
||||
return true
|
||||
return state.BodyLen == bodyLen
|
||||
}
|
||||
|
||||
// this works with positive integers
|
||||
func atoI(s []byte, base int) (num int, ok bool) {
|
||||
var v int
|
||||
ok = true
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] > 127 {
|
||||
return 0, false
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
v = int(hexTable[s[i]])
|
||||
if v >= base || (v == 0 && s[i] != '0') {
|
||||
return 0, false
|
||||
ok = false
|
||||
break
|
||||
}
|
||||
num = (num * base) + v
|
||||
}
|
||||
return num, true
|
||||
return
|
||||
}
|
||||
|
||||
var hexTable = [128]byte{
|
||||
|
||||
+41
-38
@@ -56,6 +56,12 @@ func TestHeader(t *testing.T) {
|
||||
if val = Header(payload, []byte("host")); !bytes.Equal(val, []byte("www.w3.org")) {
|
||||
t.Error("Should find lower case 1 word header")
|
||||
}
|
||||
|
||||
payload = []byte("GT\r\nContent-Length: 10\r\n\r\n")
|
||||
|
||||
if val = Header(payload, []byte("Content-Length")); !bytes.Equal(val, []byte("10")) {
|
||||
t.Error("Should find in partial payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMIMEHeadersEndPos(t *testing.T) {
|
||||
@@ -388,63 +394,61 @@ func TestHasRequestTitle(t *testing.T) {
|
||||
|
||||
func TestCheckChunks(t *testing.T) {
|
||||
var m = "4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"
|
||||
chunkEnd := CheckChunked([]byte(m))
|
||||
expected := bytes.Index([]byte(m), []byte("0\r\n")) + 5
|
||||
chunkEnd, _ := CheckChunked([]byte(m))
|
||||
expected := len(m)
|
||||
if chunkEnd != expected {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, expected)
|
||||
}
|
||||
|
||||
m = "7\r\nMozia\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n"
|
||||
chunkEnd = CheckChunked([]byte(m))
|
||||
if chunkEnd != -1 {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, -1)
|
||||
chunkEnd, _ = CheckChunked([]byte(m))
|
||||
if chunkEnd != 0 {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, 0)
|
||||
}
|
||||
|
||||
// with trailers
|
||||
m = "4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n0\r\n\r\nEXpires"
|
||||
chunkEnd = CheckChunked([]byte(m))
|
||||
expected = bytes.Index([]byte(m), []byte("0\r\n")) + 5
|
||||
chunkEnd, _ = CheckChunked([]byte(m))
|
||||
expected = len(m) - 7
|
||||
if chunkEnd != expected {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, expected)
|
||||
}
|
||||
|
||||
// last chunk inside the the body
|
||||
// last chunk inside the body
|
||||
// with trailers
|
||||
m = "4\r\nWiki\r\n5\r\npedia\r\nE\r\n in\r\n\r\nchunks.\r\n3\r\n0\r\n\r\n0\r\n\r\nEXpires"
|
||||
chunkEnd = CheckChunked([]byte(m))
|
||||
expected = bytes.Index([]byte(m), []byte("0\r\n")) + 10
|
||||
chunkEnd, _ = CheckChunked([]byte(m))
|
||||
expected = len(m) - 7
|
||||
if chunkEnd != expected {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, expected)
|
||||
}
|
||||
|
||||
// checks with chucks-extensions
|
||||
m = "4\r\nWiki\r\n5\r\npedia\r\nE; name='quoted string'\r\n in\r\n\r\nchunks.\r\n3\r\n0\r\n\r\n0\r\n\r\nEXpires"
|
||||
chunkEnd = CheckChunked([]byte(m))
|
||||
expected = bytes.Index([]byte(m), []byte("0\r\n")) + 10
|
||||
chunkEnd, _ = CheckChunked([]byte(m))
|
||||
expected = len(m) - 7
|
||||
if chunkEnd != expected {
|
||||
t.Errorf("expected %d to equal %d", chunkEnd, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasFullPayload(t *testing.T) {
|
||||
var m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n"
|
||||
got := HasFullPayload([]byte(m), nil)
|
||||
expected := true
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
}
|
||||
var m string
|
||||
var got, expected bool
|
||||
|
||||
// check with invalid chunk format
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n7\r\nMozia\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\n"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
expected = false
|
||||
got = HasFullPayload(nil,
|
||||
[]byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"),
|
||||
[]byte("Transfer-Encoding: chunked\r\n\r\n"),
|
||||
[]byte("7\r\nMozilla\r\n9\r\nDeveloper\r\n"),
|
||||
[]byte("7\r\nNetwork\r\n0\r\n\r\n"))
|
||||
expected = true
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
}
|
||||
|
||||
// check chunks with trailers
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nTrailer: Expires\r\n\r\n7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = true
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
@@ -452,7 +456,7 @@ func TestHasFullPayload(t *testing.T) {
|
||||
|
||||
// check with missing trailers
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\nTrailer: Expires\r\n\r\n7\r\nMozilla\r\n9\r\nDeveloper\r\n7\r\nNetwork\r\n0\r\n\r\nExpires: Wed, 21 Oct 2015 07:28:00"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = false
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
@@ -460,7 +464,7 @@ func TestHasFullPayload(t *testing.T) {
|
||||
|
||||
// check with content-length
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 23\r\n\r\nMozillaDeveloperNetwork"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = true
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
@@ -468,7 +472,7 @@ func TestHasFullPayload(t *testing.T) {
|
||||
|
||||
// check missing total length
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 23\r\n\r\nMozillaDeveloperNet"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = false
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
@@ -476,26 +480,25 @@ func TestHasFullPayload(t *testing.T) {
|
||||
|
||||
// check with no body
|
||||
m = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"
|
||||
got = HasFullPayload([]byte(m), nil)
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = true
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
}
|
||||
|
||||
// check with trailer and no header
|
||||
m = "Content-Type: text/plain\r\nContent-Length: 23\r\n\r\nMozillaDeveloperNetwork"
|
||||
got = HasFullPayload(nil, []byte(m))
|
||||
expected = false
|
||||
if got != expected {
|
||||
t.Errorf("expected %v to equal %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHasFullPayload(b *testing.B) {
|
||||
var buf bytes.Buffer
|
||||
buf.Write([]byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n"))
|
||||
var chunk = []byte("1e\r\n111111111111111111111111111111\r\n")
|
||||
for i := 0; i < 5000; i++ {
|
||||
buf.Write(chunk)
|
||||
}
|
||||
buf.Write([]byte("0\r\n\r\n"))
|
||||
data := buf.Bytes()
|
||||
b.ResetTimer() // ignores the upper initialization
|
||||
b.ReportMetric(float64(5000), "chunks/op")
|
||||
data := []byte("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n1e\r\n111111111111111111111111111111\r\n0\r\n\r\n")
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !HasFullPayload(data, nil) {
|
||||
if !HasFullPayload(nil, data) {
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -60,7 +60,7 @@ func NewS3ReadCloser(path string) *S3ReadCloser {
|
||||
bucket, key := parseS3Url(path)
|
||||
sess := session.Must(session.NewSession(awsConfig()))
|
||||
|
||||
log.Println("[S3 Input] S3 connection succesfully initialized", path)
|
||||
log.Println("[S3 Input] S3 connection successfully initialized", path)
|
||||
|
||||
return &S3ReadCloser{
|
||||
bucket: bucket,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
// +build pro
|
||||
//go:build pro
|
||||
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -127,7 +127,7 @@ func TestInputFileFromS3(t *testing.T) {
|
||||
<-output.closeCh
|
||||
}
|
||||
|
||||
input := NewFileInput(fmt.Sprintf("s3://test-gor-eu/%d", rnd), false)
|
||||
input := NewFileInput(fmt.Sprintf("s3://test-gor-eu/%d", rnd), false, 100, 0, false)
|
||||
|
||||
buf := make([]byte, 1000)
|
||||
for i := 0; i <= 19999; i++ {
|
||||
|
||||
+112
-44
@@ -1,9 +1,11 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/buger/goreplay/internal/size"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -12,15 +14,48 @@ import (
|
||||
var DEMO string
|
||||
|
||||
// MultiOption allows to specify multiple flags with same name and collects all values into array
|
||||
type MultiOption []string
|
||||
type MultiOption struct {
|
||||
a *[]string
|
||||
}
|
||||
|
||||
func (h *MultiOption) String() string {
|
||||
return fmt.Sprint(*h)
|
||||
if h.a == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(*h.a)
|
||||
}
|
||||
|
||||
// Set gets called multiple times for each flag with same name
|
||||
func (h *MultiOption) Set(value string) error {
|
||||
*h = append(*h, value)
|
||||
if h.a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
*h.a = append(*h.a, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiOption allows to specify multiple flags with same name and collects all values into array
|
||||
type MultiIntOption struct {
|
||||
a *[]int
|
||||
}
|
||||
|
||||
func (h *MultiIntOption) String() string {
|
||||
if h.a == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprint(*h.a)
|
||||
}
|
||||
|
||||
// Set gets called multiple times for each flag with same name
|
||||
func (h *MultiIntOption) Set(value string) error {
|
||||
if h.a == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
val, _ := strconv.Atoi(value)
|
||||
*h.a = append(*h.a, val)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,34 +69,43 @@ type AppSettings struct {
|
||||
RecognizeTCPSessions bool `json:"recognize-tcp-sessions"`
|
||||
Pprof string `json:"http-pprof"`
|
||||
|
||||
InputDummy MultiOption `json:"input-dummy"`
|
||||
OutputDummy MultiOption
|
||||
CopyBufferSize size.Size `json:"copy-buffer-size"`
|
||||
|
||||
InputDummy []string `json:"input-dummy"`
|
||||
OutputDummy []string
|
||||
OutputStdout bool `json:"output-stdout"`
|
||||
OutputNull bool `json:"output-null"`
|
||||
|
||||
InputTCP MultiOption `json:"input-tcp"`
|
||||
InputTCP []string `json:"input-tcp"`
|
||||
InputTCPConfig TCPInputConfig
|
||||
OutputTCP MultiOption `json:"output-tcp"`
|
||||
OutputTCP []string `json:"output-tcp"`
|
||||
OutputTCPConfig TCPOutputConfig
|
||||
OutputTCPStats bool `json:"output-tcp-stats"`
|
||||
|
||||
InputFile MultiOption `json:"input-file"`
|
||||
InputFileLoop bool `json:"input-file-loop"`
|
||||
OutputFile MultiOption `json:"output-file"`
|
||||
OutputFileConfig FileOutputConfig
|
||||
OutputWebSocket []string `json:"output-ws"`
|
||||
OutputWebSocketConfig WebSocketOutputConfig
|
||||
OutputWebSocketStats bool `json:"output-ws-stats"`
|
||||
|
||||
InputRAW MultiOption `json:"input_raw"`
|
||||
RAWInputConfig
|
||||
InputFile []string `json:"input-file"`
|
||||
InputFileLoop bool `json:"input-file-loop"`
|
||||
InputFileReadDepth int `json:"input-file-read-depth"`
|
||||
InputFileDryRun bool `json:"input-file-dry-run"`
|
||||
InputFileMaxWait time.Duration `json:"input-file-max-wait"`
|
||||
OutputFile []string `json:"output-file"`
|
||||
OutputFileConfig FileOutputConfig
|
||||
|
||||
InputRAW []string `json:"input_raw"`
|
||||
InputRAWConfig RAWInputConfig
|
||||
|
||||
Middleware string `json:"middleware"`
|
||||
|
||||
InputHTTP MultiOption
|
||||
OutputHTTP MultiOption `json:"output-http"`
|
||||
PrettifyHTTP bool `json:"prettify-http"`
|
||||
InputHTTP []string
|
||||
OutputHTTP []string `json:"output-http"`
|
||||
PrettifyHTTP bool `json:"prettify-http"`
|
||||
|
||||
OutputHTTPConfig HTTPOutputConfig
|
||||
|
||||
OutputBinary MultiOption `json:"output-binary"`
|
||||
OutputBinary []string `json:"output-binary"`
|
||||
OutputBinaryConfig BinaryOutputConfig
|
||||
|
||||
ModifierConfig HTTPModifierConfig
|
||||
@@ -95,26 +139,35 @@ func init() {
|
||||
flag.BoolVar(&Settings.SplitOutput, "split-output", false, "By default each output gets same traffic. If set to `true` it splits traffic equally among all outputs.")
|
||||
flag.BoolVar(&Settings.RecognizeTCPSessions, "recognize-tcp-sessions", false, "[PRO] If turned on http output will create separate worker for each TCP session. Splitting output will session based as well.")
|
||||
|
||||
flag.Var(&Settings.InputDummy, "input-dummy", "Used for testing outputs. Emits 'Get /' request every 1s")
|
||||
flag.Var(&MultiOption{&Settings.InputDummy}, "input-dummy", "Used for testing outputs. Emits 'Get /' request every 1s")
|
||||
flag.BoolVar(&Settings.OutputStdout, "output-stdout", false, "Used for testing inputs. Just prints to console data coming from inputs.")
|
||||
flag.BoolVar(&Settings.OutputNull, "output-null", false, "Used for testing inputs. Drops all requests.")
|
||||
|
||||
flag.Var(&Settings.InputTCP, "input-tcp", "Used for internal communication between Gor instances. Example: \n\t# Receive requests from other Gor instances on 28020 port, and redirect output to staging\n\tgor --input-tcp :28020 --output-http staging.com")
|
||||
flag.Var(&MultiOption{&Settings.InputTCP}, "input-tcp", "Used for internal communication between Gor instances. Example: \n\t# Receive requests from other Gor instances on 28020 port, and redirect output to staging\n\tgor --input-tcp :28020 --output-http staging.com")
|
||||
flag.BoolVar(&Settings.InputTCPConfig.Secure, "input-tcp-secure", false, "Turn on TLS security. Do not forget to specify certificate and key files.")
|
||||
flag.StringVar(&Settings.InputTCPConfig.CertificatePath, "input-tcp-certificate", "", "Path to PEM encoded certificate file. Used when TLS turned on.")
|
||||
flag.StringVar(&Settings.InputTCPConfig.KeyPath, "input-tcp-certificate-key", "", "Path to PEM encoded certificate key file. Used when TLS turned on.")
|
||||
|
||||
flag.Var(&Settings.OutputTCP, "output-tcp", "Used for internal communication between Gor instances. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-tcp replay.local:28020")
|
||||
flag.Var(&MultiOption{&Settings.OutputTCP}, "output-tcp", "Used for internal communication between Gor instances. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-tcp replay.local:28020")
|
||||
flag.BoolVar(&Settings.OutputTCPConfig.Secure, "output-tcp-secure", false, "Use TLS secure connection. --input-file on another end should have TLS turned on as well.")
|
||||
flag.BoolVar(&Settings.OutputTCPConfig.SkipVerify, "output-tcp-skip-verify", false, "Don't verify hostname on TLS secure connection.")
|
||||
flag.BoolVar(&Settings.OutputTCPConfig.Sticky, "output-tcp-sticky", false, "Use Sticky connection. Request/Response with same ID will be sent to the same connection.")
|
||||
flag.IntVar(&Settings.OutputTCPConfig.Workers, "output-tcp-workers", 10, "Number of parallel tcp connections, default is 10")
|
||||
flag.BoolVar(&Settings.OutputTCPStats, "output-tcp-stats", false, "Report TCP output queue stats to console every 5 seconds.")
|
||||
|
||||
flag.Var(&Settings.InputFile, "input-file", "Read requests from file: \n\tgor --input-file ./requests.gor --output-http staging.com")
|
||||
flag.BoolVar(&Settings.InputFileLoop, "input-file-loop", false, "Loop input files, useful for performance testing.")
|
||||
flag.Var(&MultiOption{&Settings.OutputWebSocket}, "output-ws", "Just like output tcp, just with WebSocket. Example: \n\t# Listen for requests on 80 port and forward them to other Gor instance on 28020 port\n\tgor --input-raw :80 --output-ws wss://replay.local:28020/endpoint")
|
||||
flag.BoolVar(&Settings.OutputWebSocketConfig.SkipVerify, "output-ws-skip-verify", false, "Don't verify hostname on TLS secure connection.")
|
||||
flag.BoolVar(&Settings.OutputWebSocketConfig.Sticky, "output-ws-sticky", false, "Use Sticky connection. Request/Response with same ID will be sent to the same connection.")
|
||||
flag.IntVar(&Settings.OutputWebSocketConfig.Workers, "output-ws-workers", 10, "Number of parallel ws connections, default is 10")
|
||||
flag.BoolVar(&Settings.OutputWebSocketStats, "output-ws-stats", false, "Report WebSocket output queue stats to console every 5 seconds.")
|
||||
|
||||
flag.Var(&Settings.OutputFile, "output-file", "Write incoming requests to file: \n\tgor --input-raw :80 --output-file ./requests.gor")
|
||||
flag.Var(&MultiOption{&Settings.InputFile}, "input-file", "Read requests from file: \n\tgor --input-file ./requests.gor --output-http staging.com")
|
||||
flag.BoolVar(&Settings.InputFileLoop, "input-file-loop", false, "Loop input files, useful for performance testing.")
|
||||
flag.IntVar(&Settings.InputFileReadDepth, "input-file-read-depth", 100, "GoReplay tries to read and cache multiple records, in advance. In parallel it also perform sorting of requests, if they came out of order. Since it needs hold this buffer in memory, bigger values can cause worse performance")
|
||||
flag.BoolVar(&Settings.InputFileDryRun, "input-file-dry-run", false, "Simulate reading from the data source without replaying it. You will get information about expected replay time, number of found records etc.")
|
||||
flag.DurationVar(&Settings.InputFileMaxWait, "input-file-max-wait", 0, "Set the maximum time between requests. Can help in situations when you have too long periods between request, and you want to skip them. Example: --input-raw-max-wait 1s")
|
||||
|
||||
flag.Var(&MultiOption{&Settings.OutputFile}, "output-file", "Write incoming requests to file: \n\tgor --input-raw :80 --output-file ./requests.gor")
|
||||
flag.DurationVar(&Settings.OutputFileConfig.FlushInterval, "output-file-flush-interval", time.Second, "Interval for forcing buffer flush to the file, default: 1s.")
|
||||
flag.BoolVar(&Settings.OutputFileConfig.Append, "output-file-append", false, "The flushed chunk is appended to existence file or not. ")
|
||||
flag.Var(&Settings.OutputFileConfig.SizeLimit, "output-file-size-limit", "Size of each chunk. Default: 32mb")
|
||||
@@ -123,28 +176,35 @@ func init() {
|
||||
|
||||
flag.StringVar(&Settings.OutputFileConfig.BufferPath, "output-file-buffer", "/tmp", "The path for temporary storing current buffer: \n\tgor --input-raw :80 --output-file s3://mybucket/logs/%Y-%m-%d.gz --output-file-buffer /mnt/logs")
|
||||
|
||||
flag.BoolVar(&Settings.PrettifyHTTP, "prettify-http", false, "If enabled, will automatically decode requests and responses with: Content-Encoding: gzip and Transfer-Encoding: chunked. Useful for debugging, in conjuction with --output-stdout")
|
||||
flag.BoolVar(&Settings.PrettifyHTTP, "prettify-http", false, "If enabled, will automatically decode requests and responses with: Content-Encoding: gzip and Transfer-Encoding: chunked. Useful for debugging, in conjunction with --output-stdout")
|
||||
|
||||
flag.Var(&Settings.CopyBufferSize, "copy-buffer-size", "Set the buffer size for an individual request (default 5MB)")
|
||||
|
||||
// input raw flags
|
||||
flag.Var(&Settings.InputRAW, "input-raw", "Capture traffic from given port (use RAW sockets and require *sudo* access):\n\t# Capture traffic from 8080 port\n\tgor --input-raw :8080 --output-http staging.com")
|
||||
flag.BoolVar(&Settings.TrackResponse, "input-raw-track-response", false, "If turned on Gor will track responses in addition to requests, and they will be available to middleware and file output.")
|
||||
flag.Var(&Settings.Engine, "input-raw-engine", "Intercept traffic using `libpcap` (default), `raw_socket` or `pcap_file`")
|
||||
flag.Var(&Settings.Protocol, "input-raw-protocol", "Specify application protocol of intercepted traffic. Possible values: http, binary")
|
||||
flag.StringVar(&Settings.RealIPHeader, "input-raw-realip-header", "", "If not blank, injects header with given name and real IP value to the request payload. Usually this header should be named: X-Real-IP")
|
||||
flag.DurationVar(&Settings.Expire, "input-raw-expire", time.Second*2, "How much it should wait for the last TCP packet, till consider that TCP message complete.")
|
||||
flag.StringVar(&Settings.BPFFilter, "input-raw-bpf-filter", "", "BPF filter to write custom expressions. Can be useful in case of non standard network interfaces like tunneling or SPAN port. Example: --input-raw-bpf-filter 'dst port 80'")
|
||||
flag.StringVar(&Settings.TimestampType, "input-raw-timestamp-type", "", "Possible values: PCAP_TSTAMP_HOST, PCAP_TSTAMP_HOST_LOWPREC, PCAP_TSTAMP_HOST_HIPREC, PCAP_TSTAMP_ADAPTER, PCAP_TSTAMP_ADAPTER_UNSYNCED. This values not supported on all systems, GoReplay will tell you available values of you put wrong one.")
|
||||
flag.Var(&Settings.CopyBufferSize, "copy-buffer-size", "Set the buffer size for an individual request (default 5MB)")
|
||||
flag.BoolVar(&Settings.Snaplen, "input-raw-override-snaplen", false, "Override the capture snaplen to be 64k. Required for some Virtualized environments")
|
||||
flag.DurationVar(&Settings.BufferTimeout, "input-raw-buffer-timeout", 0, "set the pcap timeout. for immediate mode don't set this flag")
|
||||
flag.Var(&Settings.BufferSize, "input-raw-buffer-size", "Controls size of the OS buffer which holds packets until they dispatched. Default value depends by system: in Linux around 2MB. If you see big package drop, increase this value.")
|
||||
flag.BoolVar(&Settings.Promiscuous, "input-raw-promisc", false, "enable promiscuous mode")
|
||||
flag.BoolVar(&Settings.Monitor, "input-raw-monitor", false, "enable RF monitor mode")
|
||||
flag.BoolVar(&Settings.Stats, "input-raw-stats", false, "enable stats generator on raw TCP messages")
|
||||
flag.Var(&MultiOption{&Settings.InputRAW}, "input-raw", "Capture traffic from given port (use RAW sockets and require *sudo* access):\n\t# Capture traffic from 8080 port\n\tgor --input-raw :8080 --output-http staging.com")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.TrackResponse, "input-raw-track-response", false, "If turned on Gor will track responses in addition to requests, and they will be available to middleware and file output.")
|
||||
flag.IntVar(&Settings.InputRAWConfig.VXLANPort, "input-raw-vxlan-port", 4789, "VXLAN port. Can be used only when engine set to `vxlan`. Default: 4789")
|
||||
flag.Var(&MultiIntOption{&Settings.InputRAWConfig.VXLANVNIs}, "input-raw-vxlan-vni", "VXLAN VNI to capture. By default capture all VNIs. Ignore VNI by setting them with minus sign, example: `--input-raw-vxlan-vni -2`")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.VLAN, "input-raw-vlan", false, "Enable VLAN (802.1Q) support")
|
||||
flag.Var(&MultiIntOption{&Settings.InputRAWConfig.VLANVIDs}, "input-raw-vlan-vid", "VLAN VID to capture. By default capture all VIDs")
|
||||
flag.Var(&Settings.InputRAWConfig.Engine, "input-raw-engine", "Intercept traffic using `libpcap` (default), `raw_socket`, `pcap_file`, `vxlan`")
|
||||
flag.Var(&Settings.InputRAWConfig.Protocol, "input-raw-protocol", "Specify application protocol of intercepted traffic. Possible values: http, binary")
|
||||
flag.StringVar(&Settings.InputRAWConfig.RealIPHeader, "input-raw-realip-header", "", "If not blank, injects header with given name and real IP value to the request payload. Usually this header should be named: X-Real-IP")
|
||||
flag.DurationVar(&Settings.InputRAWConfig.Expire, "input-raw-expire", time.Second*2, "How much it should wait for the last TCP packet, till consider that TCP message complete.")
|
||||
flag.StringVar(&Settings.InputRAWConfig.BPFFilter, "input-raw-bpf-filter", "", "BPF filter to write custom expressions. Can be useful in case of non standard network interfaces like tunneling or SPAN port. Example: --input-raw-bpf-filter 'dst port 80'")
|
||||
flag.StringVar(&Settings.InputRAWConfig.TimestampType, "input-raw-timestamp-type", "", "Possible values: PCAP_TSTAMP_HOST, PCAP_TSTAMP_HOST_LOWPREC, PCAP_TSTAMP_HOST_HIPREC, PCAP_TSTAMP_ADAPTER, PCAP_TSTAMP_ADAPTER_UNSYNCED. This values not supported on all systems, GoReplay will tell you available values of you put wrong one.")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.Snaplen, "input-raw-override-snaplen", false, "Override the capture snaplen to be 64k. Required for some Virtualized environments")
|
||||
flag.DurationVar(&Settings.InputRAWConfig.BufferTimeout, "input-raw-buffer-timeout", 0, "set the pcap timeout. for immediate mode don't set this flag")
|
||||
flag.Var(&Settings.InputRAWConfig.BufferSize, "input-raw-buffer-size", "Controls size of the OS buffer which holds packets until they dispatched. Default value depends by system: in Linux around 2MB. If you see big package drop, increase this value.")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.Promiscuous, "input-raw-promisc", false, "enable promiscuous mode")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.Monitor, "input-raw-monitor", false, "enable RF monitor mode")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.Stats, "input-raw-stats", false, "enable stats generator on raw TCP messages")
|
||||
flag.BoolVar(&Settings.InputRAWConfig.AllowIncomplete, "input-raw-allow-incomplete", false, "If turned on Gor will record HTTP messages with missing packets")
|
||||
flag.Var(&MultiOption{&Settings.InputRAWConfig.IgnoreInterface}, "input-raw-ignore-interface", "In case if you want listen for all interfaces except a few ones. Can be used in k8s environment. Example: --input-raw-ignore-interface cbr0 --input-raw-ignore-interface eth0 --input-raw-ignore-interface localhost")
|
||||
|
||||
flag.StringVar(&Settings.Middleware, "middleware", "", "Used for modifying traffic using external command")
|
||||
|
||||
flag.Var(&Settings.OutputHTTP, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com")
|
||||
flag.Var(&MultiOption{&Settings.OutputHTTP}, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com")
|
||||
|
||||
/* outputHTTPConfig */
|
||||
flag.Var(&Settings.OutputHTTPConfig.BufferSize, "output-http-response-buffer", "HTTP response buffer size, all data after this size will be discarded.")
|
||||
@@ -164,7 +224,7 @@ func init() {
|
||||
flag.StringVar(&Settings.OutputHTTPConfig.ElasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'")
|
||||
/* outputHTTPConfig */
|
||||
|
||||
flag.Var(&Settings.OutputBinary, "output-binary", "Forwards incoming binary payloads to given address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --input-raw-protocol binary --output-binary staging.com:80")
|
||||
flag.Var(&MultiOption{&Settings.OutputBinary}, "output-binary", "Forwards incoming binary payloads to given address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --input-raw-protocol binary --output-binary staging.com:80")
|
||||
|
||||
/* outputBinaryConfig */
|
||||
flag.Var(&Settings.OutputBinaryConfig.BufferSize, "output-tcp-response-buffer", "TCP response buffer size, all data after this size will be discarded.")
|
||||
@@ -178,16 +238,24 @@ func init() {
|
||||
flag.StringVar(&Settings.OutputKafkaConfig.Host, "output-kafka-host", "", "Read request and response stats from Kafka:\n\tgor --input-raw :8080 --output-kafka-host '192.168.0.1:9092,192.168.0.2:9092'")
|
||||
flag.StringVar(&Settings.OutputKafkaConfig.Topic, "output-kafka-topic", "", "Read request and response stats from Kafka:\n\tgor --input-raw :8080 --output-kafka-topic 'kafka-log'")
|
||||
flag.BoolVar(&Settings.OutputKafkaConfig.UseJSON, "output-kafka-json-format", false, "If turned on, it will serialize messages from GoReplay text format to JSON.")
|
||||
flag.BoolVar(&Settings.OutputKafkaConfig.SASLConfig.UseSASL, "output-kafka-use-sasl", false, "--output-kafka-use-sasl true")
|
||||
flag.StringVar(&Settings.OutputKafkaConfig.SASLConfig.Mechanism, "output-kafka-mechanism", "", "mechanism\n\tgor --input-raw :8080 --output-kafka-mechanism 'SCRAM-SHA-512'")
|
||||
flag.StringVar(&Settings.OutputKafkaConfig.SASLConfig.Username, "output-kafka-username", "", "username\n\tgor --input-raw :8080 --output-kafka-username 'username'")
|
||||
flag.StringVar(&Settings.OutputKafkaConfig.SASLConfig.Password, "output-kafka-password", "", "password\n\tgor --input-raw :8080 --output-kafka-password 'password'")
|
||||
|
||||
flag.StringVar(&Settings.InputKafkaConfig.Host, "input-kafka-host", "", "Send request and response stats to Kafka:\n\tgor --output-stdout --input-kafka-host '192.168.0.1:9092,192.168.0.2:9092'")
|
||||
flag.StringVar(&Settings.InputKafkaConfig.Topic, "input-kafka-topic", "", "Send request and response stats to Kafka:\n\tgor --output-stdout --input-kafka-topic 'kafka-log'")
|
||||
flag.BoolVar(&Settings.InputKafkaConfig.UseJSON, "input-kafka-json-format", false, "If turned on, it will assume that messages coming in JSON format rather than GoReplay text format.")
|
||||
flag.BoolVar(&Settings.InputKafkaConfig.SASLConfig.UseSASL, "input-kafka-use-sasl", false, "use-sasl\n\t--use-sasl true")
|
||||
flag.StringVar(&Settings.InputKafkaConfig.SASLConfig.Mechanism, "input-kafka-mechanism", "", "mechanism\n\tgor --input-raw :8080 --output-kafka-mechanism 'SCRAM-SHA-512'")
|
||||
flag.StringVar(&Settings.InputKafkaConfig.SASLConfig.Username, "input-kafka-username", "", "username\n\tgor --input-raw :8080 --output-kafka-username 'username'")
|
||||
flag.StringVar(&Settings.InputKafkaConfig.SASLConfig.Password, "input-kafka-password", "", "password\n\tgor --input-raw :8080 --output-kafka-password 'password'")
|
||||
|
||||
flag.StringVar(&Settings.KafkaTLSConfig.CACert, "kafka-tls-ca-cert", "", "CA certificate for Kafka TLS Config:\n\tgor --input-raw :3000 --output-kafka-host '192.168.0.1:9092' --output-kafka-topic 'topic' --kafka-tls-ca-cert cacert.cer.pem --kafka-tls-client-cert client.cer.pem --kafka-tls-client-key client.key.pem")
|
||||
flag.StringVar(&Settings.KafkaTLSConfig.ClientCert, "kafka-tls-client-cert", "", "Client certificate for Kafka TLS Config (mandatory with to kafka-tls-ca-cert and kafka-tls-client-key)")
|
||||
flag.StringVar(&Settings.KafkaTLSConfig.ClientKey, "kafka-tls-client-key", "", "Client Key for Kafka TLS Config (mandatory with to kafka-tls-client-cert and kafka-tls-client-key)")
|
||||
|
||||
flag.Var(&Settings.ModifierConfig.Headers, "http-set-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'")
|
||||
flag.Var(&Settings.ModifierConfig.Headers, "http-set-header", "Inject additional headers to http request:\n\tgor --input-raw :8080 --output-http staging.com --http-set-header 'User-Agent: Gor'")
|
||||
flag.Var(&Settings.ModifierConfig.HeaderRewrite, "http-rewrite-header", "Rewrite the request header based on a mapping:\n\tgor --input-raw :8080 --output-http staging.com --http-rewrite-header Host: (.*).example.com,$1.beta.example.com")
|
||||
flag.Var(&Settings.ModifierConfig.Params, "http-set-param", "Set request url param, if param already exists it will be overwritten:\n\tgor --input-raw :8080 --output-http staging.com --http-set-param api_key=1")
|
||||
flag.Var(&Settings.ModifierConfig.Methods, "http-allow-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --http-allow-method GET --http-allow-method OPTIONS")
|
||||
@@ -207,7 +275,7 @@ func init() {
|
||||
|
||||
}
|
||||
|
||||
func checkSettings() {
|
||||
func CheckSettings() {
|
||||
if Settings.OutputFileConfig.SizeLimit < 1 {
|
||||
Settings.OutputFileConfig.SizeLimit.Set("32mb")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package goreplay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/buger/goreplay/capture"
|
||||
"github.com/buger/goreplay/size"
|
||||
)
|
||||
|
||||
// Stats every message carry its own stats object
|
||||
type Stats struct {
|
||||
LostData int
|
||||
Length int // length of the data
|
||||
Start time.Time // first packet's timestamp
|
||||
End time.Time // last packet's timestamp
|
||||
SrcAddr string
|
||||
DstAddr string
|
||||
IsIncoming bool
|
||||
TimedOut bool // timeout before getting the whole message
|
||||
Truncated bool // last packet truncated due to max message size
|
||||
IPversion byte
|
||||
}
|
||||
|
||||
// Message is the representation of a tcp message
|
||||
type Message struct {
|
||||
packets []*Packet
|
||||
done chan bool
|
||||
pool *MessagePool
|
||||
buf *bytes.Buffer
|
||||
feedback interface{}
|
||||
Stats
|
||||
}
|
||||
|
||||
// NewMessage ...
|
||||
func NewMessage(srcAddr, dstAddr string, ipVersion uint8) (m *Message) {
|
||||
m = new(Message)
|
||||
m.DstAddr = dstAddr
|
||||
m.SrcAddr = srcAddr
|
||||
m.IPversion = ipVersion
|
||||
m.done = make(chan bool)
|
||||
m.buf = &bytes.Buffer{}
|
||||
return
|
||||
}
|
||||
|
||||
// UUID returns the UUID of a TCP request and its response.
|
||||
func (m *Message) UUID() []byte {
|
||||
var key uint64
|
||||
pckt := m.packets[0]
|
||||
|
||||
// check if response or request have generated the ID before.
|
||||
if m.pool.uuids != nil {
|
||||
lst := len(pckt.SrcIP) - 4
|
||||
if m.IsIncoming {
|
||||
key = uint64(pckt.SrcPort)<<48 | uint64(pckt.DstPort)<<32 |
|
||||
uint64(_uint32(pckt.SrcIP[lst:]))
|
||||
} else {
|
||||
key = uint64(pckt.DstPort)<<48 | uint64(pckt.SrcPort)<<32 |
|
||||
uint64(_uint32(pckt.DstIP[lst:]))
|
||||
}
|
||||
if uuidHex, ok := m.pool.uuids[key]; ok {
|
||||
delete(m.pool.uuids, key)
|
||||
return uuidHex
|
||||
}
|
||||
}
|
||||
|
||||
id := make([]byte, 12, 12)
|
||||
binary.BigEndian.PutUint32(id, pckt.Seq)
|
||||
tStamp := m.End.UnixNano()
|
||||
binary.BigEndian.PutUint64(id[4:], uint64(tStamp))
|
||||
uuidHex := make([]byte, 24, 24)
|
||||
hex.Encode(uuidHex[:], id[:])
|
||||
if m.pool.uuids != nil {
|
||||
if len(m.pool.uuids) >= 1000 {
|
||||
m.pool.cleanUUIDs()
|
||||
}
|
||||
m.pool.uuids[key] = uuidHex
|
||||
}
|
||||
return uuidHex
|
||||
}
|
||||
|
||||
func (m *Message) add(pckt *Packet) {
|
||||
m.Length += len(pckt.Payload)
|
||||
m.LostData += int(pckt.Lost)
|
||||
m.packets = append(m.packets, pckt)
|
||||
m.End = pckt.Timestamp
|
||||
m.buf.Write(pckt.Payload)
|
||||
}
|
||||
|
||||
// Packets returns packets of the message
|
||||
func (m *Message) Packets() []*Packet {
|
||||
return m.packets
|
||||
}
|
||||
|
||||
// Data returns data in this message
|
||||
func (m *Message) Data() []byte {
|
||||
return m.buf.Bytes()
|
||||
}
|
||||
|
||||
// SetFeedback set feedback/data that can be used later, e.g with End or Start hint
|
||||
func (m *Message) SetFeedback(feedback interface{}) {
|
||||
m.feedback = feedback
|
||||
}
|
||||
|
||||
// Feedback returns feedback associated to this message
|
||||
func (m *Message) Feedback() interface{} {
|
||||
return m.feedback
|
||||
}
|
||||
|
||||
// Sort a helper to sort packets
|
||||
func (m *Message) Sort() {
|
||||
sort.SliceStable(m.packets, func(i, j int) bool { return m.packets[i].Seq < m.packets[j].Seq })
|
||||
}
|
||||
|
||||
// Handler message handler
|
||||
type Handler func(*Message)
|
||||
|
||||
// Debugger is the debugger function. first params is the indicator of the issue's priority
|
||||
// the higher the number, the lower the priority. it can be 4 <= level <= 6.
|
||||
type Debugger func(int, ...interface{})
|
||||
|
||||
// HintEnd hints the pool to stop the session, see MessagePool.End
|
||||
// when set, it will be executed before checking FIN or RST flag
|
||||
type HintEnd func(*Message) bool
|
||||
|
||||
// HintStart hints the pool to start the reassembling the message, see MessagePool.Start
|
||||
// when set, it will be called after checking SYN flag
|
||||
type HintStart func(*Packet) (IsIncoming, IsOutgoing bool)
|
||||
|
||||
// MessagePool holds data of all tcp messages in progress(still receiving/sending packets).
|
||||
// message is identified by its source port and dst port, and last 4bytes of src IP.
|
||||
type MessagePool struct {
|
||||
sync.Mutex
|
||||
debug Debugger
|
||||
maxSize size.Size // maximum message size, default 5mb
|
||||
pool map[uint64]*Message
|
||||
uuids map[uint64][]byte
|
||||
handler Handler
|
||||
messageExpire time.Duration // the maximum time to wait for the final packet, minimum is 100ms
|
||||
End HintEnd
|
||||
Start HintStart
|
||||
}
|
||||
|
||||
// NewMessagePool returns a new instance of message pool
|
||||
func NewMessagePool(maxSize size.Size, messageExpire time.Duration, debugger Debugger, handler Handler) (pool *MessagePool) {
|
||||
pool = new(MessagePool)
|
||||
pool.debug = debugger
|
||||
pool.handler = handler
|
||||
pool.messageExpire = time.Millisecond * 100
|
||||
if pool.messageExpire < messageExpire {
|
||||
pool.messageExpire = messageExpire
|
||||
}
|
||||
pool.maxSize = maxSize
|
||||
if pool.maxSize < 1 {
|
||||
pool.maxSize = 5 << 20
|
||||
}
|
||||
pool.pool = make(map[uint64]*Message)
|
||||
return pool
|
||||
}
|
||||
|
||||
// Handler returns packet handler
|
||||
func (pool *MessagePool) Handler(packet *capture.Packet) {
|
||||
var in, out bool
|
||||
pckt, err := ParsePacket(packet)
|
||||
if err != nil {
|
||||
go pool.say(4, fmt.Sprintf("error decoding packet(%dBytes):%s\n", packet.Info.CaptureLength, err))
|
||||
return
|
||||
}
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
lst := len(pckt.SrcIP) - 4
|
||||
key := uint64(pckt.SrcPort)<<48 | uint64(pckt.DstPort)<<32 |
|
||||
uint64(_uint32(pckt.SrcIP[lst:]))
|
||||
m, ok := pool.pool[key]
|
||||
if pckt.RST {
|
||||
if ok {
|
||||
m.done <- true
|
||||
<-m.done
|
||||
}
|
||||
key = uint64(pckt.DstPort)<<48 | uint64(pckt.SrcPort)<<32 |
|
||||
uint64(_uint32(pckt.DstIP[lst:]))
|
||||
m, ok = pool.pool[key]
|
||||
if ok {
|
||||
m.done <- true
|
||||
<-m.done
|
||||
}
|
||||
go pool.say(4, fmt.Sprintf("RST flag from %s to %s at %s\n", pckt.Src(), pckt.Dst(), pckt.Timestamp))
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case ok:
|
||||
pool.addPacket(m, pckt)
|
||||
return
|
||||
case pckt.SYN:
|
||||
in = !pckt.ACK
|
||||
case pool.Start != nil:
|
||||
if in, out = pool.Start(pckt); !(in || out) {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
m = NewMessage(pckt.Src(), pckt.Dst(), pckt.Version)
|
||||
m.IsIncoming = in
|
||||
pool.pool[key] = m
|
||||
m.Start = pckt.Timestamp
|
||||
m.pool = pool
|
||||
go pool.dispatch(key, m)
|
||||
pool.addPacket(m, pckt)
|
||||
}
|
||||
|
||||
// MatchUUID instructs the pool to use same UUID for request and responses
|
||||
// this function should be called at initial stage of the pool
|
||||
func (pool *MessagePool) MatchUUID(match bool) {
|
||||
if match {
|
||||
pool.uuids = make(map[uint64][]byte)
|
||||
return
|
||||
}
|
||||
pool.uuids = nil
|
||||
}
|
||||
|
||||
// run GC on UUID map
|
||||
func (pool *MessagePool) cleanUUIDs() {
|
||||
var tStamp int64
|
||||
now := time.Now().UnixNano()
|
||||
for k, v := range pool.uuids {
|
||||
// there is a timestamp wrapped in every ID
|
||||
tStamp = int64(binary.BigEndian.Uint64(v[4:]))
|
||||
if time.Duration(now-tStamp) > pool.messageExpire {
|
||||
delete(pool.uuids, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pool *MessagePool) dispatch(key uint64, m *Message) {
|
||||
select {
|
||||
case <-m.done:
|
||||
defer func() { m.done <- true }() // signal that message was dispatched
|
||||
case <-time.After(pool.messageExpire):
|
||||
pool.Lock()
|
||||
defer pool.Unlock()
|
||||
// avoid dispathing message twice
|
||||
if _, ok := pool.pool[key]; !ok {
|
||||
return
|
||||
}
|
||||
m.TimedOut = true
|
||||
}
|
||||
delete(pool.pool, key)
|
||||
pool.handler(m)
|
||||
}
|
||||
|
||||
func (pool *MessagePool) addPacket(m *Message, pckt *Packet) {
|
||||
trunc := m.Length + len(pckt.Payload) - int(pool.maxSize)
|
||||
if trunc > 0 {
|
||||
m.Truncated = true
|
||||
pckt.Payload = pckt.Payload[:int(pool.maxSize)-m.Length]
|
||||
}
|
||||
m.add(pckt)
|
||||
switch {
|
||||
|
||||
// if one of this cases matches, we dispatch the message
|
||||
case trunc >= 0:
|
||||
case pckt.FIN:
|
||||
case pool.End != nil && pool.End(m):
|
||||
default:
|
||||
// continue to receive packets
|
||||
return
|
||||
}
|
||||
m.done <- true
|
||||
<-m.done
|
||||
}
|
||||
|
||||
// this function should not block other pool operations
|
||||
func (pool *MessagePool) say(level int, args ...interface{}) {
|
||||
if pool.debug != nil {
|
||||
pool.debug(level, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func _uint32(b []byte) uint32 {
|
||||
return binary.BigEndian.Uint32(b)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user