mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 16:26:38 +00:00
trying embedded caddy reverse proxy
This commit is contained in:
27
proxy/.gitignore
vendored
Normal file
27
proxy/.gitignore
vendored
Normal file
@@ -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
|
||||||
661
proxy/LICENSE
Normal file
661
proxy/LICENSE
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
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.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
83
proxy/Makefile
Normal file
83
proxy/Makefile
Normal file
@@ -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"
|
||||||
177
proxy/README.md
Normal file
177
proxy/README.md
Normal file
@@ -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.
|
||||||
120
proxy/cmd/root.go
Normal file
120
proxy/cmd/root.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
proxy/config.example.json
Normal file
9
proxy/config.example.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
135
proxy/go.mod
Normal file
135
proxy/go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
641
proxy/go.sum
Normal file
641
proxy/go.sum
Normal file
@@ -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=
|
||||||
109
proxy/internal/health/health.go
Normal file
109
proxy/internal/health/health.go
Normal file
@@ -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"))
|
||||||
|
}
|
||||||
12
proxy/internal/middleware/chain.go
Normal file
12
proxy/internal/middleware/chain.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
55
proxy/internal/middleware/logging.go
Normal file
55
proxy/internal/middleware/logging.go
Normal file
@@ -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")
|
||||||
|
})
|
||||||
|
}
|
||||||
33
proxy/internal/middleware/recovery.go
Normal file
33
proxy/internal/middleware/recovery.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
626
proxy/internal/reverseproxy/caddy.go
Normal file
626
proxy/internal/reverseproxy/caddy.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
225
proxy/internal/reverseproxy/logwriter.go
Normal file
225
proxy/internal/reverseproxy/logwriter.go
Normal file
@@ -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)
|
||||||
251
proxy/internal/reverseproxy/logwriter_test.go
Normal file
251
proxy/internal/reverseproxy/logwriter_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
131
proxy/internal/reverseproxy/middleware.go
Normal file
131
proxy/internal/reverseproxy/middleware.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
139
proxy/internal/reverseproxy/transport.go
Normal file
139
proxy/internal/reverseproxy/transport.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
13
proxy/main.go
Normal file
13
proxy/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
proxy/pkg/errors/common.go
Normal file
73
proxy/pkg/errors/common.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
138
proxy/pkg/errors/errors.go
Normal file
138
proxy/pkg/errors/errors.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
160
proxy/pkg/errors/errors_test.go
Normal file
160
proxy/pkg/errors/errors_test.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
1796
proxy/pkg/grpc/proto/proxy.pb.go
Normal file
1796
proxy/pkg/grpc/proto/proxy.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
159
proxy/pkg/grpc/proto/proxy.proto
Normal file
159
proxy/pkg/grpc/proto/proxy.proto
Normal file
@@ -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<string, uint64> status_code_counts = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyEvent represents events from the proxy
|
||||||
|
message ProxyEvent {
|
||||||
|
google.protobuf.Timestamp timestamp = 1;
|
||||||
|
EventType type = 2;
|
||||||
|
string message = 3;
|
||||||
|
map<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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;
|
||||||
|
}
|
||||||
137
proxy/pkg/grpc/proto/proxy_grpc.pb.go
Normal file
137
proxy/pkg/grpc/proto/proxy_grpc.pb.go
Normal file
@@ -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",
|
||||||
|
}
|
||||||
286
proxy/pkg/grpc/server.go
Normal file
286
proxy/pkg/grpc/server.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
125
proxy/pkg/proxy/config.go
Normal file
125
proxy/pkg/proxy/config.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
570
proxy/pkg/proxy/server.go
Normal file
570
proxy/pkg/proxy/server.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
56
proxy/pkg/version/version.go
Normal file
56
proxy/pkg/version/version.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
BIN
proxy/proxy
Executable file
BIN
proxy/proxy
Executable file
Binary file not shown.
31
proxy/scripts/generate-proto.sh
Executable file
31
proxy/scripts/generate-proto.sh
Executable file
@@ -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!"
|
||||||
Reference in New Issue
Block a user