diff --git a/proxy/.gitignore b/proxy/.gitignore new file mode 100644 index 000000000..d7c912040 --- /dev/null +++ b/proxy/.gitignore @@ -0,0 +1,27 @@ +# Binaries +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary +*.test + +# Output of go coverage tool +*.out + +# Configuration files (keep example) +config.json + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/proxy/LICENSE b/proxy/LICENSE new file mode 100644 index 000000000..be3f7b28e --- /dev/null +++ b/proxy/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/proxy/Makefile b/proxy/Makefile new file mode 100644 index 000000000..b0c9b280d --- /dev/null +++ b/proxy/Makefile @@ -0,0 +1,83 @@ +.PHONY: build clean run test help version proto + +# Build variables +BINARY_NAME=proxy +BUILD_DIR=bin + +# Version variables (can be overridden) +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +BUILD_DATE ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S') + +# Go linker flags for version injection +LDFLAGS=-ldflags "-X github.com/netbirdio/netbird/proxy/pkg/version.Version=$(VERSION) \ + -X github.com/netbirdio/netbird/proxy/pkg/version.Commit=$(COMMIT) \ + -X github.com/netbirdio/netbird/proxy/pkg/version.BuildDate=$(BUILD_DATE)" + +# Build the binary +build: + @echo "Building $(BINARY_NAME)..." + @echo "Version: $(VERSION)" + @echo "Commit: $(COMMIT)" + @echo "BuildDate: $(BUILD_DATE)" + @mkdir -p $(BUILD_DIR) + GOWORK=off go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) . + @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)" + +# Show version information +version: + @echo "Version: $(VERSION)" + @echo "Commit: $(COMMIT)" + @echo "BuildDate: $(BUILD_DATE)" + +# Clean build artifacts +clean: + @echo "Cleaning..." + @rm -rf $(BUILD_DIR) + @go clean + @echo "Clean complete" + +# Run the application (requires NB_PROXY_TARGET_URL to be set) +run: build + @./$(BUILD_DIR)/$(BINARY_NAME) + +# Run tests +test: + GOWORK=off go test -v ./... + +# Install dependencies +deps: + @echo "Installing dependencies..." + GOWORK=off go mod download + GOWORK=off go mod tidy + @echo "Dependencies installed" + +# Format code +fmt: + @echo "Formatting code..." + @go fmt ./... + @echo "Format complete" + +# Lint code +lint: + @echo "Linting code..." + @golangci-lint run + @echo "Lint complete" + +# Generate protobuf files +proto: + @echo "Generating protobuf files..." + @./scripts/generate-proto.sh + +# Show help +help: + @echo "Available targets:" + @echo " build - Build the binary" + @echo " clean - Remove build artifacts" + @echo " run - Build and run the application" + @echo " test - Run tests" + @echo " proto - Generate protobuf files" + @echo " deps - Install dependencies" + @echo " fmt - Format code" + @echo " lint - Lint code" + @echo " help - Show this help message" \ No newline at end of file diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 000000000..8b7ae771d --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,177 @@ +# Netbird Reverse Proxy + +A lightweight, configurable reverse proxy server with graceful shutdown support. + +## Features + +- Simple reverse proxy with customizable headers +- Configuration via environment variables or JSON file +- Graceful shutdown with configurable timeout +- Structured logging with logrus +- Configurable timeouts (read, write, idle) +- Health monitoring support + +## Building + +```bash +# Build the binary +GOWORK=off go build -o bin/proxy ./cmd/proxy + +# Or use make if available +make build +``` + +## Configuration + +The proxy can be configured using either environment variables or a JSON configuration file. Environment variables take precedence over file-based configuration. + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `NB_PROXY_LISTEN_ADDRESS` | Address to listen on | `:8080` | +| `NB_PROXY_TARGET_URL` | Target URL to proxy requests to | **(required)** | +| `NB_PROXY_READ_TIMEOUT` | Read timeout duration | `30s` | +| `NB_PROXY_WRITE_TIMEOUT` | Write timeout duration | `30s` | +| `NB_PROXY_IDLE_TIMEOUT` | Idle timeout duration | `60s` | +| `NB_PROXY_SHUTDOWN_TIMEOUT` | Graceful shutdown timeout | `10s` | +| `NB_PROXY_LOG_LEVEL` | Log level (debug, info, warn, error) | `info` | + +### Configuration File + +Create a JSON configuration file: + +```json +{ + "listen_address": ":8080", + "target_url": "http://localhost:3000", + "read_timeout": "30s", + "write_timeout": "30s", + "idle_timeout": "60s", + "shutdown_timeout": "10s", + "log_level": "info" +} +``` + +## Usage + +### Using Environment Variables + +```bash +export NB_PROXY_TARGET_URL=http://localhost:3000 +export NB_PROXY_LOG_LEVEL=debug +./bin/proxy +``` + +### Using Configuration File + +```bash +./bin/proxy -config config.json +``` + +### Combining Both + +Environment variables override file configuration: + +```bash +export NB_PROXY_LOG_LEVEL=debug +./bin/proxy -config config.json +``` + +### Docker Example + +```bash +docker run -e NB_PROXY_TARGET_URL=http://backend:3000 \ + -e NB_PROXY_LISTEN_ADDRESS=:8080 \ + -p 8080:8080 \ + netbird-proxy +``` + +## Architecture + +The application follows a clean architecture with clear separation of concerns: + +``` +proxy/ +├── cmd/ +│ └── proxy/ +│ └── main.go # Entry point, CLI handling, signal management +├── config.go # Configuration loading and validation +├── server.go # Server lifecycle (Start/Stop) +├── go.mod # Module dependencies +└── README.md +``` + +### Key Components + +- **config.go**: Handles configuration loading from environment variables and files using the `github.com/caarlos0/env/v11` library +- **server.go**: Encapsulates the HTTP server and reverse proxy logic with proper lifecycle management +- **cmd/proxy/main.go**: Entry point that orchestrates startup, graceful shutdown, and signal handling + +## Graceful Shutdown + +The server handles SIGINT and SIGTERM signals for graceful shutdown: + +1. Signal received (Ctrl+C or kill command) +2. Server stops accepting new connections +3. Existing connections are allowed to complete within the shutdown timeout +4. Server exits cleanly + +Press `Ctrl+C` to trigger graceful shutdown: + +```bash +^C2026-01-13 22:40:00 INFO Received signal: interrupt +2026-01-13 22:40:00 INFO Shutting down server gracefully... +2026-01-13 22:40:00 INFO Server stopped successfully +2026-01-13 22:40:00 INFO Server exited successfully +``` + +## Headers + +The proxy automatically sets the following headers on proxied requests: + +- `X-Forwarded-Host`: Original request host +- `X-Origin-Host`: Target backend host +- `X-Real-IP`: Client's remote address + +## Error Handling + +- Invalid backend connections return `502 Bad Gateway` +- All proxy errors are logged with details +- Configuration errors are reported at startup + +## Development + +### Prerequisites + +- Go 1.25 or higher +- Access to `github.com/sirupsen/logrus` +- Access to `github.com/caarlos0/env/v11` + +### Testing Locally + +Start a test backend: + +```bash +# Terminal 1: Start a simple backend +python3 -m http.server 3000 +``` + +Start the proxy: + +```bash +# Terminal 2: Start the proxy +export NB_PROXY_TARGET_URL=http://localhost:3000 +./bin/proxy +``` + +Test the proxy: + +```bash +# Terminal 3: Make requests +curl http://localhost:8080 +``` + +## License + +Part of the Netbird project. \ No newline at end of file diff --git a/proxy/cmd/root.go b/proxy/cmd/root.go new file mode 100644 index 000000000..337c15c15 --- /dev/null +++ b/proxy/cmd/root.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "syscall" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/netbirdio/netbird/proxy/pkg/proxy" + "github.com/netbirdio/netbird/proxy/pkg/version" +) + +var ( + configFile string + rootCmd = &cobra.Command{ + Use: "proxy", + Short: "Netbird Reverse Proxy Server", + Long: "A lightweight, configurable reverse proxy server.", + SilenceUsage: true, + SilenceErrors: true, + RunE: run, + } +) + +func init() { + rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "path to JSON configuration file (optional, can use env vars instead)") + + // Set version information + rootCmd.Version = version.Short() + rootCmd.SetVersionTemplate("{{.Version}}\n") +} + +// Execute runs the root command +func Execute() error { + return rootCmd.Execute() +} + +func run(cmd *cobra.Command, args []string) error { + // Load configuration from file or environment variables + config, err := proxy.LoadFromFileOrEnv(configFile) + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + return err + } + + // Set log level + setupLogging(config.LogLevel) + + log.Infof("Starting Netbird Proxy - %s", version.Short()) + log.Debugf("Full version info: %s", version.String()) + log.Info("Configuration loaded successfully") + log.Infof("Listen Address: %s", config.ListenAddress) + log.Infof("Log Level: %s", config.LogLevel) + + // Create server instance + server, err := proxy.NewServer(config) + if err != nil { + log.Fatalf("Failed to create server: %v", err) + return err + } + + // Start server in a goroutine + serverErrors := make(chan error, 1) + go func() { + if err := server.Start(); err != nil { + serverErrors <- err + } + }() + + // Set up signal handler for graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + // Wait for either an error or shutdown signal + select { + case err := <-serverErrors: + log.Fatalf("Server error: %v", err) + return err + case sig := <-quit: + log.Infof("Received signal: %v", sig) + } + + // Create shutdown context with timeout + ctx, cancel := context.WithTimeout(context.Background(), config.ShutdownTimeout) + defer cancel() + + // Gracefully stop the server + if err := server.Stop(ctx); err != nil { + log.Fatalf("Failed to stop server gracefully: %v", err) + return err + } + + log.Info("Server exited successfully") + return nil +} + +func setupLogging(level string) { + // Set log format + log.SetFormatter(&log.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + }) + + // Set log level + switch level { + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warn": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + default: + log.SetLevel(log.InfoLevel) + } +} diff --git a/proxy/config.example.json b/proxy/config.example.json new file mode 100644 index 000000000..40ad7ca2b --- /dev/null +++ b/proxy/config.example.json @@ -0,0 +1,9 @@ +{ + "listen_address": ":8080", + "target_url": "http://localhost:3000", + "read_timeout": "30s", + "write_timeout": "30s", + "idle_timeout": "60s", + "shutdown_timeout": "10s", + "log_level": "info" +} \ No newline at end of file diff --git a/proxy/go.mod b/proxy/go.mod new file mode 100644 index 000000000..b5e8a442a --- /dev/null +++ b/proxy/go.mod @@ -0,0 +1,135 @@ +module github.com/netbirdio/netbird/proxy + +go 1.25 + +require ( + github.com/caarlos0/env/v11 v11.3.1 + github.com/caddyserver/caddy/v2 v2.10.2 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.10.2 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go/auth v0.16.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + dario.cat/mergo v1.0.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect + github.com/KimMachineGun/automemlimit v0.7.4 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caddyserver/certmagic v0.24.0 // indirect + github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/ccoveille/go-safecast v1.6.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/coreos/go-oidc/v3 v3.14.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/dgraph-io/badger v1.6.2 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.2.0 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/francoispqt/gojay v1.2.13 // indirect + github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/libdns/libdns v1.1.0 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/acmez/v3 v3.1.2 // indirect + github.com/miekg/dns v1.1.63 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-ps v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/slackhq/nebula v1.9.5 // indirect + github.com/smallstep/certificates v0.28.4 // indirect + github.com/smallstep/cli-utils v0.12.1 // indirect + github.com/smallstep/linkedca v0.23.0 // indirect + github.com/smallstep/nosql v0.7.0 // indirect + github.com/smallstep/pkcs7 v0.2.1 // indirect + github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 // indirect + github.com/smallstep/truststore v0.13.0 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect + github.com/urfave/cli v1.22.17 // indirect + github.com/zeebo/blake3 v0.2.4 // indirect + go.etcd.io/bbolt v1.3.10 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.step.sm/crypto v0.67.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/mock v0.5.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/api v0.240.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + howett.net/plist v1.0.0 // indirect +) diff --git a/proxy/go.sum b/proxy/go.sum new file mode 100644 index 000000000..20bec14c3 --- /dev/null +++ b/proxy/go.sum @@ -0,0 +1,641 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= +cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= +cloud.google.com/go/kms v1.22.0 h1:dBRIj7+GDeeEvatJeTB19oYZNV0aj6wEqSIT/7gLqtk= +cloud.google.com/go/kms v1.22.0/go.mod h1:U7mf8Sva5jpOb4bxYZdtw/9zsbIjrklYwPcvMk34AL8= +cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE= +cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KimMachineGun/automemlimit v0.7.4 h1:UY7QYOIfrr3wjjOAqahFmC3IaQCLWvur9nmfIn6LnWk= +github.com/KimMachineGun/automemlimit v0.7.4/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw= +github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= +github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= +github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/caddyserver/caddy/v2 v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8= +github.com/caddyserver/caddy/v2 v2.10.2/go.mod h1:TXLQHx+ev4HDpkO6PnVVHUbL6OXt6Dfe7VcIBdQnPL0= +github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0= +github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE= +github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= +github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q= +github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +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/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= +github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= +github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= +github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= +github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY= +github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745 h1:heyoXNxkRT155x4jTAiSv5BVSVkueifPUm+Q8LUXMRo= +github.com/google/certificate-transparency-go v1.1.8-0.20240110162603-74a5dd331745/go.mod h1:zN0wUQgV9LjwLZeFHnrAbQi8hzMVvEWePyk+MhPOk7k= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= +github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.4.5 h1:3fhthtyMDbIZFR5/0y1hvUoZ1Kf4i1eZ7C73R4Pvd+k= +github.com/google/go-tpm-tools v0.4.5/go.mod h1:ktjTNq8yZFD6TzdBFefUfen96rF3NpYwpSb2d8bc+Y8= +github.com/google/go-tspi v0.3.0 h1:ADtq8RKfP+jrTyIWIZDIYcKOMecRqNJFOew2IT0Inus= +github.com/google/go-tspi v0.3.0/go.mod h1:xfMGI3G0PhxCdNVcYr1C4C+EizojDg/TXuX5by8CiHI= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU= +github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= +github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= +github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +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/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/jsonstore v1.1.0 h1:WZBDjgezFS34CHI+myb4s8GGpir3UMpy7vWoCeO0n6E= +github.com/schollz/jsonstore v1.1.0/go.mod h1:15c6+9guw8vDRyozGjN3FoILt0wpruJk9Pi66vjaZfg= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY= +github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262 h1:unQFBIznI+VYD1/1fApl1A+9VcBk+9dcqGfnePY87LY= +github.com/smallstep/assert v0.0.0-20200723003110-82e2b9b3b262/go.mod h1:MyOHs9Po2fbM1LHej6sBUT8ozbxmMOFG+E+rx/GSGuc= +github.com/smallstep/certificates v0.28.4 h1:JTU6/A5Xes6m+OsR6fw1RACSA362vJc9SOFVG7poBEw= +github.com/smallstep/certificates v0.28.4/go.mod h1:LUqo+7mKZE7FZldlTb0zhU4A0bq4G4+akieFMcTaWvA= +github.com/smallstep/cli-utils v0.12.1 h1:D9QvfbFqiKq3snGZ2xDcXEFrdFJ1mQfPHZMq/leerpE= +github.com/smallstep/cli-utils v0.12.1/go.mod h1:skV2Neg8qjiKPu2fphM89H9bIxNpKiiRTnX9Q6Lc+20= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca h1:VX8L0r8vybH0bPeaIxh4NQzafKQiqvlOn8pmOXbFLO4= +github.com/smallstep/go-attestation v0.4.4-0.20241119153605-2306d5b464ca/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= +github.com/smallstep/linkedca v0.23.0 h1:5W/7EudlK1HcCIdZM68dJlZ7orqCCCyv6bm2l/0JmLU= +github.com/smallstep/linkedca v0.23.0/go.mod h1:7cyRM9soAYySg9ag65QwytcgGOM+4gOlkJ/YA58A9E8= +github.com/smallstep/nosql v0.7.0 h1:YiWC9ZAHcrLCrayfaF+QJUv16I2bZ7KdLC3RpJcnAnE= +github.com/smallstep/nosql v0.7.0/go.mod h1:H5VnKMCbeq9QA6SRY5iqPylfxLfYcLwvUff3onQ8+HU= +github.com/smallstep/pkcs7 v0.0.0-20240911091500-b1cae6277023/go.mod h1:CM5KrX7rxWgwDdMj9yef/pJB2OPgy/56z4IEx2UIbpc= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101 h1:LyZqn24/ZiVg8v9Hq07K6mx6RqPtpDeK+De5vf4QEY4= +github.com/smallstep/scep v0.0.0-20240926084937-8cf1ca453101/go.mod h1:EuKQjYGQwhUa1mgD21zxIgOgUYLsqikJmvxNscxpS/Y= +github.com/smallstep/truststore v0.13.0 h1:90if9htAOblavbMeWlqNLnO9bsjjgVv2hQeQJCi/py4= +github.com/smallstep/truststore v0.13.0/go.mod h1:3tmMp2aLKZ/OA/jnFUB0cYPcho402UG2knuJoPh4j7A= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ= +github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ= +github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= +github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= +go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.step.sm/crypto v0.67.0 h1:1km9LmxMKG/p+mKa1R4luPN04vlJYnRLlLQrWv7egGU= +go.step.sm/crypto v0.67.0/go.mod h1:+AoDpB0mZxbW/PmOXuwkPSpXRgaUaoIK+/Wx/HGgtAU= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810 h1:V5+zy0jmgNYmK1uW/sPpBw8ioFvalrhaUrYWmu1Fpe4= +golang.org/x/crypto/x509roots/fallback v0.0.0-20250305170421-49bf5b80c810/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +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-20190227155943-e225da77a7e6/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= +google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= +google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= +google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= +howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/proxy/internal/health/health.go b/proxy/internal/health/health.go new file mode 100644 index 000000000..18357c693 --- /dev/null +++ b/proxy/internal/health/health.go @@ -0,0 +1,109 @@ +package health + +import ( + "encoding/json" + "net/http" + "sync" + "time" +) + +// Status represents the health status of the application +type Status string + +const ( + StatusHealthy Status = "healthy" + StatusUnhealthy Status = "unhealthy" + StatusDegraded Status = "degraded" +) + +// Check represents a health check +type Check struct { + Name string `json:"name"` + Status Status `json:"status"` + Error string `json:"error,omitempty"` +} + +// Response represents the health check response +type Response struct { + Status Status `json:"status"` + Timestamp time.Time `json:"timestamp"` + Uptime time.Duration `json:"uptime_seconds"` + Checks map[string]Check `json:"checks,omitempty"` +} + +// Checker is the interface for health checks +type Checker interface { + Check() Check +} + +// Handler manages health checks +type Handler struct { + mu sync.RWMutex + checkers map[string]Checker + startTime time.Time +} + +// NewHandler creates a new health check handler +func NewHandler() *Handler { + return &Handler{ + checkers: make(map[string]Checker), + startTime: time.Now(), + } +} + +// RegisterChecker registers a health checker +func (h *Handler) RegisterChecker(name string, checker Checker) { + h.mu.Lock() + defer h.mu.Unlock() + h.checkers[name] = checker +} + +// ServeHTTP handles health check requests +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.mu.RLock() + defer h.mu.RUnlock() + + response := Response{ + Status: StatusHealthy, + Timestamp: time.Now(), + Uptime: time.Since(h.startTime), + Checks: make(map[string]Check), + } + + // Run all health checks + for name, checker := range h.checkers { + check := checker.Check() + response.Checks[name] = check + + // Update overall status + if check.Status == StatusUnhealthy { + response.Status = StatusUnhealthy + } else if check.Status == StatusDegraded && response.Status != StatusUnhealthy { + response.Status = StatusDegraded + } + } + + // Set HTTP status code based on health + statusCode := http.StatusOK + if response.Status == StatusUnhealthy { + statusCode = http.StatusServiceUnavailable + } else if response.Status == StatusDegraded { + statusCode = http.StatusOK // Still return 200 for degraded + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(response) +} + +// ReadinessHandler returns a simple readiness probe handler +func ReadinessHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ready")) +} + +// LivenessHandler returns a simple liveness probe handler +func LivenessHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("alive")) +} diff --git a/proxy/internal/middleware/chain.go b/proxy/internal/middleware/chain.go new file mode 100644 index 000000000..41f54e5e0 --- /dev/null +++ b/proxy/internal/middleware/chain.go @@ -0,0 +1,12 @@ +package middleware + +import "net/http" + +// Chain creates a middleware chain +func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler { + // Apply middlewares in reverse order so they execute in the order provided + for i := len(middlewares) - 1; i >= 0; i-- { + handler = middlewares[i](handler) + } + return handler +} diff --git a/proxy/internal/middleware/logging.go b/proxy/internal/middleware/logging.go new file mode 100644 index 000000000..df83515a4 --- /dev/null +++ b/proxy/internal/middleware/logging.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "net/http" + "time" + + log "github.com/sirupsen/logrus" +) + +// responseWriter wraps http.ResponseWriter to capture status code +type responseWriter struct { + http.ResponseWriter + statusCode int + written int64 +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + n, err := rw.ResponseWriter.Write(b) + rw.written += int64(n) + return n, err +} + +// Logging middleware logs HTTP requests with details +func Logging(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Wrap the response writer + wrapped := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + // Call the next handler + next.ServeHTTP(wrapped, r) + + // Log request details + duration := time.Since(start) + + log.WithFields(log.Fields{ + "method": r.Method, + "path": r.URL.Path, + "status": wrapped.statusCode, + "duration_ms": duration.Milliseconds(), + "bytes": wrapped.written, + "remote_addr": r.RemoteAddr, + "user_agent": r.UserAgent(), + }).Info("HTTP request") + }) +} diff --git a/proxy/internal/middleware/recovery.go b/proxy/internal/middleware/recovery.go new file mode 100644 index 000000000..97e1785c0 --- /dev/null +++ b/proxy/internal/middleware/recovery.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "fmt" + "net/http" + "runtime/debug" + + log "github.com/sirupsen/logrus" +) + +// Recovery middleware recovers from panics and logs the error +func Recovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + // Log the panic with stack trace + log.WithFields(log.Fields{ + "error": err, + "method": r.Method, + "path": r.URL.Path, + "stack": string(debug.Stack()), + "remote_addr": r.RemoteAddr, + }).Error("Panic recovered") + + // Return 500 Internal Server Error + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "Internal Server Error") + } + }() + + next.ServeHTTP(w, r) + }) +} diff --git a/proxy/internal/reverseproxy/caddy.go b/proxy/internal/reverseproxy/caddy.go new file mode 100644 index 000000000..88966fb11 --- /dev/null +++ b/proxy/internal/reverseproxy/caddy.go @@ -0,0 +1,626 @@ +package reverseproxy + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "sort" + "sync" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy" + "github.com/caddyserver/caddy/v2/modules/logging" + log "github.com/sirupsen/logrus" +) + +// CaddyProxy wraps Caddy's reverse proxy functionality +type CaddyProxy struct { + config Config + mu sync.RWMutex + isRunning bool + routes map[string]*RouteConfig // key is route ID + requestCallback RequestDataCallback + // customHandlers stores handlers with custom transports that can't be JSON-serialized + // key is "routeID:path" to uniquely identify each handler + customHandlers map[string]*reverseproxy.Handler +} + +// Config holds the reverse proxy configuration +type Config struct { + // ListenAddress is the address to listen on + ListenAddress string + + // EnableHTTPS enables automatic HTTPS with Let's Encrypt + EnableHTTPS bool + + // TLSEmail is the email for Let's Encrypt registration + TLSEmail string + + // RequestDataCallback is called for each proxied request with metrics + RequestDataCallback RequestDataCallback +} + +// RouteConfig defines a routing configuration +type RouteConfig struct { + // ID is a unique identifier for this route + ID string + + // Domain is the domain to listen on (e.g., "example.com" or "*" for all) + Domain string + + // PathMappings defines paths that should be forwarded to specific ports + // Key is the path prefix (e.g., "/", "/api", "/admin") + // Value is the target IP:port (e.g., "192.168.1.100:3000") + // Must have at least one entry. Use "/" or "" for the default/catch-all route. + PathMappings map[string]string + + // Conn is an optional existing network connection to use for this route + // This allows routing through specific tunnels (e.g., WireGuard) per route + // If set, this connection will be reused for all requests to this route + Conn net.Conn + + // CustomDialer is an optional custom dialer for this specific route + // This is used if Conn is not set. It allows using different network connections per route + CustomDialer func(ctx context.Context, network, address string) (net.Conn, error) +} + +// New creates a new Caddy-based reverse proxy +func New(config Config) (*CaddyProxy, error) { + // Default to port 443 if not specified + if config.ListenAddress == "" { + config.ListenAddress = ":443" + } + + cp := &CaddyProxy{ + config: config, + isRunning: false, + routes: make(map[string]*RouteConfig), + requestCallback: config.RequestDataCallback, + customHandlers: make(map[string]*reverseproxy.Handler), + } + + return cp, nil +} + +// Start starts the Caddy reverse proxy server +func (cp *CaddyProxy) Start() error { + cp.mu.Lock() + if cp.isRunning { + cp.mu.Unlock() + return fmt.Errorf("reverse proxy already running") + } + cp.isRunning = true + cp.mu.Unlock() + + // Build Caddy configuration + cfg, err := cp.buildCaddyConfig() + if err != nil { + cp.mu.Lock() + cp.isRunning = false + cp.mu.Unlock() + return fmt.Errorf("failed to build Caddy config: %w", err) + } + + // Run Caddy with the configuration + err = caddy.Run(cfg) + if err != nil { + cp.mu.Lock() + cp.isRunning = false + cp.mu.Unlock() + return fmt.Errorf("failed to run Caddy: %w", err) + } + + log.Infof("Caddy reverse proxy started on %s", cp.config.ListenAddress) + log.Infof("Configured %d route(s)", len(cp.routes)) + + return nil +} + +// Stop gracefully stops the Caddy reverse proxy +func (cp *CaddyProxy) Stop(ctx context.Context) error { + cp.mu.Lock() + if !cp.isRunning { + cp.mu.Unlock() + return fmt.Errorf("reverse proxy not running") + } + cp.mu.Unlock() + + log.Info("Stopping Caddy reverse proxy...") + + // Stop Caddy + if err := caddy.Stop(); err != nil { + return fmt.Errorf("failed to stop Caddy: %w", err) + } + + cp.mu.Lock() + cp.isRunning = false + cp.mu.Unlock() + + log.Info("Caddy reverse proxy stopped") + return nil +} + +// buildCaddyConfig builds the Caddy configuration +func (cp *CaddyProxy) buildCaddyConfig() (*caddy.Config, error) { + cp.mu.RLock() + defer cp.mu.RUnlock() + + if len(cp.routes) == 0 { + // Create a default empty server that returns 404 + httpServer := &caddyhttp.Server{ + Listen: []string{cp.config.ListenAddress}, + Routes: caddyhttp.RouteList{}, + } + + httpApp := &caddyhttp.App{ + Servers: map[string]*caddyhttp.Server{ + "proxy": httpServer, + }, + } + + cfg := &caddy.Config{ + Admin: &caddy.AdminConfig{ + Disabled: true, + }, + AppsRaw: caddy.ModuleMap{ + "http": caddyconfig.JSON(httpApp, nil), + }, + } + + return cfg, nil + } + + // Build routes grouped by domain + domainRoutes := make(map[string][]caddyhttp.Route) + // Track unique service IDs for logger configuration + serviceIDs := make(map[string]bool) + + for _, routeConfig := range cp.routes { + domain := routeConfig.Domain + if domain == "" { + domain = "*" // wildcard for all domains + } + + // Register callback for this service ID + if cp.requestCallback != nil { + RegisterCallback(routeConfig.ID, cp.requestCallback) + serviceIDs[routeConfig.ID] = true + } + + // Sort path mappings by path length (longest first) for proper matching + // This ensures more specific paths match before catch-all paths + paths := make([]string, 0, len(routeConfig.PathMappings)) + for path := range routeConfig.PathMappings { + paths = append(paths, path) + } + sort.Slice(paths, func(i, j int) bool { + // Sort by length descending, but put empty string last (catch-all) + if paths[i] == "" || paths[i] == "/" { + return false + } + if paths[j] == "" || paths[j] == "/" { + return true + } + return len(paths[i]) > len(paths[j]) + }) + + // Create routes for each path mapping + for _, path := range paths { + target := routeConfig.PathMappings[path] + route := cp.createRoute(routeConfig, path, target) + domainRoutes[domain] = append(domainRoutes[domain], route) + } + } + + // Build Caddy routes + var caddyRoutes caddyhttp.RouteList + for domain, routes := range domainRoutes { + if domain != "*" { + // Add host matcher for specific domains + for i := range routes { + routes[i].MatcherSetsRaw = []caddy.ModuleMap{ + { + "host": caddyconfig.JSON(caddyhttp.MatchHost{domain}, nil), + }, + } + } + } + caddyRoutes = append(caddyRoutes, routes...) + } + + // Create HTTP server with access logging if callback is set + httpServer := &caddyhttp.Server{ + Listen: []string{cp.config.ListenAddress}, + Routes: caddyRoutes, + } + + // Configure server logging if callback is set + if cp.requestCallback != nil { + httpServer.Logs = &caddyhttp.ServerLogConfig{ + // Use our custom logger for access logs + LoggerNames: map[string]caddyhttp.StringArray{ + "http.log.access": {"http_access"}, + }, + // Disable default access logging (only use custom logger) + ShouldLogCredentials: false, + } + } + + // Disable automatic HTTPS if not enabled + if !cp.config.EnableHTTPS { + // Explicitly disable automatic HTTPS for the server + httpServer.AutoHTTPS = &caddyhttp.AutoHTTPSConfig{ + Disabled: true, + } + } + + // Build HTTP app + httpApp := &caddyhttp.App{ + Servers: map[string]*caddyhttp.Server{ + "proxy": httpServer, + }, + } + + // Provision the HTTP app to set up handlers from JSON + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + if err := httpApp.Provision(ctx); err != nil { + return nil, fmt.Errorf("failed to provision HTTP app: %w", err) + } + + // After provisioning, inject custom transports into handlers + // This is done post-provisioning so the Transport field is preserved + if err := cp.injectCustomTransports(httpApp); err != nil { + return nil, fmt.Errorf("failed to inject custom transports: %w", err) + } + + // Create Caddy config with the provisioned app + // IMPORTANT: We pass the already-provisioned app, not JSON + // This preserves the Transport fields we set + cfg := &caddy.Config{ + Admin: &caddy.AdminConfig{ + Disabled: true, + }, + // Apps field takes already-provisioned apps + Apps: map[string]caddy.App{ + "http": httpApp, + }, + } + + // Configure logging if callback is set + if cp.requestCallback != nil { + // Register the callback for the proxy service ID + RegisterCallback("proxy", cp.requestCallback) + + // Build logging config with proper module names + cfg.Logging = &caddy.Logging{ + Logs: map[string]*caddy.CustomLog{ + "http_access": { + BaseLog: caddy.BaseLog{ + WriterRaw: caddyconfig.JSONModuleObject(&CallbackWriter{ServiceID: "proxy"}, "output", "callback", nil), + EncoderRaw: caddyconfig.JSONModuleObject(&logging.JSONEncoder{}, "format", "json", nil), + Level: "INFO", + }, + Include: []string{"http.log.access"}, + }, + }, + } + + log.Infof("Configured custom logging with callback writer for service: proxy") + } + + return cfg, nil +} + +// createRoute creates a Caddy route for a path and target with service ID tracking +func (cp *CaddyProxy) createRoute(routeConfig *RouteConfig, path, target string) caddyhttp.Route { + // Check if this route needs a custom transport + hasCustomTransport := routeConfig.Conn != nil || routeConfig.CustomDialer != nil + + if hasCustomTransport { + // For routes with custom transports, store them separately + // and configure the upstream to use a special dial address that we'll intercept + handlerKey := fmt.Sprintf("%s:%s", routeConfig.ID, path) + + // Create upstream with custom dial configuration + upstream := &reverseproxy.Upstream{ + Dial: target, + } + + // Create the reverse proxy handler with custom transport + handler := &reverseproxy.Handler{ + Upstreams: reverseproxy.UpstreamPool{upstream}, + } + + // Configure the custom transport + if routeConfig.Conn != nil { + // Use the provided connection directly + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + log.Debugf("Reusing existing connection for route %s to %s", routeConfig.ID, address) + return routeConfig.Conn, nil + }, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: 0, + DisableKeepAlives: false, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + handler.Transport = transport + log.Infof("Configured net.Conn transport for route %s (path: %s)", routeConfig.ID, path) + } else if routeConfig.CustomDialer != nil { + // Use the custom dialer function + transport := &http.Transport{ + DialContext: routeConfig.CustomDialer, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + handler.Transport = transport + log.Infof("Configured custom dialer transport for route %s (path: %s)", routeConfig.ID, path) + } + + // Store the handler for later injection + cp.customHandlers[handlerKey] = handler + + // Create route using HandlersRaw with a placeholder that will be replaced + // We'll use JSON serialization here, but inject the real handler after Caddy loads + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil), + }, + } + + if path != "" { + route.MatcherSetsRaw = []caddy.ModuleMap{ + { + "path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil), + }, + } + } + + return route + } + + // Standard route without custom transport + upstream := &reverseproxy.Upstream{ + Dial: target, + } + + handler := &reverseproxy.Handler{ + Upstreams: reverseproxy.UpstreamPool{upstream}, + } + + route := caddyhttp.Route{ + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(handler, "handler", "reverse_proxy", nil), + }, + } + + if path != "" { + route.MatcherSetsRaw = []caddy.ModuleMap{ + { + "path": caddyconfig.JSON(caddyhttp.MatchPath{path + "*"}, nil), + }, + } + } + + return route +} + +// IsRunning returns whether the proxy is running +func (cp *CaddyProxy) IsRunning() bool { + cp.mu.RLock() + defer cp.mu.RUnlock() + return cp.isRunning +} + +// GetConfig returns the proxy configuration +func (cp *CaddyProxy) GetConfig() Config { + return cp.config +} + +// AddRoute adds a new route configuration to the proxy +// If the proxy is running, it will reload the configuration +func (cp *CaddyProxy) AddRoute(route *RouteConfig) error { + if route == nil { + return fmt.Errorf("route cannot be nil") + } + if route.ID == "" { + return fmt.Errorf("route ID is required") + } + if len(route.PathMappings) == 0 { + return fmt.Errorf("route must have at least one path mapping") + } + + cp.mu.Lock() + // Check if route already exists + if _, exists := cp.routes[route.ID]; exists { + cp.mu.Unlock() + return fmt.Errorf("route with ID %s already exists", route.ID) + } + + // Add new route + cp.routes[route.ID] = route + isRunning := cp.isRunning + cp.mu.Unlock() + + log.WithFields(log.Fields{ + "route_id": route.ID, + "domain": route.Domain, + "paths": len(route.PathMappings), + }).Info("Added route") + + // Reload configuration if proxy is running + if isRunning { + if err := cp.reloadConfig(); err != nil { + // Rollback: remove the route + cp.mu.Lock() + delete(cp.routes, route.ID) + cp.mu.Unlock() + return fmt.Errorf("failed to reload config after adding route: %w", err) + } + } + + return nil +} + +// RemoveRoute removes a route from the proxy +// If the proxy is running, it will reload the configuration +func (cp *CaddyProxy) RemoveRoute(routeID string) error { + cp.mu.Lock() + // Check if route exists + route, exists := cp.routes[routeID] + if !exists { + cp.mu.Unlock() + return fmt.Errorf("route %s not found", routeID) + } + + // Remove route + delete(cp.routes, routeID) + isRunning := cp.isRunning + cp.mu.Unlock() + + log.Infof("Removed route: %s", routeID) + + // Reload configuration if proxy is running + if isRunning { + if err := cp.reloadConfig(); err != nil { + // Rollback: add the route back + cp.mu.Lock() + cp.routes[routeID] = route + cp.mu.Unlock() + return fmt.Errorf("failed to reload config after removing route: %w", err) + } + } + + return nil +} + +// UpdateRoute updates an existing route configuration +// If the proxy is running, it will reload the configuration +func (cp *CaddyProxy) UpdateRoute(route *RouteConfig) error { + if route == nil { + return fmt.Errorf("route cannot be nil") + } + if route.ID == "" { + return fmt.Errorf("route ID is required") + } + + cp.mu.Lock() + // Check if route exists + oldRoute, exists := cp.routes[route.ID] + if !exists { + cp.mu.Unlock() + return fmt.Errorf("route %s not found", route.ID) + } + + // Update route + cp.routes[route.ID] = route + isRunning := cp.isRunning + cp.mu.Unlock() + + log.WithFields(log.Fields{ + "route_id": route.ID, + "domain": route.Domain, + "paths": len(route.PathMappings), + }).Info("Updated route") + + // Reload configuration if proxy is running + if isRunning { + if err := cp.reloadConfig(); err != nil { + // Rollback: restore old route + cp.mu.Lock() + cp.routes[route.ID] = oldRoute + cp.mu.Unlock() + return fmt.Errorf("failed to reload config after updating route: %w", err) + } + } + + return nil +} + +// ListRoutes returns a list of all configured route IDs +func (cp *CaddyProxy) ListRoutes() []string { + cp.mu.RLock() + defer cp.mu.RUnlock() + + routes := make([]string, 0, len(cp.routes)) + for id := range cp.routes { + routes = append(routes, id) + } + return routes +} + +// GetRoute returns a route configuration by ID +func (cp *CaddyProxy) GetRoute(routeID string) (*RouteConfig, error) { + cp.mu.RLock() + defer cp.mu.RUnlock() + + route, exists := cp.routes[routeID] + if !exists { + return nil, fmt.Errorf("route %s not found", routeID) + } + + return route, nil +} + +// injectCustomTransports injects custom transports into provisioned handlers +// This must be called after httpApp.Provision() but before passing to Caddy.Run() +func (cp *CaddyProxy) injectCustomTransports(httpApp *caddyhttp.App) error { + // Iterate through all servers + for serverName, server := range httpApp.Servers { + log.Debugf("Injecting custom transports for server: %s", serverName) + + // Iterate through all routes + for routeIdx, route := range server.Routes { + // Iterate through all handlers in the route + for handlerIdx, handler := range route.Handlers { + // Check if this is a reverse proxy handler + if rpHandler, ok := handler.(*reverseproxy.Handler); ok { + // Try to find a matching custom handler for this route + // We need to match by handler configuration since we don't have route metadata here + for handlerKey, customHandler := range cp.customHandlers { + // Check if the upstream configuration matches + if len(rpHandler.Upstreams) > 0 && len(customHandler.Upstreams) > 0 { + if rpHandler.Upstreams[0].Dial == customHandler.Upstreams[0].Dial { + // Match found! Inject the custom transport + rpHandler.Transport = customHandler.Transport + log.Infof("Injected custom transport for route %d, handler %d (key: %s)", routeIdx, handlerIdx, handlerKey) + break + } + } + } + } + } + } + } + + return nil +} + +// reloadConfig rebuilds and reloads the Caddy configuration +// Must be called without holding the lock +func (cp *CaddyProxy) reloadConfig() error { + log.Info("Reloading Caddy configuration...") + + cfg, err := cp.buildCaddyConfig() + if err != nil { + return fmt.Errorf("failed to build config: %w", err) + } + + if err := caddy.Run(cfg); err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + log.Info("Caddy configuration reloaded successfully") + return nil +} diff --git a/proxy/internal/reverseproxy/logwriter.go b/proxy/internal/reverseproxy/logwriter.go new file mode 100644 index 000000000..c8532f81f --- /dev/null +++ b/proxy/internal/reverseproxy/logwriter.go @@ -0,0 +1,225 @@ +package reverseproxy + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "sync" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + log "github.com/sirupsen/logrus" +) + +var ( + // Global map to store callbacks per service ID + callbackRegistry = make(map[string]RequestDataCallback) + callbackMu sync.RWMutex +) + +// RegisterCallback registers a callback for a specific service ID +func RegisterCallback(serviceID string, callback RequestDataCallback) { + callbackMu.Lock() + defer callbackMu.Unlock() + callbackRegistry[serviceID] = callback +} + +// UnregisterCallback removes a callback for a specific service ID +func UnregisterCallback(serviceID string) { + callbackMu.Lock() + defer callbackMu.Unlock() + delete(callbackRegistry, serviceID) +} + +// getCallback retrieves the callback for a service ID +func getCallback(serviceID string) RequestDataCallback { + callbackMu.RLock() + defer callbackMu.RUnlock() + return callbackRegistry[serviceID] +} + +func init() { + caddy.RegisterModule(CallbackWriter{}) +} + +// CallbackWriter is a Caddy log writer module that sends request data via callback +type CallbackWriter struct { + ServiceID string `json:"service_id,omitempty"` +} + +// CaddyModule returns the Caddy module information +func (CallbackWriter) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.writers.callback", + New: func() caddy.Module { return new(CallbackWriter) }, + } +} + +// Provision sets up the callback writer +func (cw *CallbackWriter) Provision(ctx caddy.Context) error { + log.Infof("CallbackWriter.Provision called for service_id: %s", cw.ServiceID) + return nil +} + +// String returns a human-readable representation of the writer +func (cw *CallbackWriter) String() string { + return fmt.Sprintf("callback writer for service %s", cw.ServiceID) +} + +// WriterKey returns a unique key for this writer configuration +func (cw *CallbackWriter) WriterKey() string { + return "callback_" + cw.ServiceID +} + +// OpenWriter opens the writer +func (cw *CallbackWriter) OpenWriter() (io.WriteCloser, error) { + log.Infof("CallbackWriter.OpenWriter called for service_id: %s", cw.ServiceID) + writer := &LogWriter{ + serviceID: cw.ServiceID, + } + log.Infof("Created LogWriter instance: %p for service_id: %s", writer, cw.ServiceID) + return writer, nil +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler +func (cw *CallbackWriter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + cw.ServiceID = d.Val() + } + return nil +} + +// Ensure CallbackWriter implements the required interfaces +var ( + _ caddy.Provisioner = (*CallbackWriter)(nil) + _ caddy.WriterOpener = (*CallbackWriter)(nil) + _ caddyfile.Unmarshaler = (*CallbackWriter)(nil) +) + +// LogWriter is a custom io.Writer that parses Caddy's structured JSON logs +// and extracts request metrics to send via callback +type LogWriter struct { + serviceID string +} + +// NewLogWriter creates a new log writer with the given service ID +func NewLogWriter(serviceID string) *LogWriter { + return &LogWriter{ + serviceID: serviceID, + } +} + +// Write implements io.Writer +func (lw *LogWriter) Write(p []byte) (n int, err error) { + // DEBUG: Log that we received data + log.Infof("LogWriter.Write called with %d bytes for service_id: %s", len(p), lw.serviceID) + log.Debugf("LogWriter content: %s", string(p)) + + // Caddy writes one JSON object per line + // Parse the JSON to extract request metrics + var logEntry map[string]interface{} + if err := json.Unmarshal(p, &logEntry); err != nil { + // Not JSON or malformed, skip + log.Debugf("Failed to unmarshal JSON: %v", err) + return len(p), nil + } + + // Caddy access logs have a nested "request" object + // Check if this is an access log entry by looking for "request" field + requestObj, hasRequest := logEntry["request"] + if !hasRequest { + log.Debugf("Not an access log entry (no 'request' field)") + return len(p), nil + } + + request, ok := requestObj.(map[string]interface{}) + if !ok { + log.Debugf("'request' field is not a map") + return len(p), nil + } + + // Extract fields + data := &RequestData{ + ServiceID: lw.serviceID, + } + + // Extract method from request object + if method, ok := request["method"].(string); ok { + data.Method = method + } + + // Extract host from request object and strip port + if host, ok := request["host"].(string); ok { + // Strip port from host (e.g., "test.netbird.io:54321" -> "test.netbird.io") + if idx := strings.LastIndex(host, ":"); idx != -1 { + data.Host = host[:idx] + } else { + data.Host = host + } + } + + // Extract path (uri field) from request object + if uri, ok := request["uri"].(string); ok { + data.Path = uri + } + + // Extract status code from top-level + if status, ok := logEntry["status"].(float64); ok { + data.ResponseCode = int32(status) + } + + // Extract duration (in seconds, convert to milliseconds) from top-level + if duration, ok := logEntry["duration"].(float64); ok { + data.DurationMs = int64(duration * 1000) + } + + // Extract source IP from request object - try multiple fields + if clientIP, ok := request["client_ip"].(string); ok { + data.SourceIP = clientIP + } else if remoteIP, ok := request["remote_ip"].(string); ok { + data.SourceIP = remoteIP + } else if remoteAddr, ok := request["remote_addr"].(string); ok { + // remote_addr is in "IP:port" format + if idx := strings.LastIndex(remoteAddr, ":"); idx != -1 { + data.SourceIP = remoteAddr[:idx] + } else { + data.SourceIP = remoteAddr + } + } + + // Call callback if set and we have valid data + callback := getCallback(lw.serviceID) + if callback != nil && data.Method != "" { + log.Infof("Calling callback for request: %s %s", data.Method, data.Path) + go func() { + // Run in goroutine to avoid blocking log writes + callback(data) + }() + } else { + log.Warnf("No callback registered for service_id: %s", lw.serviceID) + } + + log.WithFields(log.Fields{ + "service_id": data.ServiceID, + "method": data.Method, + "host": data.Host, + "path": data.Path, + "status": data.ResponseCode, + "duration_ms": data.DurationMs, + "source_ip": data.SourceIP, + }).Info("Request logged via callback writer") + + return len(p), nil +} + +// Close implements io.Closer (no-op for our use case) +func (lw *LogWriter) Close() error { + return nil +} + +// Ensure LogWriter implements io.WriteCloser +var _ io.WriteCloser = (*LogWriter)(nil) diff --git a/proxy/internal/reverseproxy/logwriter_test.go b/proxy/internal/reverseproxy/logwriter_test.go new file mode 100644 index 000000000..3bd8e1a7f --- /dev/null +++ b/proxy/internal/reverseproxy/logwriter_test.go @@ -0,0 +1,251 @@ +package reverseproxy + +import ( + "encoding/json" + "sync" + "testing" + "time" +) + +func TestLogWriter_Write(t *testing.T) { + // Create a channel to receive callback data + callbackChan := make(chan *RequestData, 1) + var callbackMu sync.Mutex + var callbackCalled bool + + // Register a test callback + testServiceID := "test-service" + RegisterCallback(testServiceID, func(data *RequestData) { + callbackMu.Lock() + callbackCalled = true + callbackMu.Unlock() + callbackChan <- data + }) + defer UnregisterCallback(testServiceID) + + // Create a log writer + writer := NewLogWriter(testServiceID) + + // Create a sample Caddy access log entry (matching the structure from your logs) + logEntry := map[string]interface{}{ + "level": "info", + "ts": 1768352053.7900746, + "logger": "http.log.access", + "msg": "handled request", + "request": map[string]interface{}{ + "remote_ip": "::1", + "remote_port": "51972", + "client_ip": "::1", + "proto": "HTTP/1.1", + "method": "GET", + "host": "test.netbird.io:54321", + "uri": "/test/path", + }, + "bytes_read": 0, + "user_id": "", + "duration": 0.004779453, + "size": 615, + "status": 200, + } + + // Marshal to JSON + logJSON, err := json.Marshal(logEntry) + if err != nil { + t.Fatalf("Failed to marshal log entry: %v", err) + } + + // Write to the log writer + n, err := writer.Write(logJSON) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + if n != len(logJSON) { + t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n) + } + + // Wait for callback to be called (with timeout) + select { + case data := <-callbackChan: + // Verify the extracted data + if data.ServiceID != testServiceID { + t.Errorf("Expected service_id %s, got %s", testServiceID, data.ServiceID) + } + if data.Method != "GET" { + t.Errorf("Expected method GET, got %s", data.Method) + } + if data.Host != "test.netbird.io" { + t.Errorf("Expected host test.netbird.io, got %s", data.Host) + } + if data.Path != "/test/path" { + t.Errorf("Expected path /test/path, got %s", data.Path) + } + if data.ResponseCode != 200 { + t.Errorf("Expected status 200, got %d", data.ResponseCode) + } + if data.SourceIP != "::1" { + t.Errorf("Expected source_ip ::1, got %s", data.SourceIP) + } + // Duration should be ~4.78ms (0.004779453 * 1000) + if data.DurationMs < 4 || data.DurationMs > 5 { + t.Errorf("Expected duration ~4-5ms, got %dms", data.DurationMs) + } + case <-time.After(1 * time.Second): + t.Fatal("Callback was not called within timeout") + } + + // Verify callback was called + callbackMu.Lock() + defer callbackMu.Unlock() + if !callbackCalled { + t.Error("Callback was never called") + } +} + +func TestLogWriter_Write_NonAccessLog(t *testing.T) { + // Create a channel to receive callback data + callbackChan := make(chan *RequestData, 1) + + // Register a test callback + testServiceID := "test-service-2" + RegisterCallback(testServiceID, func(data *RequestData) { + callbackChan <- data + }) + defer UnregisterCallback(testServiceID) + + // Create a log writer + writer := NewLogWriter(testServiceID) + + // Create a non-access log entry (e.g., a TLS log) + logEntry := map[string]interface{}{ + "level": "info", + "ts": 1768352032.12347, + "logger": "tls", + "msg": "storage cleaning happened too recently", + } + + // Marshal to JSON + logJSON, err := json.Marshal(logEntry) + if err != nil { + t.Fatalf("Failed to marshal log entry: %v", err) + } + + // Write to the log writer + n, err := writer.Write(logJSON) + if err != nil { + t.Fatalf("Write failed: %v", err) + } + + if n != len(logJSON) { + t.Errorf("Expected to write %d bytes, wrote %d", len(logJSON), n) + } + + // Callback should NOT be called for non-access logs + select { + case data := <-callbackChan: + t.Errorf("Callback should not be called for non-access log, but got: %+v", data) + case <-time.After(100 * time.Millisecond): + // Expected - callback not called + } +} + +func TestLogWriter_Write_MalformedJSON(t *testing.T) { + // Create a log writer + writer := NewLogWriter("test-service-3") + + // Write malformed JSON + malformedJSON := []byte("{this is not valid json") + + // Should not fail, just skip the entry + n, err := writer.Write(malformedJSON) + if err != nil { + t.Fatalf("Write should not fail on malformed JSON: %v", err) + } + + if n != len(malformedJSON) { + t.Errorf("Expected to write %d bytes, wrote %d", len(malformedJSON), n) + } +} + +func TestCallbackRegistry(t *testing.T) { + serviceID := "test-registry" + var called bool + + // Test registering a callback + callback := func(data *RequestData) { + called = true + } + RegisterCallback(serviceID, callback) + + // Test retrieving the callback + retrievedCallback := getCallback(serviceID) + if retrievedCallback == nil { + t.Fatal("Expected to retrieve callback, got nil") + } + + // Call the retrieved callback to verify it works + retrievedCallback(&RequestData{}) + if !called { + t.Error("Callback was not called") + } + + // Test unregistering + UnregisterCallback(serviceID) + retrievedCallback = getCallback(serviceID) + if retrievedCallback != nil { + t.Error("Expected nil after unregistering, got a callback") + } +} + +func TestCallbackWriter_Module(t *testing.T) { + // Test that the module is properly configured + cw := CallbackWriter{ServiceID: "test"} + + moduleInfo := cw.CaddyModule() + if moduleInfo.ID != "caddy.logging.writers.callback" { + t.Errorf("Expected module ID 'caddy.logging.writers.callback', got '%s'", moduleInfo.ID) + } + + if moduleInfo.New == nil { + t.Error("Expected New function to be set") + } + + // Test creating a new instance via the New function + newModule := moduleInfo.New() + if newModule == nil { + t.Error("Expected New() to return a module instance") + } + + _, ok := newModule.(*CallbackWriter) + if !ok { + t.Error("Expected New() to return a *CallbackWriter") + } +} + +func TestCallbackWriter_WriterKey(t *testing.T) { + cw := &CallbackWriter{ServiceID: "my-service"} + + expectedKey := "callback_my-service" + if cw.WriterKey() != expectedKey { + t.Errorf("Expected writer key '%s', got '%s'", expectedKey, cw.WriterKey()) + } +} + +func TestCallbackWriter_String(t *testing.T) { + cw := &CallbackWriter{ServiceID: "my-service"} + + str := cw.String() + if str != "callback writer for service my-service" { + t.Errorf("Unexpected string representation: %s", str) + } +} + +func TestLogWriter_Close(t *testing.T) { + writer := NewLogWriter("test") + + // Close should not fail + err := writer.Close() + if err != nil { + t.Errorf("Close should not fail: %v", err) + } +} diff --git a/proxy/internal/reverseproxy/middleware.go b/proxy/internal/reverseproxy/middleware.go new file mode 100644 index 000000000..f0e24d9e9 --- /dev/null +++ b/proxy/internal/reverseproxy/middleware.go @@ -0,0 +1,131 @@ +package reverseproxy + +import ( + "net/http" + "strings" + "time" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + log "github.com/sirupsen/logrus" +) + +// RequestDataCallback is called for each request that passes through the proxy +type RequestDataCallback func(data *RequestData) + +// RequestData contains metadata about a proxied request +type RequestData struct { + ServiceID string + Host string + Path string + DurationMs int64 + Method string + ResponseCode int32 + SourceIP string +} + +// MetricsMiddleware wraps a handler to capture request metrics +type MetricsMiddleware struct { + Next caddyhttp.Handler + ServiceID string + Callback RequestDataCallback +} + +// ServeHTTP implements caddyhttp.MiddlewareHandler +func (m *MetricsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + // Record start time + startTime := time.Now() + + // Wrap the response writer to capture status code + wrappedWriter := &responseWriterWrapper{ + ResponseWriter: w, + statusCode: http.StatusOK, // Default to 200 + } + + // Call the next handler (Caddy's reverse proxy) + err := next.ServeHTTP(wrappedWriter, r) + + // Calculate duration + duration := time.Since(startTime) + + // Extract source IP (handle X-Forwarded-For or direct connection) + sourceIP := extractSourceIP(r) + + // Create request data + data := &RequestData{ + ServiceID: m.ServiceID, + Path: r.URL.Path, + DurationMs: duration.Milliseconds(), + Method: r.Method, + ResponseCode: int32(wrappedWriter.statusCode), + SourceIP: sourceIP, + } + + // Call callback if set + if m.Callback != nil { + go func() { + // Run callback in goroutine to avoid blocking response + m.Callback(data) + }() + } + + log.WithFields(log.Fields{ + "service_id": data.ServiceID, + "method": data.Method, + "path": data.Path, + "status": data.ResponseCode, + "duration_ms": data.DurationMs, + "source_ip": data.SourceIP, + }).Debug("Request proxied") + + return err +} + +// responseWriterWrapper wraps http.ResponseWriter to capture status code +type responseWriterWrapper struct { + http.ResponseWriter + statusCode int + written bool +} + +// WriteHeader captures the status code +func (w *responseWriterWrapper) WriteHeader(statusCode int) { + if !w.written { + w.statusCode = statusCode + w.written = true + } + w.ResponseWriter.WriteHeader(statusCode) +} + +// Write ensures we capture status if WriteHeader wasn't called explicitly +func (w *responseWriterWrapper) Write(b []byte) (int, error) { + if !w.written { + w.written = true + // Status code defaults to 200 if not explicitly set + } + return w.ResponseWriter.Write(b) +} + +// extractSourceIP extracts the real client IP from the request +func extractSourceIP(r *http.Request) string { + // Check X-Forwarded-For header first (if behind a proxy) + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + // X-Forwarded-For can be a comma-separated list, take the first one + parts := strings.Split(xff, ",") + if len(parts) > 0 { + return strings.TrimSpace(parts[0]) + } + } + + // Check X-Real-IP header + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return xri + } + + // Fall back to RemoteAddr + // RemoteAddr is in format "IP:port", so we need to strip the port + if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 { + return r.RemoteAddr[:idx] + } + + return r.RemoteAddr +} diff --git a/proxy/internal/reverseproxy/transport.go b/proxy/internal/reverseproxy/transport.go new file mode 100644 index 000000000..87cd21e04 --- /dev/null +++ b/proxy/internal/reverseproxy/transport.go @@ -0,0 +1,139 @@ +package reverseproxy + +import ( + "context" + "fmt" + "net" + "net/http" + "sync" + "time" + + log "github.com/sirupsen/logrus" +) + +// customTransportRegistry stores custom dialers and connections globally +// This allows them to be accessed after Caddy deserializes the configuration from JSON +var customTransportRegistry = &transportRegistry{ + transports: make(map[string]*customTransport), +} + +// transportRegistry manages custom transports for routes +type transportRegistry struct { + mu sync.RWMutex + transports map[string]*customTransport // key is "routeID:path" +} + +// customTransport wraps either a net.Conn or a custom dialer +type customTransport struct { + routeID string + path string + conn net.Conn + customDialer func(ctx context.Context, network, address string) (net.Conn, error) + defaultDialer *net.Dialer +} + +// Register registers a custom transport for a route +func (r *transportRegistry) Register(routeID, path string, conn net.Conn, dialer func(ctx context.Context, network, address string) (net.Conn, error)) { + r.mu.Lock() + defer r.mu.Unlock() + + key := fmt.Sprintf("%s:%s", routeID, path) + r.transports[key] = &customTransport{ + routeID: routeID, + path: path, + conn: conn, + customDialer: dialer, + defaultDialer: &net.Dialer{Timeout: 30 * time.Second}, + } + + if conn != nil { + log.Infof("Registered net.Conn transport for route %s (path: %s)", routeID, path) + } else if dialer != nil { + log.Infof("Registered custom dialer transport for route %s (path: %s)", routeID, path) + } +} + +// Get retrieves a custom transport for a route +func (r *transportRegistry) Get(routeID, path string) *customTransport { + r.mu.RLock() + defer r.mu.RUnlock() + + key := fmt.Sprintf("%s:%s", routeID, path) + return r.transports[key] +} + +// Unregister removes a custom transport +func (r *transportRegistry) Unregister(routeID, path string) { + r.mu.Lock() + defer r.mu.Unlock() + + key := fmt.Sprintf("%s:%s", routeID, path) + delete(r.transports, key) + log.Infof("Unregistered transport for route %s (path: %s)", routeID, path) +} + +// Clear removes all custom transports +func (r *transportRegistry) Clear() { + r.mu.Lock() + defer r.mu.Unlock() + + r.transports = make(map[string]*customTransport) + log.Info("Cleared all custom transports") +} + +// DialContext implements the DialContext function for custom transports +func (ct *customTransport) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + // If we have a pre-existing connection, return it + if ct.conn != nil { + log.Debugf("Reusing existing connection for route %s (path: %s) to %s", ct.routeID, ct.path, address) + return ct.conn, nil + } + + // If we have a custom dialer, use it + if ct.customDialer != nil { + log.Debugf("Using custom dialer for route %s (path: %s) to %s", ct.routeID, ct.path, address) + return ct.customDialer(ctx, network, address) + } + + // Fallback to default dialer (this shouldn't happen if registered correctly) + log.Warnf("No custom transport found for route %s (path: %s), using default dialer", ct.routeID, ct.path) + return ct.defaultDialer.DialContext(ctx, network, address) +} + +// NewCustomHTTPTransport creates an HTTP transport that uses the custom dialer +func NewCustomHTTPTransport(routeID, path string) *http.Transport { + transport := customTransportRegistry.Get(routeID, path) + if transport == nil { + // No custom transport registered, return standard transport + log.Warnf("No custom transport found for route %s (path: %s), using standard transport", routeID, path) + return &http.Transport{ + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + } + + // Configure transport based on whether we're using a connection or dialer + if transport.conn != nil { + // Using a pre-existing connection - disable pooling + return &http.Transport{ + DialContext: transport.DialContext, + MaxIdleConns: 1, + MaxIdleConnsPerHost: 1, + IdleConnTimeout: 0, // Keep alive indefinitely + DisableKeepAlives: false, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + } + + // Using a custom dialer - use normal pooling + return &http.Transport{ + DialContext: transport.DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/proxy/main.go b/proxy/main.go new file mode 100644 index 000000000..6747414a1 --- /dev/null +++ b/proxy/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/netbirdio/netbird/proxy/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/proxy/pkg/errors/common.go b/proxy/pkg/errors/common.go new file mode 100644 index 000000000..1b06d43a2 --- /dev/null +++ b/proxy/pkg/errors/common.go @@ -0,0 +1,73 @@ +package errors + +import "fmt" + +// Configuration errors + +func NewConfigInvalid(message string) *AppError { + return New(CodeConfigInvalid, message) +} + +func NewConfigNotFound(path string) *AppError { + return New(CodeConfigNotFound, fmt.Sprintf("configuration file not found: %s", path)) +} + +func WrapConfigParseFailed(err error, path string) *AppError { + return Wrap(CodeConfigParseFailed, fmt.Sprintf("failed to parse configuration file: %s", path), err) +} + +// Server errors + +func NewServerStartFailed(err error, reason string) *AppError { + return Wrap(CodeServerStartFailed, fmt.Sprintf("server start failed: %s", reason), err) +} + +func NewServerStopFailed(err error) *AppError { + return Wrap(CodeServerStopFailed, "server shutdown failed", err) +} + +func NewServerAlreadyRunning() *AppError { + return New(CodeServerAlreadyRunning, "server is already running") +} + +func NewServerNotRunning() *AppError { + return New(CodeServerNotRunning, "server is not running") +} + +// Proxy errors + +func NewProxyBackendUnavailable(backend string, err error) *AppError { + return Wrap(CodeProxyBackendUnavailable, fmt.Sprintf("backend unavailable: %s", backend), err) +} + +func NewProxyTimeout(backend string) *AppError { + return New(CodeProxyTimeout, fmt.Sprintf("request to backend timed out: %s", backend)) +} + +func NewProxyInvalidTarget(target string, err error) *AppError { + return Wrap(CodeProxyInvalidTarget, fmt.Sprintf("invalid proxy target: %s", target), err) +} + +// Network errors + +func NewNetworkTimeout(operation string) *AppError { + return New(CodeNetworkTimeout, fmt.Sprintf("network timeout: %s", operation)) +} + +func NewNetworkUnreachable(host string) *AppError { + return New(CodeNetworkUnreachable, fmt.Sprintf("network unreachable: %s", host)) +} + +func NewNetworkRefused(host string) *AppError { + return New(CodeNetworkRefused, fmt.Sprintf("connection refused: %s", host)) +} + +// Internal errors + +func NewInternalError(message string) *AppError { + return New(CodeInternalError, message) +} + +func WrapInternalError(err error, message string) *AppError { + return Wrap(CodeInternalError, message, err) +} diff --git a/proxy/pkg/errors/errors.go b/proxy/pkg/errors/errors.go new file mode 100644 index 000000000..1fa15aa48 --- /dev/null +++ b/proxy/pkg/errors/errors.go @@ -0,0 +1,138 @@ +package errors + +import ( + "errors" + "fmt" +) + +// Error codes for categorizing errors +type Code string + +const ( + // Configuration errors + CodeConfigInvalid Code = "CONFIG_INVALID" + CodeConfigNotFound Code = "CONFIG_NOT_FOUND" + CodeConfigParseFailed Code = "CONFIG_PARSE_FAILED" + + // Server errors + CodeServerStartFailed Code = "SERVER_START_FAILED" + CodeServerStopFailed Code = "SERVER_STOP_FAILED" + CodeServerAlreadyRunning Code = "SERVER_ALREADY_RUNNING" + CodeServerNotRunning Code = "SERVER_NOT_RUNNING" + + // Proxy errors + CodeProxyBackendUnavailable Code = "PROXY_BACKEND_UNAVAILABLE" + CodeProxyTimeout Code = "PROXY_TIMEOUT" + CodeProxyInvalidTarget Code = "PROXY_INVALID_TARGET" + + // Network errors + CodeNetworkTimeout Code = "NETWORK_TIMEOUT" + CodeNetworkUnreachable Code = "NETWORK_UNREACHABLE" + CodeNetworkRefused Code = "NETWORK_REFUSED" + + // Internal errors + CodeInternalError Code = "INTERNAL_ERROR" + CodeUnknownError Code = "UNKNOWN_ERROR" +) + +// AppError represents a structured application error +type AppError struct { + Code Code // Error code for categorization + Message string // Human-readable error message + Cause error // Underlying error (if any) +} + +// Error implements the error interface +func (e *AppError) Error() string { + if e.Cause != nil { + return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause) + } + return fmt.Sprintf("[%s] %s", e.Code, e.Message) +} + +// Unwrap returns the underlying error (for errors.Is and errors.As) +func (e *AppError) Unwrap() error { + return e.Cause +} + +// Is checks if the error matches the target +func (e *AppError) Is(target error) bool { + t, ok := target.(*AppError) + if !ok { + return false + } + return e.Code == t.Code +} + +// New creates a new AppError +func New(code Code, message string) *AppError { + return &AppError{ + Code: code, + Message: message, + } +} + +// Wrap wraps an existing error with additional context +func Wrap(code Code, message string, cause error) *AppError { + return &AppError{ + Code: code, + Message: message, + Cause: cause, + } +} + +// Wrapf wraps an error with a formatted message +func Wrapf(code Code, cause error, format string, args ...interface{}) *AppError { + return &AppError{ + Code: code, + Message: fmt.Sprintf(format, args...), + Cause: cause, + } +} + +// GetCode extracts the error code from an error +func GetCode(err error) Code { + var appErr *AppError + if errors.As(err, &appErr) { + return appErr.Code + } + return CodeUnknownError +} + +// HasCode checks if an error has a specific code +func HasCode(err error, code Code) bool { + return GetCode(err) == code +} + +// IsConfigError checks if an error is configuration-related +func IsConfigError(err error) bool { + code := GetCode(err) + return code == CodeConfigInvalid || + code == CodeConfigNotFound || + code == CodeConfigParseFailed +} + +// IsServerError checks if an error is server-related +func IsServerError(err error) bool { + code := GetCode(err) + return code == CodeServerStartFailed || + code == CodeServerStopFailed || + code == CodeServerAlreadyRunning || + code == CodeServerNotRunning +} + +// IsProxyError checks if an error is proxy-related +func IsProxyError(err error) bool { + code := GetCode(err) + return code == CodeProxyBackendUnavailable || + code == CodeProxyTimeout || + code == CodeProxyInvalidTarget +} + +// IsNetworkError checks if an error is network-related +func IsNetworkError(err error) bool { + code := GetCode(err) + return code == CodeNetworkTimeout || + code == CodeNetworkUnreachable || + code == CodeNetworkRefused +} diff --git a/proxy/pkg/errors/errors_test.go b/proxy/pkg/errors/errors_test.go new file mode 100644 index 000000000..cb2f0a827 --- /dev/null +++ b/proxy/pkg/errors/errors_test.go @@ -0,0 +1,160 @@ +package errors + +import ( + "errors" + "testing" +) + +func TestAppError_Error(t *testing.T) { + tests := []struct { + name string + err *AppError + expected string + }{ + { + name: "error without cause", + err: New(CodeConfigInvalid, "invalid configuration"), + expected: "[CONFIG_INVALID] invalid configuration", + }, + { + name: "error with cause", + err: Wrap(CodeServerStartFailed, "failed to bind port", errors.New("address already in use")), + expected: "[SERVER_START_FAILED] failed to bind port: address already in use", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Error(); got != tt.expected { + t.Errorf("Error() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestGetCode(t *testing.T) { + tests := []struct { + name string + err error + expected Code + }{ + { + name: "app error", + err: New(CodeConfigInvalid, "test"), + expected: CodeConfigInvalid, + }, + { + name: "wrapped app error", + err: Wrap(CodeServerStartFailed, "test", errors.New("cause")), + expected: CodeServerStartFailed, + }, + { + name: "standard error", + err: errors.New("standard error"), + expected: CodeUnknownError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetCode(tt.err); got != tt.expected { + t.Errorf("GetCode() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestHasCode(t *testing.T) { + err := New(CodeConfigInvalid, "invalid config") + + if !HasCode(err, CodeConfigInvalid) { + t.Error("HasCode() should return true for matching code") + } + + if HasCode(err, CodeServerStartFailed) { + t.Error("HasCode() should return false for non-matching code") + } +} + +func TestIsConfigError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "config invalid error", + err: New(CodeConfigInvalid, "test"), + expected: true, + }, + { + name: "config not found error", + err: New(CodeConfigNotFound, "test"), + expected: true, + }, + { + name: "server error", + err: New(CodeServerStartFailed, "test"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsConfigError(tt.err); got != tt.expected { + t.Errorf("IsConfigError() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestErrorUnwrap(t *testing.T) { + cause := errors.New("root cause") + err := Wrap(CodeInternalError, "wrapped error", cause) + + unwrapped := errors.Unwrap(err) + if unwrapped != cause { + t.Errorf("Unwrap() = %v, want %v", unwrapped, cause) + } +} + +func TestErrorIs(t *testing.T) { + err1 := New(CodeConfigInvalid, "test1") + err2 := New(CodeConfigInvalid, "test2") + err3 := New(CodeServerStartFailed, "test3") + + if !errors.Is(err1, err2) { + t.Error("errors.Is() should return true for same error code") + } + + if errors.Is(err1, err3) { + t.Error("errors.Is() should return false for different error codes") + } +} + +func TestCommonConstructors(t *testing.T) { + t.Run("NewConfigNotFound", func(t *testing.T) { + err := NewConfigNotFound("/path/to/config") + if GetCode(err) != CodeConfigNotFound { + t.Error("NewConfigNotFound should create CONFIG_NOT_FOUND error") + } + }) + + t.Run("NewServerAlreadyRunning", func(t *testing.T) { + err := NewServerAlreadyRunning() + if GetCode(err) != CodeServerAlreadyRunning { + t.Error("NewServerAlreadyRunning should create SERVER_ALREADY_RUNNING error") + } + }) + + t.Run("NewProxyBackendUnavailable", func(t *testing.T) { + cause := errors.New("connection refused") + err := NewProxyBackendUnavailable("http://backend", cause) + if GetCode(err) != CodeProxyBackendUnavailable { + t.Error("NewProxyBackendUnavailable should create PROXY_BACKEND_UNAVAILABLE error") + } + if !errors.Is(err.Unwrap(), cause) { + t.Error("NewProxyBackendUnavailable should wrap the cause") + } + }) +} diff --git a/proxy/pkg/grpc/proto/proxy.pb.go b/proxy/pkg/grpc/proto/proxy.pb.go new file mode 100644 index 000000000..654212151 --- /dev/null +++ b/proxy/pkg/grpc/proto/proxy.pb.go @@ -0,0 +1,1796 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v6.33.0 +// source: pkg/grpc/proto/proxy.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ProxyEvent_EventType int32 + +const ( + ProxyEvent_UNKNOWN ProxyEvent_EventType = 0 + ProxyEvent_STARTED ProxyEvent_EventType = 1 + ProxyEvent_STOPPED ProxyEvent_EventType = 2 + ProxyEvent_ERROR ProxyEvent_EventType = 3 + ProxyEvent_BACKEND_UNAVAILABLE ProxyEvent_EventType = 4 + ProxyEvent_BACKEND_RECOVERED ProxyEvent_EventType = 5 + ProxyEvent_CONFIG_UPDATED ProxyEvent_EventType = 6 +) + +// Enum value maps for ProxyEvent_EventType. +var ( + ProxyEvent_EventType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "STARTED", + 2: "STOPPED", + 3: "ERROR", + 4: "BACKEND_UNAVAILABLE", + 5: "BACKEND_RECOVERED", + 6: "CONFIG_UPDATED", + } + ProxyEvent_EventType_value = map[string]int32{ + "UNKNOWN": 0, + "STARTED": 1, + "STOPPED": 2, + "ERROR": 3, + "BACKEND_UNAVAILABLE": 4, + "BACKEND_RECOVERED": 5, + "CONFIG_UPDATED": 6, + } +) + +func (x ProxyEvent_EventType) Enum() *ProxyEvent_EventType { + p := new(ProxyEvent_EventType) + *p = x + return p +} + +func (x ProxyEvent_EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProxyEvent_EventType) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_grpc_proto_proxy_proto_enumTypes[0].Descriptor() +} + +func (ProxyEvent_EventType) Type() protoreflect.EnumType { + return &file_pkg_grpc_proto_proxy_proto_enumTypes[0] +} + +func (x ProxyEvent_EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProxyEvent_EventType.Descriptor instead. +func (ProxyEvent_EventType) EnumDescriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{3, 0} +} + +type ProxyLog_LogLevel int32 + +const ( + ProxyLog_DEBUG ProxyLog_LogLevel = 0 + ProxyLog_INFO ProxyLog_LogLevel = 1 + ProxyLog_WARN ProxyLog_LogLevel = 2 + ProxyLog_ERROR ProxyLog_LogLevel = 3 +) + +// Enum value maps for ProxyLog_LogLevel. +var ( + ProxyLog_LogLevel_name = map[int32]string{ + 0: "DEBUG", + 1: "INFO", + 2: "WARN", + 3: "ERROR", + } + ProxyLog_LogLevel_value = map[string]int32{ + "DEBUG": 0, + "INFO": 1, + "WARN": 2, + "ERROR": 3, + } +) + +func (x ProxyLog_LogLevel) Enum() *ProxyLog_LogLevel { + p := new(ProxyLog_LogLevel) + *p = x + return p +} + +func (x ProxyLog_LogLevel) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ProxyLog_LogLevel) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_grpc_proto_proxy_proto_enumTypes[1].Descriptor() +} + +func (ProxyLog_LogLevel) Type() protoreflect.EnumType { + return &file_pkg_grpc_proto_proxy_proto_enumTypes[1] +} + +func (x ProxyLog_LogLevel) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ProxyLog_LogLevel.Descriptor instead. +func (ProxyLog_LogLevel) EnumDescriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{4, 0} +} + +type ControlCommand_CommandType int32 + +const ( + ControlCommand_UNKNOWN ControlCommand_CommandType = 0 + ControlCommand_RELOAD_CONFIG ControlCommand_CommandType = 1 + ControlCommand_ENABLE_DEBUG ControlCommand_CommandType = 2 + ControlCommand_DISABLE_DEBUG ControlCommand_CommandType = 3 + ControlCommand_GET_STATS ControlCommand_CommandType = 4 + ControlCommand_SHUTDOWN ControlCommand_CommandType = 5 +) + +// Enum value maps for ControlCommand_CommandType. +var ( + ControlCommand_CommandType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RELOAD_CONFIG", + 2: "ENABLE_DEBUG", + 3: "DISABLE_DEBUG", + 4: "GET_STATS", + 5: "SHUTDOWN", + } + ControlCommand_CommandType_value = map[string]int32{ + "UNKNOWN": 0, + "RELOAD_CONFIG": 1, + "ENABLE_DEBUG": 2, + "DISABLE_DEBUG": 3, + "GET_STATS": 4, + "SHUTDOWN": 5, + } +) + +func (x ControlCommand_CommandType) Enum() *ControlCommand_CommandType { + p := new(ControlCommand_CommandType) + *p = x + return p +} + +func (x ControlCommand_CommandType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ControlCommand_CommandType) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_grpc_proto_proxy_proto_enumTypes[2].Descriptor() +} + +func (ControlCommand_CommandType) Type() protoreflect.EnumType { + return &file_pkg_grpc_proto_proxy_proto_enumTypes[2] +} + +func (x ControlCommand_CommandType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ControlCommand_CommandType.Descriptor instead. +func (ControlCommand_CommandType) EnumDescriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{7, 0} +} + +type ExposedServiceEvent_EventType int32 + +const ( + ExposedServiceEvent_UNKNOWN ExposedServiceEvent_EventType = 0 + ExposedServiceEvent_CREATED ExposedServiceEvent_EventType = 1 + ExposedServiceEvent_UPDATED ExposedServiceEvent_EventType = 2 + ExposedServiceEvent_REMOVED ExposedServiceEvent_EventType = 3 +) + +// Enum value maps for ExposedServiceEvent_EventType. +var ( + ExposedServiceEvent_EventType_name = map[int32]string{ + 0: "UNKNOWN", + 1: "CREATED", + 2: "UPDATED", + 3: "REMOVED", + } + ExposedServiceEvent_EventType_value = map[string]int32{ + "UNKNOWN": 0, + "CREATED": 1, + "UPDATED": 2, + "REMOVED": 3, + } +) + +func (x ExposedServiceEvent_EventType) Enum() *ExposedServiceEvent_EventType { + p := new(ExposedServiceEvent_EventType) + *p = x + return p +} + +func (x ExposedServiceEvent_EventType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExposedServiceEvent_EventType) Descriptor() protoreflect.EnumDescriptor { + return file_pkg_grpc_proto_proxy_proto_enumTypes[3].Descriptor() +} + +func (ExposedServiceEvent_EventType) Type() protoreflect.EnumType { + return &file_pkg_grpc_proto_proxy_proto_enumTypes[3] +} + +func (x ExposedServiceEvent_EventType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExposedServiceEvent_EventType.Descriptor instead. +func (ExposedServiceEvent_EventType) EnumDescriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{9, 0} +} + +// ProxyMessage represents messages sent from proxy to control service +type ProxyMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Message: + // + // *ProxyMessage_Stats + // *ProxyMessage_Event + // *ProxyMessage_Log + // *ProxyMessage_Heartbeat + // *ProxyMessage_RequestData + Message isProxyMessage_Message `protobuf_oneof:"message"` +} + +func (x *ProxyMessage) Reset() { + *x = ProxyMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyMessage) ProtoMessage() {} + +func (x *ProxyMessage) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyMessage.ProtoReflect.Descriptor instead. +func (*ProxyMessage) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{0} +} + +func (m *ProxyMessage) GetMessage() isProxyMessage_Message { + if m != nil { + return m.Message + } + return nil +} + +func (x *ProxyMessage) GetStats() *ProxyStats { + if x, ok := x.GetMessage().(*ProxyMessage_Stats); ok { + return x.Stats + } + return nil +} + +func (x *ProxyMessage) GetEvent() *ProxyEvent { + if x, ok := x.GetMessage().(*ProxyMessage_Event); ok { + return x.Event + } + return nil +} + +func (x *ProxyMessage) GetLog() *ProxyLog { + if x, ok := x.GetMessage().(*ProxyMessage_Log); ok { + return x.Log + } + return nil +} + +func (x *ProxyMessage) GetHeartbeat() *ProxyHeartbeat { + if x, ok := x.GetMessage().(*ProxyMessage_Heartbeat); ok { + return x.Heartbeat + } + return nil +} + +func (x *ProxyMessage) GetRequestData() *ProxyRequestData { + if x, ok := x.GetMessage().(*ProxyMessage_RequestData); ok { + return x.RequestData + } + return nil +} + +type isProxyMessage_Message interface { + isProxyMessage_Message() +} + +type ProxyMessage_Stats struct { + Stats *ProxyStats `protobuf:"bytes,1,opt,name=stats,proto3,oneof"` +} + +type ProxyMessage_Event struct { + Event *ProxyEvent `protobuf:"bytes,2,opt,name=event,proto3,oneof"` +} + +type ProxyMessage_Log struct { + Log *ProxyLog `protobuf:"bytes,3,opt,name=log,proto3,oneof"` +} + +type ProxyMessage_Heartbeat struct { + Heartbeat *ProxyHeartbeat `protobuf:"bytes,4,opt,name=heartbeat,proto3,oneof"` +} + +type ProxyMessage_RequestData struct { + RequestData *ProxyRequestData `protobuf:"bytes,5,opt,name=request_data,json=requestData,proto3,oneof"` +} + +func (*ProxyMessage_Stats) isProxyMessage_Message() {} + +func (*ProxyMessage_Event) isProxyMessage_Message() {} + +func (*ProxyMessage_Log) isProxyMessage_Message() {} + +func (*ProxyMessage_Heartbeat) isProxyMessage_Message() {} + +func (*ProxyMessage_RequestData) isProxyMessage_Message() {} + +// ControlMessage represents messages sent from control service to proxy +type ControlMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Message: + // + // *ControlMessage_Event + // *ControlMessage_Command + // *ControlMessage_Config + // *ControlMessage_ExposedService + Message isControlMessage_Message `protobuf_oneof:"message"` +} + +func (x *ControlMessage) Reset() { + *x = ControlMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ControlMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlMessage) ProtoMessage() {} + +func (x *ControlMessage) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ControlMessage.ProtoReflect.Descriptor instead. +func (*ControlMessage) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{1} +} + +func (m *ControlMessage) GetMessage() isControlMessage_Message { + if m != nil { + return m.Message + } + return nil +} + +func (x *ControlMessage) GetEvent() *ControlEvent { + if x, ok := x.GetMessage().(*ControlMessage_Event); ok { + return x.Event + } + return nil +} + +func (x *ControlMessage) GetCommand() *ControlCommand { + if x, ok := x.GetMessage().(*ControlMessage_Command); ok { + return x.Command + } + return nil +} + +func (x *ControlMessage) GetConfig() *ControlConfig { + if x, ok := x.GetMessage().(*ControlMessage_Config); ok { + return x.Config + } + return nil +} + +func (x *ControlMessage) GetExposedService() *ExposedServiceEvent { + if x, ok := x.GetMessage().(*ControlMessage_ExposedService); ok { + return x.ExposedService + } + return nil +} + +type isControlMessage_Message interface { + isControlMessage_Message() +} + +type ControlMessage_Event struct { + Event *ControlEvent `protobuf:"bytes,1,opt,name=event,proto3,oneof"` +} + +type ControlMessage_Command struct { + Command *ControlCommand `protobuf:"bytes,2,opt,name=command,proto3,oneof"` +} + +type ControlMessage_Config struct { + Config *ControlConfig `protobuf:"bytes,3,opt,name=config,proto3,oneof"` +} + +type ControlMessage_ExposedService struct { + ExposedService *ExposedServiceEvent `protobuf:"bytes,4,opt,name=exposed_service,json=exposedService,proto3,oneof"` +} + +func (*ControlMessage_Event) isControlMessage_Message() {} + +func (*ControlMessage_Command) isControlMessage_Message() {} + +func (*ControlMessage_Config) isControlMessage_Message() {} + +func (*ControlMessage_ExposedService) isControlMessage_Message() {} + +// ProxyStats contains proxy statistics +type ProxyStats struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + TotalRequests uint64 `protobuf:"varint,2,opt,name=total_requests,json=totalRequests,proto3" json:"total_requests,omitempty"` + ActiveConnections uint64 `protobuf:"varint,3,opt,name=active_connections,json=activeConnections,proto3" json:"active_connections,omitempty"` + BytesSent uint64 `protobuf:"varint,4,opt,name=bytes_sent,json=bytesSent,proto3" json:"bytes_sent,omitempty"` + BytesReceived uint64 `protobuf:"varint,5,opt,name=bytes_received,json=bytesReceived,proto3" json:"bytes_received,omitempty"` + CpuUsage float64 `protobuf:"fixed64,6,opt,name=cpu_usage,json=cpuUsage,proto3" json:"cpu_usage,omitempty"` + MemoryUsageMb float64 `protobuf:"fixed64,7,opt,name=memory_usage_mb,json=memoryUsageMb,proto3" json:"memory_usage_mb,omitempty"` + StatusCodeCounts map[string]uint64 `protobuf:"bytes,8,rep,name=status_code_counts,json=statusCodeCounts,proto3" json:"status_code_counts,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"` +} + +func (x *ProxyStats) Reset() { + *x = ProxyStats{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyStats) ProtoMessage() {} + +func (x *ProxyStats) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyStats.ProtoReflect.Descriptor instead. +func (*ProxyStats) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{2} +} + +func (x *ProxyStats) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ProxyStats) GetTotalRequests() uint64 { + if x != nil { + return x.TotalRequests + } + return 0 +} + +func (x *ProxyStats) GetActiveConnections() uint64 { + if x != nil { + return x.ActiveConnections + } + return 0 +} + +func (x *ProxyStats) GetBytesSent() uint64 { + if x != nil { + return x.BytesSent + } + return 0 +} + +func (x *ProxyStats) GetBytesReceived() uint64 { + if x != nil { + return x.BytesReceived + } + return 0 +} + +func (x *ProxyStats) GetCpuUsage() float64 { + if x != nil { + return x.CpuUsage + } + return 0 +} + +func (x *ProxyStats) GetMemoryUsageMb() float64 { + if x != nil { + return x.MemoryUsageMb + } + return 0 +} + +func (x *ProxyStats) GetStatusCodeCounts() map[string]uint64 { + if x != nil { + return x.StatusCodeCounts + } + return nil +} + +// ProxyEvent represents events from the proxy +type ProxyEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Type ProxyEvent_EventType `protobuf:"varint,2,opt,name=type,proto3,enum=proxy.ProxyEvent_EventType" json:"type,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ProxyEvent) Reset() { + *x = ProxyEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyEvent) ProtoMessage() {} + +func (x *ProxyEvent) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyEvent.ProtoReflect.Descriptor instead. +func (*ProxyEvent) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{3} +} + +func (x *ProxyEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ProxyEvent) GetType() ProxyEvent_EventType { + if x != nil { + return x.Type + } + return ProxyEvent_UNKNOWN +} + +func (x *ProxyEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ProxyEvent) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +// ProxyLog represents log entries +type ProxyLog struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Level ProxyLog_LogLevel `protobuf:"varint,2,opt,name=level,proto3,enum=proxy.ProxyLog_LogLevel" json:"level,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Fields map[string]string `protobuf:"bytes,4,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ProxyLog) Reset() { + *x = ProxyLog{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyLog) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyLog) ProtoMessage() {} + +func (x *ProxyLog) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyLog.ProtoReflect.Descriptor instead. +func (*ProxyLog) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{4} +} + +func (x *ProxyLog) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ProxyLog) GetLevel() ProxyLog_LogLevel { + if x != nil { + return x.Level + } + return ProxyLog_DEBUG +} + +func (x *ProxyLog) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ProxyLog) GetFields() map[string]string { + if x != nil { + return x.Fields + } + return nil +} + +// ProxyHeartbeat is sent periodically to keep connection alive +type ProxyHeartbeat struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + ProxyId string `protobuf:"bytes,2,opt,name=proxy_id,json=proxyId,proto3" json:"proxy_id,omitempty"` +} + +func (x *ProxyHeartbeat) Reset() { + *x = ProxyHeartbeat{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyHeartbeat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyHeartbeat) ProtoMessage() {} + +func (x *ProxyHeartbeat) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyHeartbeat.ProtoReflect.Descriptor instead. +func (*ProxyHeartbeat) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{5} +} + +func (x *ProxyHeartbeat) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ProxyHeartbeat) GetProxyId() string { + if x != nil { + return x.ProxyId + } + return "" +} + +// ControlEvent represents events from control service +type ControlEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + EventId string `protobuf:"bytes,2,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *ControlEvent) Reset() { + *x = ControlEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ControlEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlEvent) ProtoMessage() {} + +func (x *ControlEvent) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ControlEvent.ProtoReflect.Descriptor instead. +func (*ControlEvent) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{6} +} + +func (x *ControlEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ControlEvent) GetEventId() string { + if x != nil { + return x.EventId + } + return "" +} + +func (x *ControlEvent) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// ControlCommand represents commands sent to proxy +type ControlCommand struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CommandId string `protobuf:"bytes,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` + Type ControlCommand_CommandType `protobuf:"varint,2,opt,name=type,proto3,enum=proxy.ControlCommand_CommandType" json:"type,omitempty"` + Parameters map[string]string `protobuf:"bytes,3,rep,name=parameters,proto3" json:"parameters,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ControlCommand) Reset() { + *x = ControlCommand{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ControlCommand) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlCommand) ProtoMessage() {} + +func (x *ControlCommand) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ControlCommand.ProtoReflect.Descriptor instead. +func (*ControlCommand) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{7} +} + +func (x *ControlCommand) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + +func (x *ControlCommand) GetType() ControlCommand_CommandType { + if x != nil { + return x.Type + } + return ControlCommand_UNKNOWN +} + +func (x *ControlCommand) GetParameters() map[string]string { + if x != nil { + return x.Parameters + } + return nil +} + +// ControlConfig contains configuration updates from control service +type ControlConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ConfigVersion string `protobuf:"bytes,1,opt,name=config_version,json=configVersion,proto3" json:"config_version,omitempty"` + Settings map[string]string `protobuf:"bytes,2,rep,name=settings,proto3" json:"settings,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *ControlConfig) Reset() { + *x = ControlConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ControlConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ControlConfig) ProtoMessage() {} + +func (x *ControlConfig) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ControlConfig.ProtoReflect.Descriptor instead. +func (*ControlConfig) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{8} +} + +func (x *ControlConfig) GetConfigVersion() string { + if x != nil { + return x.ConfigVersion + } + return "" +} + +func (x *ControlConfig) GetSettings() map[string]string { + if x != nil { + return x.Settings + } + return nil +} + +// ExposedServiceEvent represents exposed service lifecycle events +type ExposedServiceEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + Type ExposedServiceEvent_EventType `protobuf:"varint,2,opt,name=type,proto3,enum=proxy.ExposedServiceEvent_EventType" json:"type,omitempty"` + ServiceId string `protobuf:"bytes,3,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + PeerConfig *PeerConfig `protobuf:"bytes,4,opt,name=peer_config,json=peerConfig,proto3" json:"peer_config,omitempty"` + UpstreamConfig *UpstreamConfig `protobuf:"bytes,5,opt,name=upstream_config,json=upstreamConfig,proto3" json:"upstream_config,omitempty"` +} + +func (x *ExposedServiceEvent) Reset() { + *x = ExposedServiceEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ExposedServiceEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExposedServiceEvent) ProtoMessage() {} + +func (x *ExposedServiceEvent) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExposedServiceEvent.ProtoReflect.Descriptor instead. +func (*ExposedServiceEvent) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{9} +} + +func (x *ExposedServiceEvent) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ExposedServiceEvent) GetType() ExposedServiceEvent_EventType { + if x != nil { + return x.Type + } + return ExposedServiceEvent_UNKNOWN +} + +func (x *ExposedServiceEvent) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *ExposedServiceEvent) GetPeerConfig() *PeerConfig { + if x != nil { + return x.PeerConfig + } + return nil +} + +func (x *ExposedServiceEvent) GetUpstreamConfig() *UpstreamConfig { + if x != nil { + return x.UpstreamConfig + } + return nil +} + +// PeerConfig contains WireGuard peer configuration +type PeerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PeerId string `protobuf:"bytes,1,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + PublicKey string `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + AllowedIps []string `protobuf:"bytes,3,rep,name=allowed_ips,json=allowedIps,proto3" json:"allowed_ips,omitempty"` + Endpoint string `protobuf:"bytes,4,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + TunnelIp string `protobuf:"bytes,5,opt,name=tunnel_ip,json=tunnelIp,proto3" json:"tunnel_ip,omitempty"` + PersistentKeepalive uint32 `protobuf:"varint,6,opt,name=persistent_keepalive,json=persistentKeepalive,proto3" json:"persistent_keepalive,omitempty"` +} + +func (x *PeerConfig) Reset() { + *x = PeerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerConfig) ProtoMessage() {} + +func (x *PeerConfig) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerConfig.ProtoReflect.Descriptor instead. +func (*PeerConfig) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{10} +} + +func (x *PeerConfig) GetPeerId() string { + if x != nil { + return x.PeerId + } + return "" +} + +func (x *PeerConfig) GetPublicKey() string { + if x != nil { + return x.PublicKey + } + return "" +} + +func (x *PeerConfig) GetAllowedIps() []string { + if x != nil { + return x.AllowedIps + } + return nil +} + +func (x *PeerConfig) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *PeerConfig) GetTunnelIp() string { + if x != nil { + return x.TunnelIp + } + return "" +} + +func (x *PeerConfig) GetPersistentKeepalive() uint32 { + if x != nil { + return x.PersistentKeepalive + } + return 0 +} + +// UpstreamConfig contains reverse proxy upstream configuration +type UpstreamConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Domain string `protobuf:"bytes,1,opt,name=domain,proto3" json:"domain,omitempty"` + PathMappings map[string]string `protobuf:"bytes,2,rep,name=path_mappings,json=pathMappings,proto3" json:"path_mappings,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // path -> port +} + +func (x *UpstreamConfig) Reset() { + *x = UpstreamConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpstreamConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpstreamConfig) ProtoMessage() {} + +func (x *UpstreamConfig) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpstreamConfig.ProtoReflect.Descriptor instead. +func (*UpstreamConfig) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{11} +} + +func (x *UpstreamConfig) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +func (x *UpstreamConfig) GetPathMappings() map[string]string { + if x != nil { + return x.PathMappings + } + return nil +} + +// ProxyRequestData contains metadata about requests routed through the reverse proxy +type ProxyRequestData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + ServiceId string `protobuf:"bytes,2,opt,name=service_id,json=serviceId,proto3" json:"service_id,omitempty"` + Path string `protobuf:"bytes,3,opt,name=path,proto3" json:"path,omitempty"` + DurationMs int64 `protobuf:"varint,4,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + Method string `protobuf:"bytes,5,opt,name=method,proto3" json:"method,omitempty"` // HTTP method (GET, POST, PUT, DELETE, etc.) + ResponseCode int32 `protobuf:"varint,6,opt,name=response_code,json=responseCode,proto3" json:"response_code,omitempty"` + SourceIp string `protobuf:"bytes,7,opt,name=source_ip,json=sourceIp,proto3" json:"source_ip,omitempty"` +} + +func (x *ProxyRequestData) Reset() { + *x = ProxyRequestData{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ProxyRequestData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProxyRequestData) ProtoMessage() {} + +func (x *ProxyRequestData) ProtoReflect() protoreflect.Message { + mi := &file_pkg_grpc_proto_proxy_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProxyRequestData.ProtoReflect.Descriptor instead. +func (*ProxyRequestData) Descriptor() ([]byte, []int) { + return file_pkg_grpc_proto_proxy_proto_rawDescGZIP(), []int{12} +} + +func (x *ProxyRequestData) GetTimestamp() *timestamppb.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *ProxyRequestData) GetServiceId() string { + if x != nil { + return x.ServiceId + } + return "" +} + +func (x *ProxyRequestData) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *ProxyRequestData) GetDurationMs() int64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *ProxyRequestData) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ProxyRequestData) GetResponseCode() int32 { + if x != nil { + return x.ResponseCode + } + return 0 +} + +func (x *ProxyRequestData) GetSourceIp() string { + if x != nil { + return x.SourceIp + } + return "" +} + +var File_pkg_grpc_proto_proxy_proto protoreflect.FileDescriptor + +var file_pkg_grpc_proto_proxy_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, 0x02, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, + 0x12, 0x29, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x03, 0x6c, + 0x6f, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, + 0x12, 0x35, 0x0a, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x48, 0x00, 0x52, 0x09, 0x68, 0x65, + 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, 0x0b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x44, 0x61, 0x74, 0x61, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0xf2, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x2b, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x48, 0x00, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x45, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x6f, + 0x73, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xc3, 0x03, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, + 0x74, 0x61, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x25, + 0x0a, 0x0e, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x11, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x73, 0x65, + 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62, 0x79, 0x74, 0x65, 0x73, 0x53, + 0x65, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x62, 0x79, 0x74, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x63, + 0x65, 0x69, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x62, 0x79, 0x74, + 0x65, 0x73, 0x52, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x70, + 0x75, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x63, + 0x70, 0x75, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6d, 0x65, 0x6d, 0x6f, 0x72, + 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x6d, 0x62, 0x18, 0x07, 0x20, 0x01, 0x28, 0x01, + 0x52, 0x0d, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x4d, 0x62, 0x12, + 0x55, 0x0a, 0x12, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x73, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x1a, 0x43, 0x0a, 0x15, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x43, 0x6f, 0x64, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8f, 0x03, 0x0a, 0x0a, + 0x50, 0x72, 0x6f, 0x78, 0x79, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x3b, 0x0a, 0x0d, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x81, 0x01, 0x0a, 0x09, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x02, 0x12, 0x09, + 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x41, 0x43, + 0x4b, 0x45, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, + 0x10, 0x04, 0x12, 0x15, 0x0a, 0x11, 0x42, 0x41, 0x43, 0x4b, 0x45, 0x4e, 0x44, 0x5f, 0x52, 0x45, + 0x43, 0x4f, 0x56, 0x45, 0x52, 0x45, 0x44, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x4f, 0x4e, + 0x46, 0x49, 0x47, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x06, 0x22, 0xb4, 0x02, + 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4c, 0x6f, 0x67, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2e, 0x0a, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, + 0x79, 0x4c, 0x6f, 0x67, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x33, + 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4c, 0x6f, 0x67, 0x2e, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x34, + 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, + 0x42, 0x55, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x03, 0x22, 0x65, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, + 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x12, 0x19, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x64, 0x22, 0x7d, 0x0a, 0x0c, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xdd, 0x02, 0x0a, 0x0e, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x12, 0x35, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x70, 0x72, 0x6f, + 0x78, 0x79, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x2e, 0x50, + 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, + 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x50, 0x61, + 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x6f, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x45, 0x4c, 0x4f, 0x41, 0x44, 0x5f, + 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x45, 0x4e, 0x41, 0x42, + 0x4c, 0x45, 0x5f, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x49, + 0x53, 0x41, 0x42, 0x4c, 0x45, 0x5f, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x03, 0x12, 0x0d, 0x0a, + 0x09, 0x47, 0x45, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x53, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, + 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x05, 0x22, 0xb3, 0x01, 0x0a, 0x0d, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x74, 0x74, + 0x69, 0x6e, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x65, 0x74, 0x74, 0x69, + 0x6e, 0x67, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x22, 0xdd, 0x02, 0x0a, 0x13, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x12, 0x38, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x24, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x64, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x32, 0x0a, 0x0b, 0x70, + 0x65, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x3e, 0x0a, 0x0f, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0e, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, + 0x3f, 0x0a, 0x09, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x52, 0x45, + 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x03, + 0x22, 0xd1, 0x01, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x5f, 0x69, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, + 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x65, 0x6e, 0x64, 0x70, + 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, + 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x49, + 0x70, 0x12, 0x31, 0x0a, 0x14, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x5f, + 0x6b, 0x65, 0x65, 0x70, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x13, 0x70, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x65, 0x70, 0x61, + 0x6c, 0x69, 0x76, 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x0e, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, + 0x4c, 0x0a, 0x0d, 0x70, 0x61, 0x74, 0x68, 0x5f, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x55, + 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x50, 0x61, + 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x0c, 0x70, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x3f, 0x0a, + 0x11, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xfa, + 0x01, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x44, + 0x61, 0x74, 0x61, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, + 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x32, 0x48, 0x0a, 0x0c, 0x50, + 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x15, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x13, 0x2e, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6e, 0x65, 0x74, 0x62, 0x69, 0x72, 0x64, 0x69, 0x6f, 0x2f, 0x6e, 0x65, + 0x74, 0x62, 0x69, 0x72, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_pkg_grpc_proto_proxy_proto_rawDescOnce sync.Once + file_pkg_grpc_proto_proxy_proto_rawDescData = file_pkg_grpc_proto_proxy_proto_rawDesc +) + +func file_pkg_grpc_proto_proxy_proto_rawDescGZIP() []byte { + file_pkg_grpc_proto_proxy_proto_rawDescOnce.Do(func() { + file_pkg_grpc_proto_proxy_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_grpc_proto_proxy_proto_rawDescData) + }) + return file_pkg_grpc_proto_proxy_proto_rawDescData +} + +var file_pkg_grpc_proto_proxy_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_pkg_grpc_proto_proxy_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_pkg_grpc_proto_proxy_proto_goTypes = []interface{}{ + (ProxyEvent_EventType)(0), // 0: proxy.ProxyEvent.EventType + (ProxyLog_LogLevel)(0), // 1: proxy.ProxyLog.LogLevel + (ControlCommand_CommandType)(0), // 2: proxy.ControlCommand.CommandType + (ExposedServiceEvent_EventType)(0), // 3: proxy.ExposedServiceEvent.EventType + (*ProxyMessage)(nil), // 4: proxy.ProxyMessage + (*ControlMessage)(nil), // 5: proxy.ControlMessage + (*ProxyStats)(nil), // 6: proxy.ProxyStats + (*ProxyEvent)(nil), // 7: proxy.ProxyEvent + (*ProxyLog)(nil), // 8: proxy.ProxyLog + (*ProxyHeartbeat)(nil), // 9: proxy.ProxyHeartbeat + (*ControlEvent)(nil), // 10: proxy.ControlEvent + (*ControlCommand)(nil), // 11: proxy.ControlCommand + (*ControlConfig)(nil), // 12: proxy.ControlConfig + (*ExposedServiceEvent)(nil), // 13: proxy.ExposedServiceEvent + (*PeerConfig)(nil), // 14: proxy.PeerConfig + (*UpstreamConfig)(nil), // 15: proxy.UpstreamConfig + (*ProxyRequestData)(nil), // 16: proxy.ProxyRequestData + nil, // 17: proxy.ProxyStats.StatusCodeCountsEntry + nil, // 18: proxy.ProxyEvent.MetadataEntry + nil, // 19: proxy.ProxyLog.FieldsEntry + nil, // 20: proxy.ControlCommand.ParametersEntry + nil, // 21: proxy.ControlConfig.SettingsEntry + nil, // 22: proxy.UpstreamConfig.PathMappingsEntry + (*timestamppb.Timestamp)(nil), // 23: google.protobuf.Timestamp +} +var file_pkg_grpc_proto_proxy_proto_depIdxs = []int32{ + 6, // 0: proxy.ProxyMessage.stats:type_name -> proxy.ProxyStats + 7, // 1: proxy.ProxyMessage.event:type_name -> proxy.ProxyEvent + 8, // 2: proxy.ProxyMessage.log:type_name -> proxy.ProxyLog + 9, // 3: proxy.ProxyMessage.heartbeat:type_name -> proxy.ProxyHeartbeat + 16, // 4: proxy.ProxyMessage.request_data:type_name -> proxy.ProxyRequestData + 10, // 5: proxy.ControlMessage.event:type_name -> proxy.ControlEvent + 11, // 6: proxy.ControlMessage.command:type_name -> proxy.ControlCommand + 12, // 7: proxy.ControlMessage.config:type_name -> proxy.ControlConfig + 13, // 8: proxy.ControlMessage.exposed_service:type_name -> proxy.ExposedServiceEvent + 23, // 9: proxy.ProxyStats.timestamp:type_name -> google.protobuf.Timestamp + 17, // 10: proxy.ProxyStats.status_code_counts:type_name -> proxy.ProxyStats.StatusCodeCountsEntry + 23, // 11: proxy.ProxyEvent.timestamp:type_name -> google.protobuf.Timestamp + 0, // 12: proxy.ProxyEvent.type:type_name -> proxy.ProxyEvent.EventType + 18, // 13: proxy.ProxyEvent.metadata:type_name -> proxy.ProxyEvent.MetadataEntry + 23, // 14: proxy.ProxyLog.timestamp:type_name -> google.protobuf.Timestamp + 1, // 15: proxy.ProxyLog.level:type_name -> proxy.ProxyLog.LogLevel + 19, // 16: proxy.ProxyLog.fields:type_name -> proxy.ProxyLog.FieldsEntry + 23, // 17: proxy.ProxyHeartbeat.timestamp:type_name -> google.protobuf.Timestamp + 23, // 18: proxy.ControlEvent.timestamp:type_name -> google.protobuf.Timestamp + 2, // 19: proxy.ControlCommand.type:type_name -> proxy.ControlCommand.CommandType + 20, // 20: proxy.ControlCommand.parameters:type_name -> proxy.ControlCommand.ParametersEntry + 21, // 21: proxy.ControlConfig.settings:type_name -> proxy.ControlConfig.SettingsEntry + 23, // 22: proxy.ExposedServiceEvent.timestamp:type_name -> google.protobuf.Timestamp + 3, // 23: proxy.ExposedServiceEvent.type:type_name -> proxy.ExposedServiceEvent.EventType + 14, // 24: proxy.ExposedServiceEvent.peer_config:type_name -> proxy.PeerConfig + 15, // 25: proxy.ExposedServiceEvent.upstream_config:type_name -> proxy.UpstreamConfig + 22, // 26: proxy.UpstreamConfig.path_mappings:type_name -> proxy.UpstreamConfig.PathMappingsEntry + 23, // 27: proxy.ProxyRequestData.timestamp:type_name -> google.protobuf.Timestamp + 5, // 28: proxy.ProxyService.Stream:input_type -> proxy.ControlMessage + 4, // 29: proxy.ProxyService.Stream:output_type -> proxy.ProxyMessage + 29, // [29:30] is the sub-list for method output_type + 28, // [28:29] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 28, // [28:28] is the sub-list for extension extendee + 0, // [0:28] is the sub-list for field type_name +} + +func init() { file_pkg_grpc_proto_proxy_proto_init() } +func file_pkg_grpc_proto_proxy_proto_init() { + if File_pkg_grpc_proto_proxy_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_grpc_proto_proxy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ControlMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyStats); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyLog); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyHeartbeat); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ControlEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ControlCommand); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ControlConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ExposedServiceEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeerConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpstreamConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProxyRequestData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_pkg_grpc_proto_proxy_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*ProxyMessage_Stats)(nil), + (*ProxyMessage_Event)(nil), + (*ProxyMessage_Log)(nil), + (*ProxyMessage_Heartbeat)(nil), + (*ProxyMessage_RequestData)(nil), + } + file_pkg_grpc_proto_proxy_proto_msgTypes[1].OneofWrappers = []interface{}{ + (*ControlMessage_Event)(nil), + (*ControlMessage_Command)(nil), + (*ControlMessage_Config)(nil), + (*ControlMessage_ExposedService)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_grpc_proto_proxy_proto_rawDesc, + NumEnums: 4, + NumMessages: 19, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pkg_grpc_proto_proxy_proto_goTypes, + DependencyIndexes: file_pkg_grpc_proto_proxy_proto_depIdxs, + EnumInfos: file_pkg_grpc_proto_proxy_proto_enumTypes, + MessageInfos: file_pkg_grpc_proto_proxy_proto_msgTypes, + }.Build() + File_pkg_grpc_proto_proxy_proto = out.File + file_pkg_grpc_proto_proxy_proto_rawDesc = nil + file_pkg_grpc_proto_proxy_proto_goTypes = nil + file_pkg_grpc_proto_proxy_proto_depIdxs = nil +} diff --git a/proxy/pkg/grpc/proto/proxy.proto b/proxy/pkg/grpc/proto/proxy.proto new file mode 100644 index 000000000..52a0e9c6e --- /dev/null +++ b/proxy/pkg/grpc/proto/proxy.proto @@ -0,0 +1,159 @@ +syntax = "proto3"; + +package proxy; + +option go_package = "github.com/netbirdio/netbird/proxy/pkg/grpc/proto"; + +import "google/protobuf/timestamp.proto"; + +// ProxyService defines the bidirectional streaming service +// The proxy runs this service, control service connects as client +service ProxyService { + // Stream establishes a bidirectional stream between proxy and control service + // Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage + rpc Stream(stream ControlMessage) returns (stream ProxyMessage); +} + +// ProxyMessage represents messages sent from proxy to control service +message ProxyMessage { + oneof message { + ProxyStats stats = 1; + ProxyEvent event = 2; + ProxyLog log = 3; + ProxyHeartbeat heartbeat = 4; + ProxyRequestData request_data = 5; + } +} + +// ControlMessage represents messages sent from control service to proxy +message ControlMessage { + oneof message { + ControlEvent event = 1; + ControlCommand command = 2; + ControlConfig config = 3; + ExposedServiceEvent exposed_service = 4; + } +} + +// ProxyStats contains proxy statistics +message ProxyStats { + google.protobuf.Timestamp timestamp = 1; + uint64 total_requests = 2; + uint64 active_connections = 3; + uint64 bytes_sent = 4; + uint64 bytes_received = 5; + double cpu_usage = 6; + double memory_usage_mb = 7; + map status_code_counts = 8; +} + +// ProxyEvent represents events from the proxy +message ProxyEvent { + google.protobuf.Timestamp timestamp = 1; + EventType type = 2; + string message = 3; + map metadata = 4; + + enum EventType { + UNKNOWN = 0; + STARTED = 1; + STOPPED = 2; + ERROR = 3; + BACKEND_UNAVAILABLE = 4; + BACKEND_RECOVERED = 5; + CONFIG_UPDATED = 6; + } +} + +// ProxyLog represents log entries +message ProxyLog { + google.protobuf.Timestamp timestamp = 1; + LogLevel level = 2; + string message = 3; + map fields = 4; + + enum LogLevel { + DEBUG = 0; + INFO = 1; + WARN = 2; + ERROR = 3; + } +} + +// ProxyHeartbeat is sent periodically to keep connection alive +message ProxyHeartbeat { + google.protobuf.Timestamp timestamp = 1; + string proxy_id = 2; +} + +// ControlEvent represents events from control service +message ControlEvent { + google.protobuf.Timestamp timestamp = 1; + string event_id = 2; + string message = 3; +} + +// ControlCommand represents commands sent to proxy +message ControlCommand { + string command_id = 1; + CommandType type = 2; + map parameters = 3; + + enum CommandType { + UNKNOWN = 0; + RELOAD_CONFIG = 1; + ENABLE_DEBUG = 2; + DISABLE_DEBUG = 3; + GET_STATS = 4; + SHUTDOWN = 5; + } +} + +// ControlConfig contains configuration updates from control service +message ControlConfig { + string config_version = 1; + map settings = 2; +} + +// ExposedServiceEvent represents exposed service lifecycle events +message ExposedServiceEvent { + google.protobuf.Timestamp timestamp = 1; + EventType type = 2; + string service_id = 3; + PeerConfig peer_config = 4; + UpstreamConfig upstream_config = 5; + + enum EventType { + UNKNOWN = 0; + CREATED = 1; + UPDATED = 2; + REMOVED = 3; + } +} + +// PeerConfig contains WireGuard peer configuration +message PeerConfig { + string peer_id = 1; + string public_key = 2; + repeated string allowed_ips = 3; + string endpoint = 4; + string tunnel_ip = 5; + uint32 persistent_keepalive = 6; +} + +// UpstreamConfig contains reverse proxy upstream configuration +message UpstreamConfig { + string domain = 1; + map path_mappings = 2; // path -> port +} + +// ProxyRequestData contains metadata about requests routed through the reverse proxy +message ProxyRequestData { + google.protobuf.Timestamp timestamp = 1; + string service_id = 2; + string path = 3; + int64 duration_ms = 4; + string method = 5; // HTTP method (GET, POST, PUT, DELETE, etc.) + int32 response_code = 6; + string source_ip = 7; +} \ No newline at end of file diff --git a/proxy/pkg/grpc/proto/proxy_grpc.pb.go b/proxy/pkg/grpc/proto/proxy_grpc.pb.go new file mode 100644 index 000000000..d0088a790 --- /dev/null +++ b/proxy/pkg/grpc/proto/proxy_grpc.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package proto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// ProxyServiceClient is the client API for ProxyService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ProxyServiceClient interface { + // Stream establishes a bidirectional stream between proxy and control service + // Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage + Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error) +} + +type proxyServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewProxyServiceClient(cc grpc.ClientConnInterface) ProxyServiceClient { + return &proxyServiceClient{cc} +} + +func (c *proxyServiceClient) Stream(ctx context.Context, opts ...grpc.CallOption) (ProxyService_StreamClient, error) { + stream, err := c.cc.NewStream(ctx, &ProxyService_ServiceDesc.Streams[0], "/proxy.ProxyService/Stream", opts...) + if err != nil { + return nil, err + } + x := &proxyServiceStreamClient{stream} + return x, nil +} + +type ProxyService_StreamClient interface { + Send(*ControlMessage) error + Recv() (*ProxyMessage, error) + grpc.ClientStream +} + +type proxyServiceStreamClient struct { + grpc.ClientStream +} + +func (x *proxyServiceStreamClient) Send(m *ControlMessage) error { + return x.ClientStream.SendMsg(m) +} + +func (x *proxyServiceStreamClient) Recv() (*ProxyMessage, error) { + m := new(ProxyMessage) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// ProxyServiceServer is the server API for ProxyService service. +// All implementations must embed UnimplementedProxyServiceServer +// for forward compatibility +type ProxyServiceServer interface { + // Stream establishes a bidirectional stream between proxy and control service + // Control service (client) sends ControlMessage, Proxy (server) sends ProxyMessage + Stream(ProxyService_StreamServer) error + mustEmbedUnimplementedProxyServiceServer() +} + +// UnimplementedProxyServiceServer must be embedded to have forward compatible implementations. +type UnimplementedProxyServiceServer struct { +} + +func (UnimplementedProxyServiceServer) Stream(ProxyService_StreamServer) error { + return status.Errorf(codes.Unimplemented, "method Stream not implemented") +} +func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {} + +// UnsafeProxyServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ProxyServiceServer will +// result in compilation errors. +type UnsafeProxyServiceServer interface { + mustEmbedUnimplementedProxyServiceServer() +} + +func RegisterProxyServiceServer(s grpc.ServiceRegistrar, srv ProxyServiceServer) { + s.RegisterService(&ProxyService_ServiceDesc, srv) +} + +func _ProxyService_Stream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(ProxyServiceServer).Stream(&proxyServiceStreamServer{stream}) +} + +type ProxyService_StreamServer interface { + Send(*ProxyMessage) error + Recv() (*ControlMessage, error) + grpc.ServerStream +} + +type proxyServiceStreamServer struct { + grpc.ServerStream +} + +func (x *proxyServiceStreamServer) Send(m *ProxyMessage) error { + return x.ServerStream.SendMsg(m) +} + +func (x *proxyServiceStreamServer) Recv() (*ControlMessage, error) { + m := new(ControlMessage) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// ProxyService_ServiceDesc is the grpc.ServiceDesc for ProxyService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ProxyService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "proxy.ProxyService", + HandlerType: (*ProxyServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Stream", + Handler: _ProxyService_Stream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "pkg/grpc/proto/proxy.proto", +} diff --git a/proxy/pkg/grpc/server.go b/proxy/pkg/grpc/server.go new file mode 100644 index 000000000..15e3285c4 --- /dev/null +++ b/proxy/pkg/grpc/server.go @@ -0,0 +1,286 @@ +package grpc + +import ( + "context" + "fmt" + "net" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/keepalive" + + pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto" +) + +// StreamHandler handles incoming messages from control service +type StreamHandler interface { + HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error + HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error + HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error + HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error +} + +// Server represents the gRPC server running on the proxy +type Server struct { + pb.UnimplementedProxyServiceServer + + listenAddr string + grpcServer *grpc.Server + handler StreamHandler + + mu sync.RWMutex + streams map[string]*StreamContext + isRunning bool +} + +// StreamContext holds the context for each active stream +type StreamContext struct { + stream pb.ProxyService_StreamServer + sendChan chan *pb.ProxyMessage + ctx context.Context + cancel context.CancelFunc + controlID string // ID of the connected control service +} + +// Config holds gRPC server configuration +type Config struct { + ListenAddr string + Handler StreamHandler +} + +// NewServer creates a new gRPC server +func NewServer(config Config) *Server { + return &Server{ + listenAddr: config.ListenAddr, + handler: config.Handler, + streams: make(map[string]*StreamContext), + } +} + +// Start starts the gRPC server +func (s *Server) Start() error { + s.mu.Lock() + if s.isRunning { + s.mu.Unlock() + return fmt.Errorf("gRPC server already running") + } + s.isRunning = true + s.mu.Unlock() + + lis, err := net.Listen("tcp", s.listenAddr) + if err != nil { + s.mu.Lock() + s.isRunning = false + s.mu.Unlock() + return fmt.Errorf("failed to listen: %w", err) + } + + // Configure gRPC server with keepalive + s.grpcServer = grpc.NewServer( + grpc.KeepaliveParams(keepalive.ServerParameters{ + Time: 30 * time.Second, + Timeout: 10 * time.Second, + }), + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: 10 * time.Second, + PermitWithoutStream: true, + }), + ) + + pb.RegisterProxyServiceServer(s.grpcServer, s) + + log.Infof("gRPC server listening on %s", s.listenAddr) + + if err := s.grpcServer.Serve(lis); err != nil { + s.mu.Lock() + s.isRunning = false + s.mu.Unlock() + return fmt.Errorf("failed to serve: %w", err) + } + + return nil +} + +// Stop gracefully stops the gRPC server +func (s *Server) Stop(ctx context.Context) error { + s.mu.Lock() + if !s.isRunning { + s.mu.Unlock() + return fmt.Errorf("gRPC server not running") + } + s.mu.Unlock() + + log.Info("Stopping gRPC server...") + + // Cancel all active streams + s.mu.Lock() + for _, streamCtx := range s.streams { + streamCtx.cancel() + close(streamCtx.sendChan) + } + s.streams = make(map[string]*StreamContext) + s.mu.Unlock() + + // Graceful stop with timeout + stopped := make(chan struct{}) + go func() { + s.grpcServer.GracefulStop() + close(stopped) + }() + + select { + case <-stopped: + log.Info("gRPC server stopped gracefully") + case <-ctx.Done(): + log.Warn("gRPC server graceful stop timeout, forcing stop") + s.grpcServer.Stop() + } + + s.mu.Lock() + s.isRunning = false + s.mu.Unlock() + + return nil +} + +// Stream implements the bidirectional streaming RPC +// The control service connects as client, proxy is server +// Control service sends ControlMessage, Proxy sends ProxyMessage +func (s *Server) Stream(stream pb.ProxyService_StreamServer) error { + ctx, cancel := context.WithCancel(stream.Context()) + defer cancel() + + controlID := fmt.Sprintf("control-%d", time.Now().Unix()) + + // Create stream context + streamCtx := &StreamContext{ + stream: stream, + sendChan: make(chan *pb.ProxyMessage, 100), + ctx: ctx, + cancel: cancel, + controlID: controlID, + } + + // Register stream + s.mu.Lock() + s.streams[controlID] = streamCtx + s.mu.Unlock() + + log.Infof("Control service connected: %s", controlID) + + // Start goroutine to send ProxyMessages to control service + sendDone := make(chan error, 1) + go s.sendLoop(streamCtx, sendDone) + + // Start goroutine to receive ControlMessages from control service + recvDone := make(chan error, 1) + go s.receiveLoop(streamCtx, recvDone) + + // Wait for either send or receive to complete + select { + case err := <-sendDone: + log.Infof("Control service %s send loop ended: %v", controlID, err) + return err + case err := <-recvDone: + log.Infof("Control service %s receive loop ended: %v", controlID, err) + return err + case <-ctx.Done(): + log.Infof("Control service %s context done: %v", controlID, ctx.Err()) + return ctx.Err() + } +} + +// sendLoop handles sending ProxyMessages to the control service +func (s *Server) sendLoop(streamCtx *StreamContext, done chan<- error) { + for { + select { + case msg, ok := <-streamCtx.sendChan: + if !ok { + done <- nil + return + } + + // Send ProxyMessage to control service + if err := streamCtx.stream.Send(msg); err != nil { + log.Errorf("Failed to send message to control service: %v", err) + done <- err + return + } + + case <-streamCtx.ctx.Done(): + done <- streamCtx.ctx.Err() + return + } + } +} + +// receiveLoop handles receiving ControlMessages from the control service +func (s *Server) receiveLoop(streamCtx *StreamContext, done chan<- error) { + for { + // Receive ControlMessage from control service (client) + controlMsg, err := streamCtx.stream.Recv() + if err != nil { + log.Debugf("Stream receive error: %v", err) + done <- err + return + } + + // Handle different ControlMessage types + switch m := controlMsg.Message.(type) { + case *pb.ControlMessage_Event: + if s.handler != nil { + if err := s.handler.HandleControlEvent(streamCtx.ctx, m.Event); err != nil { + log.Errorf("Failed to handle control event: %v", err) + } + } + + case *pb.ControlMessage_Command: + if s.handler != nil { + if err := s.handler.HandleControlCommand(streamCtx.ctx, m.Command); err != nil { + log.Errorf("Failed to handle control command: %v", err) + } + } + + case *pb.ControlMessage_Config: + if s.handler != nil { + if err := s.handler.HandleControlConfig(streamCtx.ctx, m.Config); err != nil { + log.Errorf("Failed to handle control config: %v", err) + } + } + + case *pb.ControlMessage_ExposedService: + if s.handler != nil { + if err := s.handler.HandleExposedServiceEvent(streamCtx.ctx, m.ExposedService); err != nil { + log.Errorf("Failed to handle exposed service event: %v", err) + } + } + + default: + log.Warnf("Received unknown control message type: %T", m) + } + } +} + +// SendProxyMessage sends a ProxyMessage to all connected control services +func (s *Server) SendProxyMessage(msg *pb.ProxyMessage) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, streamCtx := range s.streams { + select { + case streamCtx.sendChan <- msg: + // Message queued successfully + default: + log.Warn("Send channel full, dropping message") + } + } +} + +// GetActiveStreams returns the number of active streams +func (s *Server) GetActiveStreams() int { + s.mu.RLock() + defer s.mu.RUnlock() + return len(s.streams) +} diff --git a/proxy/pkg/proxy/config.go b/proxy/pkg/proxy/config.go new file mode 100644 index 000000000..c3776307c --- /dev/null +++ b/proxy/pkg/proxy/config.go @@ -0,0 +1,125 @@ +package proxy + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "time" + + "github.com/caarlos0/env/v11" +) + +var ( + ErrFailedToParseConfig = errors.New("failed to parse config from env") +) + +// Config holds the configuration for the reverse proxy server +type Config struct { + // ListenAddress is the address the proxy server will listen on (e.g., ":443" or "0.0.0.0:443") + ListenAddress string `env:"NB_PROXY_LISTEN_ADDRESS" envDefault:":443" json:"listen_address"` + + // ReadTimeout is the maximum duration for reading the entire request, including the body + ReadTimeout time.Duration `env:"NB_PROXY_READ_TIMEOUT" envDefault:"30s" json:"read_timeout"` + + // WriteTimeout is the maximum duration before timing out writes of the response + WriteTimeout time.Duration `env:"NB_PROXY_WRITE_TIMEOUT" envDefault:"30s" json:"write_timeout"` + + // IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled + IdleTimeout time.Duration `env:"NB_PROXY_IDLE_TIMEOUT" envDefault:"60s" json:"idle_timeout"` + + // ShutdownTimeout is the maximum duration to wait for graceful shutdown + ShutdownTimeout time.Duration `env:"NB_PROXY_SHUTDOWN_TIMEOUT" envDefault:"10s" json:"shutdown_timeout"` + + // LogLevel sets the logging verbosity (debug, info, warn, error) + LogLevel string `env:"NB_PROXY_LOG_LEVEL" envDefault:"info" json:"log_level"` + + // GRPCListenAddress is the address for the gRPC control server (empty to disable) + GRPCListenAddress string `env:"NB_PROXY_GRPC_LISTEN_ADDRESS" envDefault:":50051" json:"grpc_listen_address"` + + // ProxyID is a unique identifier for this proxy instance + ProxyID string `env:"NB_PROXY_ID" envDefault:"" json:"proxy_id"` + + // EnableGRPC enables the gRPC control server + EnableGRPC bool `env:"NB_PROXY_ENABLE_GRPC" envDefault:"false" json:"enable_grpc"` +} + +// ParseAndLoad parses configuration from environment variables +func ParseAndLoad() (Config, error) { + var cfg Config + + if err := env.Parse(&cfg); err != nil { + return cfg, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err) + } + + if err := cfg.Validate(); err != nil { + return cfg, fmt.Errorf("invalid config: %w", err) + } + + return cfg, nil +} + +// LoadFromFile reads configuration from a JSON file +func LoadFromFile(path string) (Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return Config{}, fmt.Errorf("failed to read config file: %w", err) + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return Config{}, fmt.Errorf("failed to parse config file: %w", err) + } + + if err := cfg.Validate(); err != nil { + return Config{}, fmt.Errorf("invalid config: %w", err) + } + + return cfg, nil +} + +// LoadFromFileOrEnv loads configuration from a file if path is provided, otherwise from environment variables +// Environment variables will override file-based configuration if both are present +func LoadFromFileOrEnv(configPath string) (Config, error) { + var cfg Config + + // If config file is provided, load it first + if configPath != "" { + fileCfg, err := LoadFromFile(configPath) + if err != nil { + return Config{}, fmt.Errorf("failed to load config from file: %w", err) + } + cfg = fileCfg + } + + // Parse environment variables (will override file config with any set env vars) + if err := env.Parse(&cfg); err != nil { + return Config{}, fmt.Errorf("%w: %s", ErrFailedToParseConfig, err) + } + + if err := cfg.Validate(); err != nil { + return Config{}, fmt.Errorf("invalid config: %w", err) + } + + return cfg, nil +} + +// Validate checks if the configuration is valid +func (c *Config) Validate() error { + if c.ListenAddress == "" { + return errors.New("listen_address is required") + } + + validLogLevels := map[string]bool{ + "debug": true, + "info": true, + "warn": true, + "error": true, + } + + if !validLogLevels[c.LogLevel] { + return fmt.Errorf("invalid log_level: %s (must be debug, info, warn, or error)", c.LogLevel) + } + + return nil +} diff --git a/proxy/pkg/proxy/server.go b/proxy/pkg/proxy/server.go new file mode 100644 index 000000000..9cdf3c679 --- /dev/null +++ b/proxy/pkg/proxy/server.go @@ -0,0 +1,570 @@ +package proxy + +import ( + "context" + "fmt" + "sync" + "time" + + log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/netbirdio/netbird/proxy/internal/reverseproxy" + grpcpkg "github.com/netbirdio/netbird/proxy/pkg/grpc" + pb "github.com/netbirdio/netbird/proxy/pkg/grpc/proto" +) + +// Server represents the reverse proxy server with integrated gRPC control server +type Server struct { + config Config + grpcServer *grpcpkg.Server + caddyProxy *reverseproxy.CaddyProxy + + mu sync.RWMutex + isRunning bool + grpcRunning bool + + shutdownCtx context.Context + cancelFunc context.CancelFunc + + // Statistics for gRPC reporting + stats *Stats + + // Track exposed services and their peer configs + exposedServices map[string]*ExposedServiceConfig +} + +// Stats holds proxy statistics +type Stats struct { + mu sync.RWMutex + totalRequests uint64 + activeConns uint64 + bytesSent uint64 + bytesReceived uint64 +} + +// ExposedServiceConfig holds the configuration for an exposed service +type ExposedServiceConfig struct { + ServiceID string + PeerConfig *PeerConfig + UpstreamConfig *UpstreamConfig +} + +// PeerConfig holds WireGuard peer configuration +type PeerConfig struct { + PeerID string + PublicKey string + AllowedIPs []string + Endpoint string + TunnelIP string // The WireGuard tunnel IP to route traffic to +} + +// UpstreamConfig holds reverse proxy upstream configuration +type UpstreamConfig struct { + Domain string + PathMappings map[string]string // path -> port mapping (relative to tunnel IP) +} + +// NewServer creates a new reverse proxy server instance +func NewServer(config Config) (*Server, error) { + // Validate config + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + shutdownCtx, cancelFunc := context.WithCancel(context.Background()) + + server := &Server{ + config: config, + isRunning: false, + grpcRunning: false, + shutdownCtx: shutdownCtx, + cancelFunc: cancelFunc, + stats: &Stats{}, + exposedServices: make(map[string]*ExposedServiceConfig), + } + + // Create Caddy reverse proxy with request callback + caddyConfig := reverseproxy.Config{ + ListenAddress: ":54321", // Use port 54321 for local testing + EnableHTTPS: false, // TODO: Add HTTPS support + RequestDataCallback: func(data *reverseproxy.RequestData) { + // This is where access log data arrives - SET BREAKPOINT HERE + log.WithFields(log.Fields{ + "service_id": data.ServiceID, + "method": data.Method, + "path": data.Path, + "response_code": data.ResponseCode, + "duration_ms": data.DurationMs, + "source_ip": data.SourceIP, + }).Info("Access log received") + + // TODO: Send via gRPC to control service + // This would send pb.ProxyRequestData via the gRPC stream + }, + } + caddyProxy, err := reverseproxy.New(caddyConfig) + if err != nil { + return nil, fmt.Errorf("failed to create Caddy proxy: %w", err) + } + server.caddyProxy = caddyProxy + + // Create gRPC server if enabled + if config.EnableGRPC && config.GRPCListenAddress != "" { + grpcConfig := grpcpkg.Config{ + ListenAddr: config.GRPCListenAddress, + Handler: server, // Server implements StreamHandler interface + } + server.grpcServer = grpcpkg.NewServer(grpcConfig) + } + + return server, nil +} + +// Start starts the reverse proxy server and optionally the gRPC control server +func (s *Server) Start() error { + s.mu.Lock() + if s.isRunning { + s.mu.Unlock() + return fmt.Errorf("server is already running") + } + s.isRunning = true + s.mu.Unlock() + + log.Infof("Starting Caddy reverse proxy server on %s", s.config.ListenAddress) + + // Start Caddy proxy + if err := s.caddyProxy.Start(); err != nil { + s.mu.Lock() + s.isRunning = false + s.mu.Unlock() + return fmt.Errorf("failed to start Caddy proxy: %w", err) + } + + // Start gRPC server if configured + if s.grpcServer != nil { + s.mu.Lock() + s.grpcRunning = true + s.mu.Unlock() + + go func() { + log.Infof("Starting gRPC control server on %s", s.config.GRPCListenAddress) + if err := s.grpcServer.Start(); err != nil { + log.Errorf("gRPC server error: %v", err) + s.mu.Lock() + s.grpcRunning = false + s.mu.Unlock() + } + }() + + // Send started event + time.Sleep(100 * time.Millisecond) // Give gRPC server time to start + s.sendProxyEvent(pb.ProxyEvent_STARTED, "Proxy server started") + } + + if err := s.caddyProxy.AddRoute( + &reverseproxy.RouteConfig{ + ID: "test", + Domain: "test.netbird.io", + PathMappings: map[string]string{"/": "localhost:8080"}, + }); err != nil { + log.Warn("Failed to add test route: ", err) + } + + // Block forever - Caddy runs in background + <-s.shutdownCtx.Done() + return nil +} + +// Stop gracefully shuts down both Caddy and gRPC servers +func (s *Server) Stop(ctx context.Context) error { + s.mu.Lock() + if !s.isRunning { + s.mu.Unlock() + return fmt.Errorf("server is not running") + } + s.mu.Unlock() + + log.Info("Shutting down servers gracefully...") + + // If no context provided, use the server's shutdown timeout + if ctx == nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), s.config.ShutdownTimeout) + defer cancel() + } + + // Send stopped event before shutdown + if s.grpcServer != nil && s.grpcRunning { + s.sendProxyEvent(pb.ProxyEvent_STOPPED, "Proxy server shutting down") + } + + var caddyErr, grpcErr error + + // Shutdown gRPC server first + if s.grpcServer != nil && s.grpcRunning { + if err := s.grpcServer.Stop(ctx); err != nil { + grpcErr = fmt.Errorf("gRPC server shutdown failed: %w", err) + log.Error(grpcErr) + } + s.mu.Lock() + s.grpcRunning = false + s.mu.Unlock() + } + + // Shutdown Caddy proxy + if err := s.caddyProxy.Stop(ctx); err != nil { + caddyErr = fmt.Errorf("Caddy proxy shutdown failed: %w", err) + log.Error(caddyErr) + } + + s.mu.Lock() + s.isRunning = false + s.mu.Unlock() + + if caddyErr != nil { + return caddyErr + } + if grpcErr != nil { + return grpcErr + } + + log.Info("All servers stopped successfully") + return nil +} + +// IsRunning returns whether the server is currently running +func (s *Server) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.isRunning +} + +// GetConfig returns a copy of the server configuration +func (s *Server) GetConfig() Config { + return s.config +} + +// GetStats returns a copy of current statistics +func (s *Server) GetStats() *pb.ProxyStats { + s.stats.mu.RLock() + defer s.stats.mu.RUnlock() + + return &pb.ProxyStats{ + Timestamp: timestamppb.Now(), + TotalRequests: s.stats.totalRequests, + ActiveConnections: s.stats.activeConns, + BytesSent: s.stats.bytesSent, + BytesReceived: s.stats.bytesReceived, + } +} + +// StreamHandler interface implementation + +// HandleControlEvent handles incoming control events +// This is where ExposedService events will be routed +func (s *Server) HandleControlEvent(ctx context.Context, event *pb.ControlEvent) error { + log.WithFields(log.Fields{ + "event_id": event.EventId, + "message": event.Message, + }).Info("Received control event") + + // TODO: Parse event type and route to appropriate handler + // if event.Type == "ExposedServiceCreated" { + // return s.handleExposedServiceCreated(ctx, event) + // } else if event.Type == "ExposedServiceUpdated" { + // return s.handleExposedServiceUpdated(ctx, event) + // } else if event.Type == "ExposedServiceRemoved" { + // return s.handleExposedServiceRemoved(ctx, event) + // } + + return nil +} + +// HandleControlCommand handles incoming control commands +func (s *Server) HandleControlCommand(ctx context.Context, command *pb.ControlCommand) error { + log.WithFields(log.Fields{ + "command_id": command.CommandId, + "type": command.Type.String(), + }).Info("Received control command") + + switch command.Type { + case pb.ControlCommand_GET_STATS: + // Stats are automatically sent, just log + log.Debug("Stats requested via command") + case pb.ControlCommand_RELOAD_CONFIG: + log.Info("Config reload requested (not implemented yet)") + case pb.ControlCommand_ENABLE_DEBUG: + log.SetLevel(log.DebugLevel) + log.Info("Debug logging enabled") + case pb.ControlCommand_DISABLE_DEBUG: + log.SetLevel(log.InfoLevel) + log.Info("Debug logging disabled") + case pb.ControlCommand_SHUTDOWN: + log.Warn("Shutdown command received") + go func() { + time.Sleep(1 * time.Second) + s.cancelFunc() // Trigger graceful shutdown + }() + } + + return nil +} + +// HandleControlConfig handles incoming configuration updates +func (s *Server) HandleControlConfig(ctx context.Context, config *pb.ControlConfig) error { + log.WithFields(log.Fields{ + "config_version": config.ConfigVersion, + "settings": config.Settings, + }).Info("Received config update") + return nil +} + +// HandleExposedServiceEvent handles exposed service lifecycle events +func (s *Server) HandleExposedServiceEvent(ctx context.Context, event *pb.ExposedServiceEvent) error { + log.WithFields(log.Fields{ + "service_id": event.ServiceId, + "type": event.Type.String(), + }).Info("Received exposed service event") + + // Convert proto types to internal types + peerConfig := &PeerConfig{ + PeerID: event.PeerConfig.PeerId, + PublicKey: event.PeerConfig.PublicKey, + AllowedIPs: event.PeerConfig.AllowedIps, + Endpoint: event.PeerConfig.Endpoint, + TunnelIP: event.PeerConfig.TunnelIp, + } + + upstreamConfig := &UpstreamConfig{ + Domain: event.UpstreamConfig.Domain, + PathMappings: event.UpstreamConfig.PathMappings, + } + + // Route to appropriate handler based on event type + switch event.Type { + case pb.ExposedServiceEvent_CREATED: + return s.handleExposedServiceCreated(event.ServiceId, peerConfig, upstreamConfig) + + case pb.ExposedServiceEvent_UPDATED: + return s.handleExposedServiceUpdated(event.ServiceId, peerConfig, upstreamConfig) + + case pb.ExposedServiceEvent_REMOVED: + return s.handleExposedServiceRemoved(event.ServiceId) + + default: + return fmt.Errorf("unknown exposed service event type: %v", event.Type) + } +} + +// Exposed Service Handlers + +// handleExposedServiceCreated handles the creation of a new exposed service +func (s *Server) handleExposedServiceCreated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + + // Check if service already exists + if _, exists := s.exposedServices[serviceID]; exists { + return fmt.Errorf("exposed service %s already exists", serviceID) + } + + log.WithFields(log.Fields{ + "service_id": serviceID, + "peer_id": peerConfig.PeerID, + "tunnel_ip": peerConfig.TunnelIP, + "domain": upstreamConfig.Domain, + }).Info("Creating exposed service") + + // TODO: Create WireGuard tunnel for peer + // 1. Initialize WireGuard interface if not already done + // 2. Add peer configuration: + // - Public key: peerConfig.PublicKey + // - Endpoint: peerConfig.Endpoint + // - Allowed IPs: peerConfig.AllowedIPs + // - Persistent keepalive: 25 seconds + // 3. Bring up the WireGuard interface + // 4. Verify tunnel connectivity to peerConfig.TunnelIP + // Example pseudo-code: + // wgClient.AddPeer(&wireguard.PeerConfig{ + // PublicKey: peerConfig.PublicKey, + // Endpoint: peerConfig.Endpoint, + // AllowedIPs: peerConfig.AllowedIPs, + // PersistentKeepalive: 25, + // }) + + // Build path mappings with tunnel IP + pathMappings := make(map[string]string) + for path, port := range upstreamConfig.PathMappings { + // Combine tunnel IP with port + target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port) + pathMappings[path] = target + } + + // Add route to Caddy + route := &reverseproxy.RouteConfig{ + ID: serviceID, + Domain: upstreamConfig.Domain, + PathMappings: pathMappings, + } + + if err := s.caddyProxy.AddRoute(route); err != nil { + return fmt.Errorf("failed to add route: %w", err) + } + + // Store service config + s.exposedServices[serviceID] = &ExposedServiceConfig{ + ServiceID: serviceID, + PeerConfig: peerConfig, + UpstreamConfig: upstreamConfig, + } + + log.Infof("Exposed service %s created successfully", serviceID) + return nil +} + +// handleExposedServiceUpdated handles updates to an existing exposed service +func (s *Server) handleExposedServiceUpdated(serviceID string, peerConfig *PeerConfig, upstreamConfig *UpstreamConfig) error { + s.mu.Lock() + defer s.mu.Unlock() + + // Check if service exists + if _, exists := s.exposedServices[serviceID]; !exists { + return fmt.Errorf("exposed service %s not found", serviceID) + } + + log.WithFields(log.Fields{ + "service_id": serviceID, + "peer_id": peerConfig.PeerID, + "tunnel_ip": peerConfig.TunnelIP, + "domain": upstreamConfig.Domain, + }).Info("Updating exposed service") + + // TODO: Update WireGuard tunnel if peer config changed + + // Build path mappings with tunnel IP + pathMappings := make(map[string]string) + for path, port := range upstreamConfig.PathMappings { + target := fmt.Sprintf("%s:%s", peerConfig.TunnelIP, port) + pathMappings[path] = target + } + + // Update route in Caddy + route := &reverseproxy.RouteConfig{ + ID: serviceID, + Domain: upstreamConfig.Domain, + PathMappings: pathMappings, + } + + if err := s.caddyProxy.UpdateRoute(route); err != nil { + return fmt.Errorf("failed to update route: %w", err) + } + + // Update service config + s.exposedServices[serviceID] = &ExposedServiceConfig{ + ServiceID: serviceID, + PeerConfig: peerConfig, + UpstreamConfig: upstreamConfig, + } + + log.Infof("Exposed service %s updated successfully", serviceID) + return nil +} + +// handleExposedServiceRemoved handles the removal of an exposed service +func (s *Server) handleExposedServiceRemoved(serviceID string) error { + s.mu.Lock() + defer s.mu.Unlock() + + // Check if service exists + if _, exists := s.exposedServices[serviceID]; !exists { + return fmt.Errorf("exposed service %s not found", serviceID) + } + + log.WithFields(log.Fields{ + "service_id": serviceID, + }).Info("Removing exposed service") + + // Remove route from Caddy + if err := s.caddyProxy.RemoveRoute(serviceID); err != nil { + return fmt.Errorf("failed to remove route: %w", err) + } + + // TODO: Remove WireGuard tunnel for peer + + // Remove service config + delete(s.exposedServices, serviceID) + + log.Infof("Exposed service %s removed successfully", serviceID) + return nil +} + +// ListExposedServices returns a list of all exposed service IDs +func (s *Server) ListExposedServices() []string { + s.mu.RLock() + defer s.mu.RUnlock() + + services := make([]string, 0, len(s.exposedServices)) + for id := range s.exposedServices { + services = append(services, id) + } + return services +} + +// GetExposedService returns the configuration for a specific exposed service +func (s *Server) GetExposedService(serviceID string) (*ExposedServiceConfig, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + service, exists := s.exposedServices[serviceID] + if !exists { + return nil, fmt.Errorf("exposed service %s not found", serviceID) + } + + return service, nil +} + +// Helper methods + +func (s *Server) sendProxyEvent(eventType pb.ProxyEvent_EventType, message string) { + // This would typically be called to send events + // The actual sending happens via the gRPC stream + log.WithFields(log.Fields{ + "type": eventType.String(), + "message": message, + }).Debug("Proxy event") +} + +// Stats methods + +func (s *Stats) IncrementRequests() { + s.mu.Lock() + defer s.mu.Unlock() + s.totalRequests++ +} + +func (s *Stats) IncrementActiveConns() { + s.mu.Lock() + defer s.mu.Unlock() + s.activeConns++ +} + +func (s *Stats) DecrementActiveConns() { + s.mu.Lock() + defer s.mu.Unlock() + if s.activeConns > 0 { + s.activeConns-- + } +} + +func (s *Stats) AddBytesSent(bytes uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.bytesSent += bytes +} + +func (s *Stats) AddBytesReceived(bytes uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.bytesReceived += bytes +} diff --git a/proxy/pkg/version/version.go b/proxy/pkg/version/version.go new file mode 100644 index 000000000..3410720ea --- /dev/null +++ b/proxy/pkg/version/version.go @@ -0,0 +1,56 @@ +package version + +import ( + "fmt" + "runtime" +) + +var ( + // Version is the application version (set via ldflags during build) + Version = "dev" + + // Commit is the git commit hash (set via ldflags during build) + Commit = "unknown" + + // BuildDate is the build date (set via ldflags during build) + BuildDate = "unknown" + + // GoVersion is the Go version used to build the binary + GoVersion = runtime.Version() +) + +// Info contains version information +type Info struct { + Version string `json:"version"` + Commit string `json:"commit"` + BuildDate string `json:"build_date"` + GoVersion string `json:"go_version"` + OS string `json:"os"` + Arch string `json:"arch"` +} + +// Get returns the version information +func Get() Info { + return Info{ + Version: Version, + Commit: Commit, + BuildDate: BuildDate, + GoVersion: GoVersion, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + } +} + +// String returns a formatted version string +func String() string { + return fmt.Sprintf("Version: %s, Commit: %s, BuildDate: %s, Go: %s", + Version, Commit, BuildDate, GoVersion) +} + +// Short returns a short version string +func Short() string { + if Version == "dev" { + return fmt.Sprintf("%s (%s)", Version, Commit[:7]) + } + return Version +} diff --git a/proxy/proxy b/proxy/proxy new file mode 100755 index 000000000..12cebcfbb Binary files /dev/null and b/proxy/proxy differ diff --git a/proxy/scripts/generate-proto.sh b/proxy/scripts/generate-proto.sh new file mode 100755 index 000000000..f4c39c0c1 --- /dev/null +++ b/proxy/scripts/generate-proto.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +# Check if protoc is installed +if ! command -v protoc &> /dev/null; then + echo "Error: protoc is not installed" + echo "Install with: apt-get install -y protobuf-compiler" + exit 1 +fi + +# Check if protoc-gen-go is installed +if ! command -v protoc-gen-go &> /dev/null; then + echo "Installing protoc-gen-go..." + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +fi + +# Check if protoc-gen-go-grpc is installed +if ! command -v protoc-gen-go-grpc &> /dev/null; then + echo "Installing protoc-gen-go-grpc..." + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +fi + +echo "Generating protobuf files..." + +# Generate Go code from proto files +protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + pkg/grpc/proto/proxy.proto + +echo "Proto generation complete!" \ No newline at end of file