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