Initial commit: QuiC Call server
- PoW (Hashcash) authentication - X.509 CA for phone number certificates (+0 XXX YYY ZZZ) - QUIC transport (hysteria quic-go fork) with ACME TLS - Custom append-only DB engine (from NikoGram) - Call signaling (dial/ring/accept/reject/end) - E2EE media relay (X25519 + ChaCha20-Poly1305) - Brutal congestion control (from hysteria) - Media datagram relay (Opus/VP9/H264/H265) - Graceful shutdown
This commit is contained in:
commit
fc0caf13ef
34 changed files with 4027 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
config.yaml
|
||||
data/
|
||||
acme/
|
||||
*.log
|
||||
*.out
|
||||
*.test
|
||||
*.exe
|
||||
/tmp
|
||||
659
LICENSE
Normal file
659
LICENSE
Normal file
|
|
@ -0,0 +1,659 @@
|
|||
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, may 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 be
|
||||
otherwise 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 whether 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/>.
|
||||
69
README.md
Normal file
69
README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# QuiC Call
|
||||
|
||||
A server for audio/video calls between people. A family phone system replacement.
|
||||
|
||||
No WebRTC. No STUN/TURN. Everything goes through the server.
|
||||
|
||||
## How it works
|
||||
|
||||
1. Client connects via QUIC (TLS + ACME Let's Encrypt)
|
||||
2. Server sends a PoW (Hashcash) challenge
|
||||
3. Client solves it, sends solution with their Ed25519 public key
|
||||
4. Server allocates a phone number (`+0 XXX YYY ZZZ`), issues an X.509 certificate proving ownership
|
||||
5. Client can re-roll the number once per 24h
|
||||
6. To call someone, dial their number — server relays media blindly (E2EE)
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Transport**: QUIC (hysteria fork of quic-go) over UDP
|
||||
- **TLS**: Automatic via certmagic + Cloudflare DNS-01 (ACME)
|
||||
- **Auth**: Proof-of-Work (Hashcash SHA-256), no passwords
|
||||
- **PKI**: Ed25519 CA, X.509 client certs with phone number as CN
|
||||
- **DB**: Custom append-only frame-based engine with AES-256-GCM at-rest encryption (hash-chain integrity)
|
||||
- **Signaling**: Binary varint frames over QUIC streams
|
||||
- **Media relay**: Unreliable QUIC datagrams, server forwards blindly
|
||||
- **Congestion control**: Brutal (CBR) from Hysteria
|
||||
- **E2EE**: X25519 + HKDF + ChaCha20-Poly1305 (key exchange during signaling)
|
||||
|
||||
## Status
|
||||
|
||||
Pre-alpha. Server compiles and passes vet. No client application exists yet.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o qccd ./cmd/qccd
|
||||
|
||||
# Configure
|
||||
cp config.yaml.example config.yaml
|
||||
# edit config.yaml — set ACME domains, email, Cloudflare API token
|
||||
|
||||
# Run
|
||||
./qccd server
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See `config.yaml.example`.
|
||||
|
||||
Key options:
|
||||
|
||||
| Field | Default | Description |
|
||||
|-------|---------|-------------|
|
||||
| `server.listen` | `:1963` | UDP listen address |
|
||||
| `pow.difficulty` | `22` | PoW leading zero bits |
|
||||
| `identity.cooldown` | `24h` | Minimum time between number re-rolls |
|
||||
| `identity.prefix` | `+0` | Phone number prefix |
|
||||
| `ca.key_type` | `ed25519` | CA key type |
|
||||
| `relay.bandwidth_up` | `100 mbps` | Media relay upstream bandwidth |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/apernet/quic-go` — QUIC transport (Hysteria fork)
|
||||
- `github.com/caddyserver/certmagic` + `github.com/libdns/cloudflare` — ACME TLS
|
||||
- `golang.org/x/crypto` — X25519, ChaCha20-Poly1305, HKDF
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0-only
|
||||
54
cmd/qccd/config.go
Normal file
54
cmd/qccd/config.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package qccd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./data")
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("QCC")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
viper.AutomaticEnv()
|
||||
|
||||
viper.SetDefault("server.listen", ":1963")
|
||||
viper.SetDefault("acme.type", "dns")
|
||||
viper.SetDefault("pow.difficulty", 22)
|
||||
viper.SetDefault("pow.challenge_ttl", "30s")
|
||||
viper.SetDefault("identity.cooldown", "24h")
|
||||
viper.SetDefault("identity.prefix", "+0")
|
||||
viper.SetDefault("ca.key_type", "ed25519")
|
||||
viper.SetDefault("ca.validity", "87600h")
|
||||
viper.SetDefault("relay.bandwidth_up", "100 mbps")
|
||||
viper.SetDefault("relay.bandwidth_down", "100 mbps")
|
||||
viper.SetDefault("relay.media_timeout", "30s")
|
||||
viper.SetDefault("data_dir", "./data")
|
||||
viper.SetDefault("log_level", "info")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok && cfgFile == "" {
|
||||
fmt.Fprintln(os.Stderr, "Warning: config.yaml not found, using defaults")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Error reading config file: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
dataDir := viper.GetString("data_dir")
|
||||
if err := os.MkdirAll(filepath.Join(dataDir, "acme"), 0700); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating data directory: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
29
cmd/qccd/main.go
Normal file
29
cmd/qccd/main.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package qccd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "qccd",
|
||||
Short: "QuiC Call server daemon",
|
||||
Long: "QuiC Call — сервер для аудио/видеозвонков с PoW аутентификацией и X.509 PKI. Замена телефонной связи.",
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (default ./config.yaml)")
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
}
|
||||
55
cmd/qccd/server.go
Normal file
55
cmd/qccd/server.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package qccd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/niko/qcc/internal/config"
|
||||
"github.com/niko/qcc/internal/server"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "Start QuiC Call server",
|
||||
Run: runServer,
|
||||
}
|
||||
|
||||
func runServer(cmd *cobra.Command, args []string) {
|
||||
logger, _ := zap.NewProduction()
|
||||
defer logger.Sync()
|
||||
|
||||
cfg := config.Load()
|
||||
|
||||
srv, err := server.New(cfg, logger)
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
sig := <-sigCh
|
||||
logger.Info("Received signal, shutting down", zap.String("signal", sig.String()))
|
||||
cancel()
|
||||
}()
|
||||
|
||||
logger.Info("Starting server",
|
||||
zap.String("listen", cfg.Server.Listen),
|
||||
zap.Strings("domains", cfg.ACME.Domains),
|
||||
)
|
||||
|
||||
if err := srv.Start(ctx); err != nil {
|
||||
logger.Fatal("Server error", zap.Error(err))
|
||||
}
|
||||
|
||||
fmt.Println("Server stopped gracefully")
|
||||
}
|
||||
32
config.yaml.example
Normal file
32
config.yaml.example
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
server:
|
||||
listen: ":1963"
|
||||
|
||||
acme:
|
||||
domains: ["n1kt.ru"]
|
||||
email: "admin@n1kt.ru"
|
||||
ca: "letsencrypt"
|
||||
type: "dns"
|
||||
dns:
|
||||
name: "cloudflare"
|
||||
config:
|
||||
cloudflare_api_token: "" # или через env: CF_API_TOKEN
|
||||
|
||||
pow:
|
||||
difficulty: 22
|
||||
challenge_ttl: 30s
|
||||
|
||||
identity:
|
||||
cooldown: 24h
|
||||
prefix: "+0"
|
||||
|
||||
ca:
|
||||
key_type: "ed25519"
|
||||
validity: 87600h # 10 лет
|
||||
|
||||
relay:
|
||||
bandwidth_up: "100 mbps"
|
||||
bandwidth_down: "100 mbps"
|
||||
media_timeout: 30s
|
||||
|
||||
data_dir: "./data"
|
||||
log_level: "info"
|
||||
41
go.mod
Normal file
41
go.mod
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
module github.com/niko/qcc
|
||||
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716
|
||||
github.com/caddyserver/certmagic v0.17.2
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/libdns/libdns v0.2.2 // indirect
|
||||
github.com/mholt/acmez v1.0.4 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
)
|
||||
141
go.sum
Normal file
141
go.sum
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE=
|
||||
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/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/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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
|
||||
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/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/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
|
||||
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
|
||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
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.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
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.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
|
||||
96
internal/auth/pow.go
Normal file
96
internal/auth/pow.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PoWChallenge struct {
|
||||
ServerNonce [8]byte
|
||||
Timestamp int64
|
||||
Bits int
|
||||
}
|
||||
|
||||
type PoWSolution struct {
|
||||
ClientNonce [8]byte
|
||||
ClientPubKey [32]byte
|
||||
}
|
||||
|
||||
func NewChallenge(bits int) *PoWChallenge {
|
||||
var nonce [8]byte
|
||||
binary.BigEndian.PutUint64(nonce[:], uint64(time.Now().UnixNano()>>10))
|
||||
return &PoWChallenge{
|
||||
ServerNonce: nonce,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
Bits: bits,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PoWChallenge) Verify(solution *PoWSolution, challengeTTL time.Duration) error {
|
||||
if time.Since(time.Unix(0, c.Timestamp)) > challengeTTL {
|
||||
return fmt.Errorf("challenge expired")
|
||||
}
|
||||
|
||||
hashInput := make([]byte, 32)
|
||||
binary.BigEndian.PutUint64(hashInput[0:8], uint64(c.Timestamp))
|
||||
copy(hashInput[8:16], c.ServerNonce[:])
|
||||
copy(hashInput[16:24], solution.ClientNonce[:])
|
||||
copy(hashInput[24:], solution.ClientPubKey[:])
|
||||
|
||||
hash := sha256.Sum256(hashInput)
|
||||
|
||||
leadingZeros := 0
|
||||
for i := 0; i < 32; i++ {
|
||||
if hash[i] == 0 {
|
||||
leadingZeros += 8
|
||||
} else {
|
||||
leadingZeros += bits.LeadingZeros8(hash[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if leadingZeros < c.Bits {
|
||||
return fmt.Errorf("insufficient PoW difficulty: got %d, need %d", leadingZeros, c.Bits)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *PoWChallenge) Marshal() []byte {
|
||||
buf := make([]byte, 20)
|
||||
copy(buf[0:8], c.ServerNonce[:])
|
||||
binary.BigEndian.PutUint64(buf[8:16], uint64(c.Timestamp))
|
||||
binary.BigEndian.PutUint32(buf[16:20], uint32(c.Bits))
|
||||
return buf
|
||||
}
|
||||
|
||||
func UnmarshalChallenge(data []byte) *PoWChallenge {
|
||||
if len(data) < 20 {
|
||||
return nil
|
||||
}
|
||||
c := &PoWChallenge{}
|
||||
copy(c.ServerNonce[:], data[0:8])
|
||||
c.Timestamp = int64(binary.BigEndian.Uint64(data[8:16]))
|
||||
c.Bits = int(binary.BigEndian.Uint32(data[16:20]))
|
||||
return c
|
||||
}
|
||||
|
||||
func MarshalSolution(nonce [8]byte, pubKey [32]byte) []byte {
|
||||
buf := make([]byte, 40)
|
||||
copy(buf[0:8], nonce[:])
|
||||
copy(buf[8:40], pubKey[:])
|
||||
return buf
|
||||
}
|
||||
|
||||
func UnmarshalSolution(data []byte) *PoWSolution {
|
||||
if len(data) < 40 {
|
||||
return nil
|
||||
}
|
||||
s := &PoWSolution{}
|
||||
copy(s.ClientNonce[:], data[0:8])
|
||||
copy(s.ClientPubKey[:], data[8:40])
|
||||
return s
|
||||
}
|
||||
150
internal/ca/ca.go
Normal file
150
internal/ca/ca.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CA struct {
|
||||
key crypto.Signer
|
||||
cert *x509.Certificate
|
||||
}
|
||||
|
||||
func NewOrLoad(dataDir string, keyType string, validity time.Duration) (*CA, error) {
|
||||
keyPath := filepath.Join(dataDir, "ca.key")
|
||||
certPath := filepath.Join(dataDir, "ca.crt")
|
||||
|
||||
caKey, err := loadOrGenKey(keyPath, keyType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ca key: %w", err)
|
||||
}
|
||||
|
||||
caCert, err := loadOrGenCert(certPath, caKey, validity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ca cert: %w", err)
|
||||
}
|
||||
|
||||
return &CA{key: caKey, cert: caCert}, nil
|
||||
}
|
||||
|
||||
func loadOrGenKey(path, keyType string) (crypto.Signer, error) {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid CA key PEM")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key.(crypto.Signer), nil
|
||||
}
|
||||
|
||||
var signer crypto.Signer
|
||||
switch keyType {
|
||||
case "ed25519":
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer = priv
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type: %s", keyType)
|
||||
}
|
||||
|
||||
b, err := x509.MarshalPKCS8PrivateKey(signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemData := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: b})
|
||||
if err := os.WriteFile(path, pemData, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
func loadOrGenCert(path string, key crypto.Signer, validity time.Duration) (*x509.Certificate, error) {
|
||||
if data, err := os.ReadFile(path); err == nil {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("invalid CA cert PEM")
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
pub := key.Public()
|
||||
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
now := time.Now()
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "QuiC Call Root CA",
|
||||
},
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(validity),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLenZero: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, pub, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
if err := os.WriteFile(path, pemData, 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return x509.ParseCertificate(certDER)
|
||||
}
|
||||
|
||||
func (ca *CA) IssueCert(commonName string, pub crypto.PublicKey, validity time.Duration) ([]byte, error) {
|
||||
serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
now := time.Now()
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: commonName,
|
||||
Organization: []string{"QuiC Call"},
|
||||
},
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(validity),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, ca.cert, pub, ca.key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create cert: %w", err)
|
||||
}
|
||||
|
||||
return certDER, nil
|
||||
}
|
||||
|
||||
func (ca *CA) CAKey() crypto.Signer {
|
||||
return ca.key
|
||||
}
|
||||
|
||||
func (ca *CA) CACert() *x509.Certificate {
|
||||
return ca.cert
|
||||
}
|
||||
|
||||
func (ca *CA) CACertPEM() []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.cert.Raw})
|
||||
}
|
||||
322
internal/call/signaling.go
Normal file
322
internal/call/signaling.go
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
package call
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/niko/qcc/internal/protocol"
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
type CallSession interface {
|
||||
Number() string
|
||||
PubKey() [32]byte
|
||||
Stream() protocol.FrameReadWriter
|
||||
Conn() *quic.Conn
|
||||
SetCallID(id uint64)
|
||||
CallID() uint64
|
||||
ClearCall()
|
||||
IsAuthenticated() bool
|
||||
}
|
||||
|
||||
type SessionRegistry interface {
|
||||
GetByNumber(number string) CallSession
|
||||
GetAll() []CallSession
|
||||
Register(sess CallSession, number string)
|
||||
Unregister(number string)
|
||||
}
|
||||
|
||||
type BrutalApplier func(conn *quic.Conn, bps uint64)
|
||||
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
calls map[uint64]*Call
|
||||
userCalls map[string]uint64
|
||||
nextCallID uint64
|
||||
logger *zap.Logger
|
||||
sessions SessionRegistry
|
||||
mediaTimeout time.Duration
|
||||
brutalBPS uint64
|
||||
applyBrutal BrutalApplier
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
type Call struct {
|
||||
ID uint64
|
||||
From string
|
||||
To string
|
||||
FromSess CallSession
|
||||
ToSess CallSession
|
||||
StartedAt int64
|
||||
Active bool
|
||||
FromE2EEKey [32]byte
|
||||
ToE2EEKey [32]byte
|
||||
}
|
||||
|
||||
func NewManager(logger *zap.Logger, sessions SessionRegistry, mediaTimeout time.Duration, brutalBPS uint64, applyBrutal BrutalApplier) *Manager {
|
||||
m := &Manager{
|
||||
calls: make(map[uint64]*Call),
|
||||
userCalls: make(map[string]uint64),
|
||||
nextCallID: 1,
|
||||
logger: logger,
|
||||
sessions: sessions,
|
||||
mediaTimeout: mediaTimeout,
|
||||
brutalBPS: brutalBPS,
|
||||
applyBrutal: applyBrutal,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
go m.cleanupLoop()
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) cleanupLoop() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.cleanupStale()
|
||||
case <-m.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Stop() {
|
||||
close(m.stopCh)
|
||||
}
|
||||
|
||||
func (m *Manager) cleanupStale() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for id, call := range m.calls {
|
||||
if !call.Active && time.Since(time.Unix(0, call.StartedAt)) > m.mediaTimeout {
|
||||
delete(m.calls, id)
|
||||
delete(m.userCalls, call.From)
|
||||
delete(m.userCalls, call.To)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseDialPayload(payload []byte) (targetNumber string, e2eePubKey [32]byte) {
|
||||
if len(payload) < 1 {
|
||||
return "", [32]byte{}
|
||||
}
|
||||
numLen := int(payload[0])
|
||||
if len(payload) < 1+numLen+32 {
|
||||
return "", [32]byte{}
|
||||
}
|
||||
targetNumber = string(payload[1 : 1+numLen])
|
||||
copy(e2eePubKey[:], payload[1+numLen:1+numLen+32])
|
||||
return targetNumber, e2eePubKey
|
||||
}
|
||||
|
||||
func marshalRingPayload(number string, e2eePubKey [32]byte) []byte {
|
||||
buf := make([]byte, 1+len(number)+32)
|
||||
buf[0] = byte(len(number))
|
||||
copy(buf[1:], number)
|
||||
copy(buf[1+len(number):], e2eePubKey[:])
|
||||
return buf
|
||||
}
|
||||
|
||||
func (m *Manager) HandleDial(sess CallSession, payload []byte) {
|
||||
targetNumber, callerE2EE := parseDialPayload(payload)
|
||||
if targetNumber == "" {
|
||||
sendError(sess, types.ErrInvalidRequest, "empty target number")
|
||||
return
|
||||
}
|
||||
|
||||
fromNumber := sess.Number()
|
||||
if fromNumber == targetNumber {
|
||||
sendError(sess, types.ErrSelfCall, "cannot call yourself")
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
if _, busy := m.userCalls[fromNumber]; busy {
|
||||
m.mu.Unlock()
|
||||
sendError(sess, types.ErrBusy, "you already have an active call")
|
||||
return
|
||||
}
|
||||
|
||||
targetSess := m.sessions.GetByNumber(targetNumber)
|
||||
if targetSess == nil {
|
||||
m.mu.Unlock()
|
||||
sendError(sess, types.ErrNumberNotFound, "target offline")
|
||||
return
|
||||
}
|
||||
|
||||
if _, busy := m.userCalls[targetNumber]; busy {
|
||||
m.mu.Unlock()
|
||||
sendError(sess, types.ErrBusy, "target is busy")
|
||||
return
|
||||
}
|
||||
|
||||
callID := m.nextCallID
|
||||
m.nextCallID++
|
||||
call := &Call{
|
||||
ID: callID,
|
||||
From: fromNumber,
|
||||
To: targetNumber,
|
||||
FromSess: sess,
|
||||
ToSess: targetSess,
|
||||
StartedAt: time.Now().UnixNano(),
|
||||
FromE2EEKey: callerE2EE,
|
||||
}
|
||||
m.calls[callID] = call
|
||||
m.userCalls[fromNumber] = callID
|
||||
m.userCalls[targetNumber] = callID
|
||||
m.mu.Unlock()
|
||||
|
||||
sess.SetCallID(callID)
|
||||
targetSess.SetCallID(callID)
|
||||
|
||||
ringPayload := marshalRingPayload(fromNumber, callerE2EE)
|
||||
if st := targetSess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpRing), ringPayload)
|
||||
}
|
||||
|
||||
m.logger.Info("Call initiated",
|
||||
zap.Uint64("call_id", callID),
|
||||
zap.String("from", fromNumber),
|
||||
zap.String("to", targetNumber))
|
||||
}
|
||||
|
||||
func (m *Manager) HandleAccept(sess CallSession, payload []byte) {
|
||||
callID := sess.CallID()
|
||||
if callID == 0 {
|
||||
sendError(sess, types.ErrCallNotFound, "no active call")
|
||||
return
|
||||
}
|
||||
|
||||
var calleeE2EE [32]byte
|
||||
if len(payload) >= 32 {
|
||||
copy(calleeE2EE[:], payload[:32])
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
call, exists := m.calls[callID]
|
||||
if !exists {
|
||||
m.mu.Unlock()
|
||||
sendError(sess, types.ErrCallNotFound, "call not found")
|
||||
return
|
||||
}
|
||||
call.ToE2EEKey = calleeE2EE
|
||||
call.Active = true
|
||||
m.mu.Unlock()
|
||||
|
||||
if m.brutalBPS > 0 && m.applyBrutal != nil {
|
||||
if c := call.FromSess.Conn(); c != nil {
|
||||
m.applyBrutal(c, m.brutalBPS)
|
||||
}
|
||||
if c := call.ToSess.Conn(); c != nil {
|
||||
m.applyBrutal(c, m.brutalBPS)
|
||||
}
|
||||
}
|
||||
|
||||
peerPayload := make([]byte, 32)
|
||||
copy(peerPayload, calleeE2EE[:])
|
||||
|
||||
if st := call.FromSess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpPeerAccept), peerPayload)
|
||||
}
|
||||
|
||||
m.logger.Info("Call accepted",
|
||||
zap.Uint64("call_id", callID),
|
||||
zap.Bool("e2ee", calleeE2EE != [32]byte{}))
|
||||
}
|
||||
|
||||
func (m *Manager) HandleReject(sess CallSession, _ []byte) {
|
||||
m.endCall(sess, false)
|
||||
}
|
||||
|
||||
func (m *Manager) HandleEnd(sess CallSession, _ []byte) {
|
||||
m.endCall(sess, true)
|
||||
}
|
||||
|
||||
func (m *Manager) endCall(sess CallSession, notify bool) {
|
||||
callID := sess.CallID()
|
||||
if callID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
call, exists := m.calls[callID]
|
||||
if !exists {
|
||||
m.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(m.calls, callID)
|
||||
delete(m.userCalls, call.From)
|
||||
delete(m.userCalls, call.To)
|
||||
m.mu.Unlock()
|
||||
|
||||
sess.ClearCall()
|
||||
|
||||
var otherSess CallSession
|
||||
if sess == call.FromSess {
|
||||
otherSess = call.ToSess
|
||||
} else {
|
||||
otherSess = call.FromSess
|
||||
}
|
||||
|
||||
if otherSess != nil {
|
||||
otherSess.ClearCall()
|
||||
if notify {
|
||||
if st := otherSess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpPeerEnd), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Info("Call ended",
|
||||
zap.Uint64("call_id", callID))
|
||||
}
|
||||
|
||||
func (m *Manager) GetCall(callID uint64) *Call {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.calls[callID]
|
||||
}
|
||||
|
||||
func (m *Manager) RelayMedia(sender *quic.Conn, data []byte) {
|
||||
pkt, ok := protocol.UnmarshalMediaPacket(data)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
call, exists := m.calls[pkt.CallID]
|
||||
m.mu.RUnlock()
|
||||
if !exists || !call.Active {
|
||||
return
|
||||
}
|
||||
|
||||
var target *quic.Conn
|
||||
if sender == call.FromSess.Conn() {
|
||||
target = call.ToSess.Conn()
|
||||
} else if sender == call.ToSess.Conn() {
|
||||
target = call.FromSess.Conn()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if err := target.SendDatagram(data); err != nil {
|
||||
m.logger.Warn("Failed to relay media datagram",
|
||||
zap.Uint64("call_id", pkt.CallID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func sendError(sess CallSession, code types.ErrorCode, msg string) {
|
||||
payload := make([]byte, 2+len(msg))
|
||||
binary.BigEndian.PutUint16(payload[0:2], uint16(code))
|
||||
copy(payload[2:], msg)
|
||||
if st := sess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpError), payload)
|
||||
}
|
||||
}
|
||||
64
internal/config/config.go
Normal file
64
internal/config/config.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
ACME ACMEConfig `mapstructure:"acme"`
|
||||
PoW PoWConfig `mapstructure:"pow"`
|
||||
Identity IdentityConfig `mapstructure:"identity"`
|
||||
CA CAConfig `mapstructure:"ca"`
|
||||
Relay RelayConfig `mapstructure:"relay"`
|
||||
DataDir string `mapstructure:"data_dir"`
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Listen string `mapstructure:"listen"`
|
||||
}
|
||||
|
||||
type ACMEConfig struct {
|
||||
Domains []string `mapstructure:"domains"`
|
||||
Email string `mapstructure:"email"`
|
||||
CA string `mapstructure:"ca"`
|
||||
Type string `mapstructure:"type"`
|
||||
DNS ACMEDNSConfig `mapstructure:"dns"`
|
||||
}
|
||||
|
||||
type ACMEDNSConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Config map[string]string `mapstructure:"config"`
|
||||
}
|
||||
|
||||
type PoWConfig struct {
|
||||
Difficulty int `mapstructure:"difficulty"`
|
||||
ChallengeTTL time.Duration `mapstructure:"challenge_ttl"`
|
||||
}
|
||||
|
||||
type IdentityConfig struct {
|
||||
Cooldown time.Duration `mapstructure:"cooldown"`
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
}
|
||||
|
||||
type CAConfig struct {
|
||||
KeyType string `mapstructure:"key_type"`
|
||||
Validity time.Duration `mapstructure:"validity"`
|
||||
}
|
||||
|
||||
type RelayConfig struct {
|
||||
BandwidthUp string `mapstructure:"bandwidth_up"`
|
||||
BandwidthDown string `mapstructure:"bandwidth_down"`
|
||||
MediaTimeout time.Duration `mapstructure:"media_timeout"`
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
cfg := &Config{}
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
144
internal/congestion/brutal/brutal.go
Normal file
144
internal/congestion/brutal/brutal.go
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
package brutal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/congestion"
|
||||
"github.com/apernet/quic-go/monotime"
|
||||
|
||||
"github.com/niko/qcc/internal/congestion/common"
|
||||
)
|
||||
|
||||
const (
|
||||
pktInfoSlotCount = 5
|
||||
minSampleCount = 50
|
||||
minAckRate = 0.8
|
||||
congestionWindowMultiplier = 2
|
||||
)
|
||||
|
||||
var _ congestion.CongestionControl = &BrutalSender{}
|
||||
|
||||
type BrutalSender struct {
|
||||
rttStats congestion.RTTStatsProvider
|
||||
bps congestion.ByteCount
|
||||
maxDatagramSize congestion.ByteCount
|
||||
pacer *common.Pacer
|
||||
|
||||
pktInfoSlots [pktInfoSlotCount]pktInfo
|
||||
ackRate float64
|
||||
}
|
||||
|
||||
type pktInfo struct {
|
||||
Timestamp int64
|
||||
AckCount uint64
|
||||
LossCount uint64
|
||||
}
|
||||
|
||||
func NewBrutalSender(bps uint64) *BrutalSender {
|
||||
bs := &BrutalSender{
|
||||
bps: congestion.ByteCount(bps),
|
||||
maxDatagramSize: congestion.InitialPacketSize,
|
||||
ackRate: 1,
|
||||
}
|
||||
bs.pacer = common.NewPacer(func() congestion.ByteCount {
|
||||
return congestion.ByteCount(float64(bs.bps) / bs.ackRate)
|
||||
})
|
||||
return bs
|
||||
}
|
||||
|
||||
func (b *BrutalSender) SetRTTStatsProvider(rttStats congestion.RTTStatsProvider) {
|
||||
b.rttStats = rttStats
|
||||
}
|
||||
|
||||
func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) monotime.Time {
|
||||
return b.pacer.TimeUntilSend()
|
||||
}
|
||||
|
||||
func (b *BrutalSender) HasPacingBudget(now monotime.Time) bool {
|
||||
return b.pacer.Budget(now) >= b.maxDatagramSize
|
||||
}
|
||||
|
||||
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {
|
||||
return bytesInFlight <= b.GetCongestionWindow()
|
||||
}
|
||||
|
||||
func (b *BrutalSender) GetCongestionWindow() congestion.ByteCount {
|
||||
rtt := b.rttStats.SmoothedRTT()
|
||||
if rtt <= 0 {
|
||||
return 10240
|
||||
}
|
||||
cwnd := congestion.ByteCount(float64(b.bps) * rtt.Seconds() * congestionWindowMultiplier / b.ackRate)
|
||||
if cwnd < b.maxDatagramSize {
|
||||
cwnd = b.maxDatagramSize
|
||||
}
|
||||
return cwnd
|
||||
}
|
||||
|
||||
func (b *BrutalSender) OnPacketSent(sentTime monotime.Time, bytesInFlight congestion.ByteCount,
|
||||
packetNumber congestion.PacketNumber, bytes congestion.ByteCount, isRetransmittable bool,
|
||||
) {
|
||||
b.pacer.SentPacket(sentTime, bytes)
|
||||
}
|
||||
|
||||
func (b *BrutalSender) OnPacketAcked(number congestion.PacketNumber, ackedBytes congestion.ByteCount,
|
||||
priorInFlight congestion.ByteCount, eventTime monotime.Time,
|
||||
) {
|
||||
}
|
||||
|
||||
func (b *BrutalSender) OnCongestionEvent(number congestion.PacketNumber, lostBytes congestion.ByteCount,
|
||||
priorInFlight congestion.ByteCount,
|
||||
) {
|
||||
}
|
||||
|
||||
func (b *BrutalSender) OnCongestionEventEx(priorInFlight congestion.ByteCount, eventTime monotime.Time, ackedPackets []congestion.AckedPacketInfo, lostPackets []congestion.LostPacketInfo) {
|
||||
currentTimestamp := int64(time.Duration(eventTime) / time.Second)
|
||||
slot := currentTimestamp % pktInfoSlotCount
|
||||
if b.pktInfoSlots[slot].Timestamp == currentTimestamp {
|
||||
b.pktInfoSlots[slot].LossCount += uint64(len(lostPackets))
|
||||
b.pktInfoSlots[slot].AckCount += uint64(len(ackedPackets))
|
||||
} else {
|
||||
b.pktInfoSlots[slot].Timestamp = currentTimestamp
|
||||
b.pktInfoSlots[slot].AckCount = uint64(len(ackedPackets))
|
||||
b.pktInfoSlots[slot].LossCount = uint64(len(lostPackets))
|
||||
}
|
||||
b.updateAckRate(currentTimestamp)
|
||||
}
|
||||
|
||||
func (b *BrutalSender) SetMaxDatagramSize(size congestion.ByteCount) {
|
||||
b.maxDatagramSize = size
|
||||
b.pacer.SetMaxDatagramSize(size)
|
||||
}
|
||||
|
||||
func (b *BrutalSender) updateAckRate(currentTimestamp int64) {
|
||||
minTimestamp := currentTimestamp - pktInfoSlotCount
|
||||
var ackCount, lossCount uint64
|
||||
for _, info := range b.pktInfoSlots {
|
||||
if info.Timestamp < minTimestamp {
|
||||
continue
|
||||
}
|
||||
ackCount += info.AckCount
|
||||
lossCount += info.LossCount
|
||||
}
|
||||
if ackCount+lossCount < minSampleCount {
|
||||
b.ackRate = 1
|
||||
return
|
||||
}
|
||||
rate := float64(ackCount) / float64(ackCount+lossCount)
|
||||
if rate < minAckRate {
|
||||
b.ackRate = minAckRate
|
||||
return
|
||||
}
|
||||
b.ackRate = rate
|
||||
}
|
||||
|
||||
func (b *BrutalSender) InSlowStart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *BrutalSender) InRecovery() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *BrutalSender) MaybeExitSlowStart() {}
|
||||
|
||||
func (b *BrutalSender) OnRetransmissionTimeout(packetsRetransmitted bool) {}
|
||||
74
internal/congestion/common/pacer.go
Normal file
74
internal/congestion/common/pacer.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/apernet/quic-go/congestion"
|
||||
"github.com/apernet/quic-go/monotime"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBurstPackets = 10
|
||||
maxBurstPacingDelayMultiplier = 4
|
||||
)
|
||||
|
||||
type Pacer struct {
|
||||
budgetAtLastSent congestion.ByteCount
|
||||
maxDatagramSize congestion.ByteCount
|
||||
lastSentTime monotime.Time
|
||||
getBandwidth func() congestion.ByteCount
|
||||
}
|
||||
|
||||
func NewPacer(getBandwidth func() congestion.ByteCount) *Pacer {
|
||||
p := &Pacer{
|
||||
budgetAtLastSent: maxBurstPackets * congestion.InitialPacketSize,
|
||||
maxDatagramSize: congestion.InitialPacketSize,
|
||||
getBandwidth: getBandwidth,
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Pacer) SentPacket(sendTime monotime.Time, size congestion.ByteCount) {
|
||||
budget := p.Budget(sendTime)
|
||||
if size > budget {
|
||||
p.budgetAtLastSent = 0
|
||||
} else {
|
||||
p.budgetAtLastSent = budget - size
|
||||
}
|
||||
p.lastSentTime = sendTime
|
||||
}
|
||||
|
||||
func (p *Pacer) Budget(now monotime.Time) congestion.ByteCount {
|
||||
if p.lastSentTime.IsZero() {
|
||||
return p.maxBurstSize()
|
||||
}
|
||||
budget := p.budgetAtLastSent + (p.getBandwidth()*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
|
||||
if budget < 0 {
|
||||
budget = congestion.ByteCount(1<<62 - 1)
|
||||
}
|
||||
return min(p.maxBurstSize(), budget)
|
||||
}
|
||||
|
||||
func (p *Pacer) maxBurstSize() congestion.ByteCount {
|
||||
return max(
|
||||
congestion.ByteCount((maxBurstPacingDelayMultiplier*congestion.MinPacingDelay).Nanoseconds())*p.getBandwidth()/1e9,
|
||||
maxBurstPackets*p.maxDatagramSize,
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Pacer) TimeUntilSend() monotime.Time {
|
||||
if p.budgetAtLastSent >= p.maxDatagramSize {
|
||||
return 0
|
||||
}
|
||||
diff := 1e9 * uint64(p.maxDatagramSize-p.budgetAtLastSent)
|
||||
bw := uint64(p.getBandwidth())
|
||||
d := diff / bw
|
||||
if diff%bw > 0 {
|
||||
d++
|
||||
}
|
||||
return p.lastSentTime.Add(max(congestion.MinPacingDelay, time.Duration(d)*time.Nanosecond))
|
||||
}
|
||||
|
||||
func (p *Pacer) SetMaxDatagramSize(s congestion.ByteCount) {
|
||||
p.maxDatagramSize = s
|
||||
}
|
||||
43
internal/crypto/atrest.go
Normal file
43
internal/crypto/atrest.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
)
|
||||
|
||||
func EncryptRecord(plaintext []byte, key [32]byte) (nonce [12]byte, ciphertext []byte, authTag []byte, err error) {
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
_, err = io.ReadFull(rand.Reader, nonce[:])
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
sealed := aead.Seal(nil, nonce[:], plaintext, nil)
|
||||
tagStart := len(sealed) - aead.Overhead()
|
||||
ciphertext = sealed[:tagStart]
|
||||
authTag = sealed[tagStart:]
|
||||
return nonce, ciphertext, authTag, nil
|
||||
}
|
||||
|
||||
func DecryptRecord(ciphertext []byte, nonce [12]byte, authTag []byte, key [32]byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sealed := make([]byte, len(ciphertext)+len(authTag))
|
||||
copy(sealed, ciphertext)
|
||||
copy(sealed[len(ciphertext):], authTag)
|
||||
return aead.Open(nil, nonce[:], sealed, nil)
|
||||
}
|
||||
74
internal/crypto/e2ee.go
Normal file
74
internal/crypto/e2ee.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
type E2EEKeypair struct {
|
||||
PrivateKey [32]byte
|
||||
PublicKey [32]byte
|
||||
}
|
||||
|
||||
func GenerateE2EEKeypair() (*E2EEKeypair, error) {
|
||||
priv := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, priv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Clamp for X25519
|
||||
priv[0] &= 248
|
||||
priv[31] &= 127
|
||||
priv[31] |= 64
|
||||
|
||||
pub, err := curve25519.X25519(priv, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privArr [32]byte
|
||||
var pubArr [32]byte
|
||||
copy(privArr[:], priv)
|
||||
copy(pubArr[:], pub)
|
||||
return &E2EEKeypair{PrivateKey: privArr, PublicKey: pubArr}, nil
|
||||
}
|
||||
|
||||
func DeriveE2EEKey(privateKey, publicKey [32]byte) []byte {
|
||||
shared, _ := curve25519.X25519(privateKey[:], publicKey[:])
|
||||
|
||||
salt := make([]byte, 32)
|
||||
info := []byte("qcc-e2ee-media-key")
|
||||
hkdf := hkdf.New(sha256.New, shared, salt, info)
|
||||
key := make([]byte, 32)
|
||||
if _, err := io.ReadFull(hkdf, key); err != nil {
|
||||
return nil
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func EncryptE2EE(key []byte, plaintext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, chacha20poly1305.NonceSizeX)
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aead.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
func DecryptE2EE(key []byte, ciphertext []byte) ([]byte, error) {
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) < chacha20poly1305.NonceSizeX {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
nonce := ciphertext[:chacha20poly1305.NonceSizeX]
|
||||
return aead.Open(nil, nonce, ciphertext[chacha20poly1305.NonceSizeX:], nil)
|
||||
}
|
||||
242
internal/identity/manager.go
Normal file
242
internal/identity/manager.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/niko/qcc/internal/store"
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
Number string
|
||||
PubKey [32]byte
|
||||
CertDER []byte
|
||||
CreatedAt int64
|
||||
LastReroll int64
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
store *store.Table
|
||||
masterKey [32]byte
|
||||
logger *zap.Logger
|
||||
prefix string
|
||||
cooldown time.Duration
|
||||
}
|
||||
|
||||
func NewManager(engine *store.Engine, masterKey [32]byte, prefix string, cooldown time.Duration, logger *zap.Logger) (*Manager, error) {
|
||||
t, err := engine.GetTable("identities")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get identities table: %w", err)
|
||||
}
|
||||
store.SetMasterKey(masterKey)
|
||||
return &Manager{
|
||||
store: t,
|
||||
masterKey: masterKey,
|
||||
logger: logger,
|
||||
prefix: prefix,
|
||||
cooldown: cooldown,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Allocate(pubKey [32]byte) (*Identity, error) {
|
||||
var number string
|
||||
for attempts := 0; attempts < 100; attempts++ {
|
||||
number = GenerateNumber(m.prefix)
|
||||
keyHash := numberKeyHash(number)
|
||||
if !m.store.Has(keyHash) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
keyHash := numberKeyHash(number)
|
||||
if m.store.Has(keyHash) {
|
||||
return nil, fmt.Errorf("failed to generate unique number")
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
ident := &Identity{
|
||||
Number: number,
|
||||
PubKey: pubKey,
|
||||
CreatedAt: now,
|
||||
LastReroll: now,
|
||||
}
|
||||
|
||||
payload := m.marshalIdentity(ident)
|
||||
if err := m.store.Insert(keyHash, types.RecIdentity, 0, payload, m.masterKey); err != nil {
|
||||
return nil, fmt.Errorf("store identity: %w", err)
|
||||
}
|
||||
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Reroll(pubKey [32]byte) (*Identity, error) {
|
||||
var foundIdent *Identity
|
||||
var foundHash uint64
|
||||
|
||||
err := m.store.Iterate(func(keyHash uint64, payload []byte) bool {
|
||||
ident := m.unmarshalIdentity(payload)
|
||||
if ident != nil && ident.PubKey == pubKey {
|
||||
foundIdent = ident
|
||||
foundHash = keyHash
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, m.masterKey)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if foundIdent == nil {
|
||||
return nil, fmt.Errorf("identity not found for pubkey")
|
||||
}
|
||||
|
||||
elapsed := time.Since(time.Unix(0, foundIdent.LastReroll))
|
||||
if elapsed < m.cooldown {
|
||||
retryAfter := m.cooldown - elapsed
|
||||
return nil, &CooldownError{RetryAfter: retryAfter}
|
||||
}
|
||||
|
||||
m.store.Delete(foundHash)
|
||||
|
||||
number := GenerateNumber(m.prefix)
|
||||
keyHash := numberKeyHash(number)
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
newIdent := &Identity{
|
||||
Number: number,
|
||||
PubKey: pubKey,
|
||||
CreatedAt: foundIdent.CreatedAt,
|
||||
LastReroll: now,
|
||||
}
|
||||
|
||||
payload := m.marshalIdentity(newIdent)
|
||||
if err := m.store.Insert(keyHash, types.RecIdentity, 0, payload, m.masterKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newIdent, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetByNumber(number string) (*Identity, error) {
|
||||
keyHash := numberKeyHash(number)
|
||||
payload, err := m.store.Get(keyHash, m.masterKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("identity not found: %s", number)
|
||||
}
|
||||
ident := m.unmarshalIdentity(payload)
|
||||
if ident == nil {
|
||||
return nil, fmt.Errorf("invalid identity data")
|
||||
}
|
||||
return ident, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Exists(number string) bool {
|
||||
return m.store.Has(numberKeyHash(number))
|
||||
}
|
||||
|
||||
func numberKeyHash(number string) uint64 {
|
||||
h, _ := blake2b.New(8, nil)
|
||||
h.Write([]byte(number))
|
||||
return binary.BigEndian.Uint64(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (m *Manager) marshalIdentity(ident *Identity) []byte {
|
||||
buf := make([]byte, 8+32+2+len(ident.CertDER)+8+8)
|
||||
off := 0
|
||||
PutString(buf, &off, ident.Number)
|
||||
copy(buf[off:off+32], ident.PubKey[:])
|
||||
off += 32
|
||||
PutBytes(buf, &off, ident.CertDER)
|
||||
binary.BigEndian.PutUint64(buf[off:off+8], uint64(ident.CreatedAt))
|
||||
off += 8
|
||||
binary.BigEndian.PutUint64(buf[off:off+8], uint64(ident.LastReroll))
|
||||
return buf
|
||||
}
|
||||
|
||||
func (m *Manager) unmarshalIdentity(data []byte) *Identity {
|
||||
if len(data) < 50 {
|
||||
return nil
|
||||
}
|
||||
ident := &Identity{}
|
||||
off := 0
|
||||
number, ok := GetString(data, &off)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ident.Number = number
|
||||
if off+32 > len(data) {
|
||||
return nil
|
||||
}
|
||||
copy(ident.PubKey[:], data[off:off+32])
|
||||
off += 32
|
||||
certDER, ok := GetBytes(data, &off)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
ident.CertDER = certDER
|
||||
if off+16 > len(data) {
|
||||
return nil
|
||||
}
|
||||
ident.CreatedAt = int64(binary.BigEndian.Uint64(data[off:]))
|
||||
off += 8
|
||||
ident.LastReroll = int64(binary.BigEndian.Uint64(data[off:]))
|
||||
return ident
|
||||
}
|
||||
|
||||
type CooldownError struct {
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
func (e *CooldownError) Error() string {
|
||||
return fmt.Sprintf("cooldown: retry after %s", e.RetryAfter)
|
||||
}
|
||||
|
||||
func PutString(buf []byte, off *int, s string) {
|
||||
l := len(s)
|
||||
binary.BigEndian.PutUint16(buf[*off:*off+2], uint16(l))
|
||||
*off += 2
|
||||
copy(buf[*off:*off+l], s)
|
||||
*off += l
|
||||
}
|
||||
|
||||
func GetString(data []byte, off *int) (string, bool) {
|
||||
if *off+2 > len(data) {
|
||||
return "", false
|
||||
}
|
||||
l := int(binary.BigEndian.Uint16(data[*off:]))
|
||||
*off += 2
|
||||
if *off+l > len(data) {
|
||||
return "", false
|
||||
}
|
||||
s := string(data[*off : *off+l])
|
||||
*off += l
|
||||
return s, true
|
||||
}
|
||||
|
||||
func PutBytes(buf []byte, off *int, b []byte) {
|
||||
l := len(b)
|
||||
binary.BigEndian.PutUint16(buf[*off:*off+2], uint16(l))
|
||||
*off += 2
|
||||
copy(buf[*off:*off+l], b)
|
||||
*off += l
|
||||
}
|
||||
|
||||
func GetBytes(data []byte, off *int) ([]byte, bool) {
|
||||
if *off+2 > len(data) {
|
||||
return nil, false
|
||||
}
|
||||
l := int(binary.BigEndian.Uint16(data[*off:]))
|
||||
*off += 2
|
||||
if *off+l > len(data) {
|
||||
return nil, false
|
||||
}
|
||||
b := make([]byte, l)
|
||||
copy(b, data[*off:*off+l])
|
||||
*off += l
|
||||
return b, true
|
||||
}
|
||||
40
internal/identity/number.go
Normal file
40
internal/identity/number.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package identity
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GenerateNumber(prefix string) string {
|
||||
n1 := randInt(100, 999)
|
||||
n2 := randInt(100, 999)
|
||||
n3 := randInt(1000, 9999)
|
||||
return fmt.Sprintf("%s %d %d %d", prefix, n1, n2, n3)
|
||||
}
|
||||
|
||||
func randInt(min, max int) int {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(max-min+1)))
|
||||
return min + int(n.Int64())
|
||||
}
|
||||
|
||||
func NormalizeNumber(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) == 4 && strings.HasPrefix(parts[0], "+") {
|
||||
return fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], parts[3])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func ValidateNumber(s string) bool {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) != 4 {
|
||||
return false
|
||||
}
|
||||
if len(parts[0]) < 2 || parts[0][0] != '+' {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
119
internal/protocol/frame.go
Normal file
119
internal/protocol/frame.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type FrameReadWriter interface {
|
||||
ReadFrame() (*Frame, error)
|
||||
WriteFrame(opCode byte, payload []byte) error
|
||||
}
|
||||
|
||||
const maxFramePayloadSize = 1 << 24
|
||||
|
||||
type Frame struct {
|
||||
OpCode byte
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func EncodeFrame(opCode byte, payload []byte) []byte {
|
||||
payloadLen := uint64(len(payload))
|
||||
buf := make([]byte, 1+varintLen(payloadLen)+len(payload))
|
||||
buf[0] = opCode
|
||||
off := 1
|
||||
off += encodeVarint(buf[off:], uint64(payloadLen))
|
||||
copy(buf[off:], payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func ReadFrame(r io.Reader) (*Frame, error) {
|
||||
opBuf := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, opBuf); err != nil {
|
||||
return nil, fmt.Errorf("read opcode: %w", err)
|
||||
}
|
||||
opCode := opBuf[0]
|
||||
|
||||
payloadLen, err := readVarint(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read payload length: %w", err)
|
||||
}
|
||||
if payloadLen > maxFramePayloadSize {
|
||||
return nil, fmt.Errorf("payload too large: %d", payloadLen)
|
||||
}
|
||||
|
||||
payload := make([]byte, payloadLen)
|
||||
if payloadLen > 0 {
|
||||
if _, err := io.ReadFull(r, payload); err != nil {
|
||||
return nil, fmt.Errorf("read payload: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &Frame{OpCode: opCode, Payload: payload}, nil
|
||||
}
|
||||
|
||||
func WriteFrame(w io.Writer, opCode byte, payload []byte) error {
|
||||
data := EncodeFrame(opCode, payload)
|
||||
_, err := w.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeVarint(buf []byte, v uint64) int {
|
||||
i := 0
|
||||
for v >= 0x80 {
|
||||
buf[i] = byte(v) | 0x80
|
||||
v >>= 7
|
||||
i++
|
||||
}
|
||||
buf[i] = byte(v)
|
||||
return i + 1
|
||||
}
|
||||
|
||||
func varintLen(v uint64) int {
|
||||
l := 0
|
||||
for {
|
||||
l++
|
||||
if v < 0x80 {
|
||||
break
|
||||
}
|
||||
v >>= 7
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func readVarint(r io.Reader) (uint64, error) {
|
||||
var v uint64
|
||||
var shift uint
|
||||
for {
|
||||
b := make([]byte, 1)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v |= uint64(b[0]&0x7F) << shift
|
||||
if b[0]&0x80 == 0 {
|
||||
break
|
||||
}
|
||||
shift += 7
|
||||
if shift > 63 {
|
||||
return 0, fmt.Errorf("varint too long")
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func ReadUint64(buf []byte) uint64 {
|
||||
return binary.BigEndian.Uint64(buf)
|
||||
}
|
||||
|
||||
func PutUint64(buf []byte, v uint64) {
|
||||
binary.BigEndian.PutUint64(buf, v)
|
||||
}
|
||||
|
||||
func ReadUint32(buf []byte) uint32 {
|
||||
return binary.BigEndian.Uint32(buf)
|
||||
}
|
||||
|
||||
func PutUint32(buf []byte, v uint32) {
|
||||
binary.BigEndian.PutUint32(buf, v)
|
||||
}
|
||||
82
internal/protocol/media.go
Normal file
82
internal/protocol/media.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
const MediaHeaderSize = 8 + 4 + 1 + 1 + 4
|
||||
|
||||
type MediaPacket struct {
|
||||
CallID uint64
|
||||
SeqNum uint32
|
||||
Codec types.CodecType
|
||||
Flags types.MediaFlags
|
||||
Timestamp uint32
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func MarshalMediaPacket(pkt *MediaPacket) []byte {
|
||||
size := MediaHeaderSize + len(pkt.Payload)
|
||||
buf := make([]byte, size)
|
||||
off := 0
|
||||
binary.BigEndian.PutUint64(buf[off:], pkt.CallID)
|
||||
off += 8
|
||||
binary.LittleEndian.PutUint32(buf[off:], pkt.SeqNum)
|
||||
off += 4
|
||||
buf[off] = byte(pkt.Codec)
|
||||
off++
|
||||
buf[off] = byte(pkt.Flags)
|
||||
off++
|
||||
binary.BigEndian.PutUint32(buf[off:], pkt.Timestamp)
|
||||
off += 4
|
||||
copy(buf[off:], pkt.Payload)
|
||||
return buf
|
||||
}
|
||||
|
||||
func UnmarshalMediaPacket(data []byte) (*MediaPacket, bool) {
|
||||
if len(data) < MediaHeaderSize {
|
||||
return nil, false
|
||||
}
|
||||
pkt := &MediaPacket{}
|
||||
off := 0
|
||||
pkt.CallID = binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
pkt.SeqNum = binary.LittleEndian.Uint32(data[off:])
|
||||
off += 4
|
||||
pkt.Codec = types.CodecType(data[off])
|
||||
off++
|
||||
pkt.Flags = types.MediaFlags(data[off])
|
||||
off++
|
||||
pkt.Timestamp = binary.BigEndian.Uint32(data[off:])
|
||||
off += 4
|
||||
pkt.Payload = make([]byte, len(data)-off)
|
||||
copy(pkt.Payload, data[off:])
|
||||
return pkt, true
|
||||
}
|
||||
|
||||
func ReadMediaPacket(r io.Reader) (*MediaPacket, error) {
|
||||
header := make([]byte, MediaHeaderSize)
|
||||
if _, err := io.ReadFull(r, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkt := &MediaPacket{}
|
||||
off := 0
|
||||
pkt.CallID = binary.BigEndian.Uint64(header[off:])
|
||||
off += 8
|
||||
pkt.SeqNum = binary.LittleEndian.Uint32(header[off:])
|
||||
off += 4
|
||||
pkt.Codec = types.CodecType(header[off])
|
||||
off++
|
||||
pkt.Flags = types.MediaFlags(header[off])
|
||||
off++
|
||||
pkt.Timestamp = binary.BigEndian.Uint32(header[off:])
|
||||
payload, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkt.Payload = payload
|
||||
return pkt, nil
|
||||
}
|
||||
46
internal/server/bandwidth.go
Normal file
46
internal/server/bandwidth.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
_byte = 1
|
||||
kilobyte = _byte * 1000
|
||||
megabyte = kilobyte * 1000
|
||||
gigabyte = megabyte * 1000
|
||||
)
|
||||
|
||||
func parseBandwidth(s string) (uint64, error) {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
split := 0
|
||||
for i, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
split = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if split == 0 {
|
||||
return 0, errors.New("invalid bandwidth format")
|
||||
}
|
||||
v, err := strconv.ParseUint(s[:split], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
unit := strings.TrimSpace(s[split:])
|
||||
|
||||
switch unit {
|
||||
case "b", "bps":
|
||||
return v / 8, nil
|
||||
case "k", "kb", "kbps":
|
||||
return v * kilobyte / 8, nil
|
||||
case "m", "mb", "mbps":
|
||||
return v * megabyte / 8, nil
|
||||
case "g", "gb", "gbps":
|
||||
return v * gigabyte / 8, nil
|
||||
default:
|
||||
return 0, errors.New("unsupported bandwidth unit")
|
||||
}
|
||||
}
|
||||
374
internal/server/server.go
Normal file
374
internal/server/server.go
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"github.com/libdns/cloudflare"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/niko/qcc/internal/auth"
|
||||
"github.com/niko/qcc/internal/ca"
|
||||
"github.com/niko/qcc/internal/call"
|
||||
"github.com/niko/qcc/internal/congestion/brutal"
|
||||
"github.com/niko/qcc/internal/config"
|
||||
"github.com/niko/qcc/internal/identity"
|
||||
"github.com/niko/qcc/internal/protocol"
|
||||
"github.com/niko/qcc/internal/store"
|
||||
"github.com/niko/qcc/internal/transport"
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
st *store.Engine
|
||||
ca *ca.CA
|
||||
identMgr *identity.Manager
|
||||
callMgr *call.Manager
|
||||
trans *transport.Server
|
||||
|
||||
sessions map[string]*Session
|
||||
sessionsMu sync.RWMutex
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, logger *zap.Logger) (*Server, error) {
|
||||
st, err := store.NewEngine(cfg.DataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init store: %w", err)
|
||||
}
|
||||
|
||||
masterKey := make([]byte, 32)
|
||||
if _, err := rand.Read(masterKey); err != nil {
|
||||
return nil, fmt.Errorf("generate master key: %w", err)
|
||||
}
|
||||
var mk [32]byte
|
||||
copy(mk[:], masterKey)
|
||||
|
||||
ca, err := ca.NewOrLoad(cfg.DataDir, cfg.CA.KeyType, cfg.CA.Validity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init ca: %w", err)
|
||||
}
|
||||
|
||||
identMgr, err := identity.NewManager(st, mk, cfg.Identity.Prefix, cfg.Identity.Cooldown, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init identity manager: %w", err)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
st: st,
|
||||
ca: ca,
|
||||
identMgr: identMgr,
|
||||
sessions: make(map[string]*Session),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
ctx, s.cancel = context.WithCancel(ctx)
|
||||
|
||||
tlsCfg, err := s.setupTLS()
|
||||
if err != nil {
|
||||
return fmt.Errorf("tls setup: %w", err)
|
||||
}
|
||||
|
||||
brutalBPS, err := parseBandwidth(s.cfg.Relay.BandwidthUp)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to parse relay bandwidth, using default", zap.Error(err))
|
||||
brutalBPS = 10 * 1000 * 1000 / 8 // 10 mbps default
|
||||
}
|
||||
|
||||
s.callMgr = call.NewManager(
|
||||
s.logger,
|
||||
s,
|
||||
s.cfg.Relay.MediaTimeout,
|
||||
brutalBPS,
|
||||
func(conn *quic.Conn, bps uint64) {
|
||||
conn.SetCongestionControl(brutal.NewBrutalSender(bps))
|
||||
},
|
||||
)
|
||||
|
||||
s.trans, err = transport.NewServer(s.cfg.Server.Listen, tlsCfg, s.logger, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("transport: %w", err)
|
||||
}
|
||||
|
||||
err = s.trans.Start(ctx)
|
||||
s.shutdown()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) shutdown() {
|
||||
s.logger.Info("Shutting down server...")
|
||||
|
||||
if s.callMgr != nil {
|
||||
s.callMgr.Stop()
|
||||
}
|
||||
|
||||
if s.trans != nil {
|
||||
s.trans.Close()
|
||||
}
|
||||
|
||||
s.sessionsMu.Lock()
|
||||
for number, sess := range s.sessions {
|
||||
if conn := sess.Conn(); conn != nil {
|
||||
conn.CloseWithError(0, "server shutdown")
|
||||
}
|
||||
delete(s.sessions, number)
|
||||
}
|
||||
s.sessionsMu.Unlock()
|
||||
|
||||
if s.st != nil {
|
||||
s.st.Close()
|
||||
}
|
||||
|
||||
s.logger.Info("Server shutdown complete")
|
||||
}
|
||||
|
||||
func (s *Server) setupTLS() (*tls.Config, error) {
|
||||
if len(s.cfg.ACME.Domains) == 0 || s.cfg.ACME.Email == "" {
|
||||
s.logger.Warn("ACME not configured, starting without TLS (insecure)")
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
InsecureSkipVerify: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
dnsSolver := &certmagic.DNS01Solver{
|
||||
DNSProvider: &cloudflare.Provider{
|
||||
APIToken: s.cfg.ACME.DNS.Config["cloudflare_api_token"],
|
||||
},
|
||||
}
|
||||
return transport.NewTLSConfig(
|
||||
s.cfg.ACME.Domains,
|
||||
s.cfg.ACME.Email,
|
||||
s.cfg.ACME.CA,
|
||||
dnsSolver,
|
||||
s.logger,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) OnConnect(ctx context.Context, conn *quic.Conn) {
|
||||
sess := NewSession(conn)
|
||||
s.logger.Info("New connection",
|
||||
zap.String("remote", conn.RemoteAddr().String()))
|
||||
|
||||
go s.handleStreams(ctx, conn, sess)
|
||||
go s.handleDatagrams(ctx, conn, sess)
|
||||
|
||||
<-ctx.Done()
|
||||
s.Unregister(sess.Number())
|
||||
conn.CloseWithError(0, "shutdown")
|
||||
}
|
||||
|
||||
func (s *Server) handleStreams(ctx context.Context, conn *quic.Conn, sess *Session) {
|
||||
for {
|
||||
str, err := conn.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go s.handleStream(ctx, str, sess)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleDatagrams(ctx context.Context, conn *quic.Conn, sess *Session) {
|
||||
for {
|
||||
data, err := conn.ReceiveDatagram(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
if s.callMgr != nil && sess.IsAuthenticated() && sess.CallID() != 0 {
|
||||
s.callMgr.RelayMedia(conn, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleStream(ctx context.Context, stream *quic.Stream, sess *Session) {
|
||||
st := transport.NewStream(stream)
|
||||
sess.SetStream(st)
|
||||
|
||||
for {
|
||||
frame, err := st.ReadFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch types.OpCode(frame.OpCode) {
|
||||
case types.OpGetChallenge:
|
||||
s.handleGetChallenge(sess)
|
||||
case types.OpSolve:
|
||||
s.handleSolve(sess, frame.Payload)
|
||||
case types.OpReroll:
|
||||
s.handleReroll(sess)
|
||||
case types.OpDial:
|
||||
if !sess.IsAuthenticated() {
|
||||
sendOpError(st, types.ErrInvalidRequest, "not authenticated")
|
||||
continue
|
||||
}
|
||||
s.callMgr.HandleDial(sess, frame.Payload)
|
||||
case types.OpAccept:
|
||||
s.callMgr.HandleAccept(sess, frame.Payload)
|
||||
case types.OpReject:
|
||||
s.callMgr.HandleReject(sess, frame.Payload)
|
||||
case types.OpEnd:
|
||||
s.callMgr.HandleEnd(sess, frame.Payload)
|
||||
default:
|
||||
sendOpError(st, types.ErrInvalidRequest, "unknown opcode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleGetChallenge(sess *Session) {
|
||||
challenge := auth.NewChallenge(s.cfg.PoW.Difficulty)
|
||||
if st := sess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpChallenge), challenge.Marshal())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleSolve(sess *Session, payload []byte) {
|
||||
solution := auth.UnmarshalSolution(payload)
|
||||
if solution == nil {
|
||||
sendOpError(sess.Stream(), types.ErrPoWInvalid, "invalid solution format")
|
||||
return
|
||||
}
|
||||
|
||||
challenge := auth.NewChallenge(s.cfg.PoW.Difficulty)
|
||||
if err := challenge.Verify(solution, s.cfg.PoW.ChallengeTTL); err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrPoWInvalid, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var pubKey [32]byte
|
||||
copy(pubKey[:], solution.ClientPubKey[:])
|
||||
|
||||
ident, err := s.identMgr.Allocate(pubKey)
|
||||
if err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrInternal, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
certDER, err := s.ca.IssueCert(ident.Number, ed25519.PublicKey(pubKey[:]), s.cfg.CA.Validity)
|
||||
if err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrInternal, "cert issuance failed")
|
||||
return
|
||||
}
|
||||
ident.CertDER = certDER
|
||||
|
||||
sess.Authenticate(ident.Number, pubKey, certDER)
|
||||
s.Register(sess, ident.Number)
|
||||
|
||||
payload = marshalIdentityResponse(ident.Number, certDER, s.ca.CACertPEM())
|
||||
|
||||
if st := sess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpIdentity), payload)
|
||||
}
|
||||
|
||||
s.logger.Info("Client authenticated",
|
||||
zap.String("number", ident.Number))
|
||||
}
|
||||
|
||||
func (s *Server) handleReroll(sess *Session) {
|
||||
if !sess.IsAuthenticated() {
|
||||
sendOpError(sess.Stream(), types.ErrInvalidRequest, "not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
newIdent, err := s.identMgr.Reroll(sess.PubKey())
|
||||
if err != nil {
|
||||
if _, ok := err.(*identity.CooldownError); ok {
|
||||
sendOpError(sess.Stream(), types.ErrCooldown, err.Error())
|
||||
} else {
|
||||
sendOpError(sess.Stream(), types.ErrInternal, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
certDER, err := s.ca.IssueCert(newIdent.Number, ed25519.PublicKey(newIdent.PubKey[:]), s.cfg.CA.Validity)
|
||||
if err != nil {
|
||||
sendOpError(sess.Stream(), types.ErrInternal, "cert issuance failed")
|
||||
return
|
||||
}
|
||||
newIdent.CertDER = certDER
|
||||
|
||||
s.Unregister(sess.Number())
|
||||
sess.Authenticate(newIdent.Number, newIdent.PubKey, certDER)
|
||||
s.Register(sess, newIdent.Number)
|
||||
|
||||
payload := marshalIdentityResponse(newIdent.Number, certDER, s.ca.CACertPEM())
|
||||
|
||||
if st := sess.Stream(); st != nil {
|
||||
st.WriteFrame(byte(types.OpIdentity), payload)
|
||||
}
|
||||
|
||||
s.logger.Info("Number rerolled",
|
||||
zap.String("old", sess.Number()),
|
||||
zap.String("new", newIdent.Number))
|
||||
}
|
||||
|
||||
func marshalIdentityResponse(number string, certDER, caCertPEM []byte) []byte {
|
||||
payload := make([]byte, len(number)+1+2+len(certDER)+len(caCertPEM))
|
||||
off := 0
|
||||
copy(payload[off:], []byte(number))
|
||||
off += len(number)
|
||||
payload[off] = 0
|
||||
off++
|
||||
binary.BigEndian.PutUint16(payload[off:off+2], uint16(len(certDER)))
|
||||
off += 2
|
||||
copy(payload[off:], certDER)
|
||||
off += len(certDER)
|
||||
copy(payload[off:], caCertPEM)
|
||||
return payload
|
||||
}
|
||||
|
||||
func (s *Server) Register(sess call.CallSession, number string) {
|
||||
s.sessionsMu.Lock()
|
||||
defer s.sessionsMu.Unlock()
|
||||
s.sessions[number] = sess.(*Session)
|
||||
}
|
||||
|
||||
func (s *Server) Unregister(number string) {
|
||||
if number == "" {
|
||||
return
|
||||
}
|
||||
s.sessionsMu.Lock()
|
||||
defer s.sessionsMu.Unlock()
|
||||
delete(s.sessions, number)
|
||||
}
|
||||
|
||||
func (s *Server) GetByNumber(number string) call.CallSession {
|
||||
s.sessionsMu.RLock()
|
||||
defer s.sessionsMu.RUnlock()
|
||||
return s.sessions[number]
|
||||
}
|
||||
|
||||
func (s *Server) GetAll() []call.CallSession {
|
||||
s.sessionsMu.RLock()
|
||||
defer s.sessionsMu.RUnlock()
|
||||
result := make([]call.CallSession, 0, len(s.sessions))
|
||||
for _, sess := range s.sessions {
|
||||
result = append(result, sess)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func sendOpError(st protocol.FrameReadWriter, code types.ErrorCode, msg string) {
|
||||
if st == nil {
|
||||
return
|
||||
}
|
||||
payload := make([]byte, 2+len(msg))
|
||||
binary.BigEndian.PutUint16(payload[0:2], uint16(code))
|
||||
copy(payload[2:], msg)
|
||||
st.WriteFrame(byte(types.OpError), payload)
|
||||
}
|
||||
92
internal/server/session.go
Normal file
92
internal/server/session.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/niko/qcc/internal/protocol"
|
||||
"github.com/niko/qcc/internal/transport"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
mu sync.Mutex
|
||||
conn *quic.Conn
|
||||
stream *transport.Stream
|
||||
|
||||
authenticated bool
|
||||
number string
|
||||
pubKey [32]byte
|
||||
certDER []byte
|
||||
|
||||
activeCallID uint64
|
||||
}
|
||||
|
||||
func NewSession(conn *quic.Conn) *Session {
|
||||
return &Session{
|
||||
conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) SetStream(st *transport.Stream) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stream = st
|
||||
}
|
||||
|
||||
func (s *Session) Stream() protocol.FrameReadWriter {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.stream == nil {
|
||||
return nil
|
||||
}
|
||||
return s.stream
|
||||
}
|
||||
|
||||
func (s *Session) Conn() *quic.Conn {
|
||||
return s.conn
|
||||
}
|
||||
|
||||
func (s *Session) Authenticate(number string, pubKey [32]byte, certDER []byte) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.authenticated = true
|
||||
s.number = number
|
||||
s.pubKey = pubKey
|
||||
s.certDER = certDER
|
||||
}
|
||||
|
||||
func (s *Session) IsAuthenticated() bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.authenticated
|
||||
}
|
||||
|
||||
func (s *Session) Number() string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.number
|
||||
}
|
||||
|
||||
func (s *Session) PubKey() [32]byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.pubKey
|
||||
}
|
||||
|
||||
func (s *Session) SetCallID(id uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.activeCallID = id
|
||||
}
|
||||
|
||||
func (s *Session) CallID() uint64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.activeCallID
|
||||
}
|
||||
|
||||
func (s *Session) ClearCall() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.activeCallID = 0
|
||||
}
|
||||
59
internal/store/encrypt.go
Normal file
59
internal/store/encrypt.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
"github.com/niko/qcc/internal/crypto"
|
||||
)
|
||||
|
||||
var MasterKey [32]byte
|
||||
|
||||
func SetMasterKey(key [32]byte) {
|
||||
MasterKey = key
|
||||
}
|
||||
|
||||
func EncryptRecordPayload(plaintext []byte, key [32]byte) (nonce [12]byte, ciphertext []byte, authTag []byte, err error) {
|
||||
return crypto.EncryptRecord(plaintext, key)
|
||||
}
|
||||
|
||||
func DecryptRecordPayload(ciphertext []byte, nonce [12]byte, authTag []byte, key [32]byte) ([]byte, error) {
|
||||
return crypto.DecryptRecord(ciphertext, nonce, authTag, key)
|
||||
}
|
||||
|
||||
func EncryptIndexData(data []byte) (nonce [12]byte, ciphertext []byte, authTag []byte, err error) {
|
||||
block, err := aes.NewCipher(MasterKey[:])
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
_, err = io.ReadFull(rand.Reader, nonce[:])
|
||||
if err != nil {
|
||||
return nonce, nil, nil, err
|
||||
}
|
||||
sealed := aead.Seal(nil, nonce[:], data, nil)
|
||||
tagStart := len(sealed) - aead.Overhead()
|
||||
ciphertext = sealed[:tagStart]
|
||||
authTag = sealed[tagStart:]
|
||||
return nonce, ciphertext, authTag, nil
|
||||
}
|
||||
|
||||
func DecryptIndexData(ciphertext []byte, nonce [12]byte, authTag []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(MasterKey[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sealed := make([]byte, len(ciphertext)+len(authTag))
|
||||
copy(sealed, ciphertext)
|
||||
copy(sealed[len(ciphertext):], authTag)
|
||||
return aead.Open(nil, nonce[:], sealed, nil)
|
||||
}
|
||||
170
internal/store/engine.go
Normal file
170
internal/store/engine.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Engine struct {
|
||||
mu sync.RWMutex
|
||||
DataDir string
|
||||
Tables map[string]*Table
|
||||
frames map[string]*frameState
|
||||
}
|
||||
|
||||
type frameState struct {
|
||||
lastFrameID uint64
|
||||
lastHash [32]byte
|
||||
frameCount int
|
||||
}
|
||||
|
||||
func NewEngine(dataDir string) (*Engine, error) {
|
||||
dirs := []string{
|
||||
dataDir,
|
||||
filepath.Join(dataDir, "identities"),
|
||||
filepath.Join(dataDir, "calls"),
|
||||
filepath.Join(dataDir, "indexes"),
|
||||
filepath.Join(dataDir, "acme"),
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if err := os.MkdirAll(d, 0700); err != nil {
|
||||
return nil, fmt.Errorf("mkdir %s: %w", d, err)
|
||||
}
|
||||
}
|
||||
e := &Engine{
|
||||
DataDir: dataDir,
|
||||
Tables: make(map[string]*Table),
|
||||
frames: make(map[string]*frameState),
|
||||
}
|
||||
if err := e.initDB(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *Engine) initDB() error {
|
||||
path := filepath.Join(e.DataDir, "qcc.db")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
header := make([]byte, 14)
|
||||
copy(header[0:4], []byte("QCC\x00"))
|
||||
header[4] = 1
|
||||
header[5] = 0
|
||||
_, err = f.Write(header)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) GetTable(name string) (*Table, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if t, ok := e.Tables[name]; ok {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
fs, ok := e.frames[name]
|
||||
if !ok {
|
||||
fs = &frameState{}
|
||||
e.frames[name] = fs
|
||||
lastFrame, lastHash, count := e.scanFrames(name)
|
||||
fs.lastFrameID = lastFrame
|
||||
fs.lastHash = lastHash
|
||||
fs.frameCount = count
|
||||
}
|
||||
|
||||
tablePath := filepath.Join(e.DataDir, name)
|
||||
t := &Table{
|
||||
engine: e,
|
||||
name: name,
|
||||
path: tablePath,
|
||||
lastHash: fs.lastHash,
|
||||
nextID: fs.lastFrameID + 1,
|
||||
frameCount: fs.frameCount,
|
||||
index: make(map[uint64]IndexEntry),
|
||||
}
|
||||
e.Tables[name] = t
|
||||
|
||||
if err := t.loadLastIndex(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (e *Engine) scanFrames(name string) (uint64, [32]byte, int) {
|
||||
dir := filepath.Join(e.DataDir, name)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, [32]byte{}, 0
|
||||
}
|
||||
var lastID uint64
|
||||
var lastHash [32]byte
|
||||
count := 0
|
||||
var prevFrameID uint64
|
||||
var prevHash [32]byte
|
||||
for _, entry := range entries {
|
||||
if filepath.Ext(entry.Name()) != ".frm" {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(data) < 66 {
|
||||
continue
|
||||
}
|
||||
storedChecksum := data[len(data)-32:]
|
||||
computedChecksum := ComputeFrameChecksum(data[:len(data)-32])
|
||||
if string(storedChecksum) != string(computedChecksum[:]) {
|
||||
continue
|
||||
}
|
||||
f, err := UnmarshalFrame(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if count > 0 {
|
||||
if f.Header.PrevFrameID != prevFrameID ||
|
||||
string(f.Header.PrevHash[:]) != string(prevHash[:]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if f.Header.FrameID > lastID {
|
||||
lastID = f.Header.FrameID
|
||||
lastHash = computedChecksum
|
||||
}
|
||||
prevFrameID = f.Header.FrameID
|
||||
prevHash = computedChecksum
|
||||
count++
|
||||
}
|
||||
return lastID, lastHash, count
|
||||
}
|
||||
|
||||
func (e *Engine) UpdateFrameState(name string, frameID uint64, hash [32]byte) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if fs, ok := e.frames[name]; ok {
|
||||
if frameID > fs.lastFrameID {
|
||||
fs.lastFrameID = frameID
|
||||
fs.lastHash = hash
|
||||
fs.frameCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Engine) Close() error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for _, t := range e.Tables {
|
||||
if err := t.flushIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
195
internal/store/frame.go
Normal file
195
internal/store/frame.go
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
type FrameHeader struct {
|
||||
Magic [4]byte
|
||||
Version uint16
|
||||
FrameID uint64
|
||||
PrevFrameID uint64
|
||||
PrevHash [32]byte
|
||||
Timestamp int64
|
||||
NumRecords uint32
|
||||
}
|
||||
|
||||
const FrameHeaderSize = 4 + 2 + 8 + 8 + 32 + 8 + 4
|
||||
const RecordHeaderSize = 1 + 1 + 2 + 8 + 4 + 12 + 16
|
||||
|
||||
type RecordHeader struct {
|
||||
RecType types.RecType
|
||||
Flags uint8
|
||||
TableID uint16
|
||||
KeyHash uint64
|
||||
PayloadSize uint32
|
||||
Nonce [12]byte
|
||||
AuthTag [16]byte
|
||||
}
|
||||
|
||||
type Frame struct {
|
||||
Header FrameHeader
|
||||
Records []Record
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Header RecordHeader
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func NewFrameHeader(prevFrameID uint64, prevHash [32]byte) FrameHeader {
|
||||
var magic [4]byte
|
||||
copy(magic[:], []byte("QCCF"))
|
||||
return FrameHeader{
|
||||
Magic: magic,
|
||||
Version: 1,
|
||||
FrameID: prevFrameID + 1,
|
||||
PrevFrameID: prevFrameID,
|
||||
PrevHash: prevHash,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
}
|
||||
}
|
||||
|
||||
func MarshalFrame(f *Frame) ([]byte, error) {
|
||||
size := FrameHeaderSize
|
||||
for _, rec := range f.Records {
|
||||
size += RecordHeaderSize + len(rec.Payload)
|
||||
}
|
||||
padding := (8 - (size % 8)) % 8
|
||||
size += padding + 32
|
||||
|
||||
buf := make([]byte, size)
|
||||
off := 0
|
||||
|
||||
copy(buf[off:], f.Header.Magic[:])
|
||||
off += 4
|
||||
binary.BigEndian.PutUint16(buf[off:], f.Header.Version)
|
||||
off += 2
|
||||
binary.BigEndian.PutUint64(buf[off:], f.Header.FrameID)
|
||||
off += 8
|
||||
binary.BigEndian.PutUint64(buf[off:], f.Header.PrevFrameID)
|
||||
off += 8
|
||||
copy(buf[off:], f.Header.PrevHash[:])
|
||||
off += 32
|
||||
binary.BigEndian.PutUint64(buf[off:], uint64(f.Header.Timestamp))
|
||||
off += 8
|
||||
binary.BigEndian.PutUint32(buf[off:], uint32(len(f.Records)))
|
||||
off += 4
|
||||
|
||||
for _, rec := range f.Records {
|
||||
buf[off] = byte(rec.Header.RecType)
|
||||
off++
|
||||
buf[off] = rec.Header.Flags
|
||||
off++
|
||||
binary.BigEndian.PutUint16(buf[off:], rec.Header.TableID)
|
||||
off += 2
|
||||
binary.BigEndian.PutUint64(buf[off:], rec.Header.KeyHash)
|
||||
off += 8
|
||||
binary.BigEndian.PutUint32(buf[off:], rec.Header.PayloadSize)
|
||||
off += 4
|
||||
copy(buf[off:], rec.Header.Nonce[:])
|
||||
off += 12
|
||||
copy(buf[off:], rec.Header.AuthTag[:])
|
||||
off += 16
|
||||
copy(buf[off:], rec.Payload)
|
||||
off += len(rec.Payload)
|
||||
}
|
||||
|
||||
for i := 0; i < padding; i++ {
|
||||
buf[off] = 0
|
||||
off++
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(buf[:off])
|
||||
copy(buf[off:], hash[:])
|
||||
off += 32
|
||||
|
||||
return buf[:off], nil
|
||||
}
|
||||
|
||||
func UnmarshalFrame(data []byte) (*Frame, error) {
|
||||
if len(data) < FrameHeaderSize+32 {
|
||||
return nil, fmt.Errorf("frame too short: %d", len(data))
|
||||
}
|
||||
dataLen := len(data)
|
||||
checksumStart := dataLen - 32
|
||||
storedChecksum := data[checksumStart:]
|
||||
|
||||
computed := sha256.Sum256(data[:checksumStart])
|
||||
if !constantTimeEqual(storedChecksum, computed[:]) {
|
||||
return nil, fmt.Errorf("frame checksum mismatch")
|
||||
}
|
||||
|
||||
f := &Frame{}
|
||||
off := 0
|
||||
|
||||
copy(f.Header.Magic[:], data[off:off+4])
|
||||
off += 4
|
||||
if string(f.Header.Magic[:]) != "QCCF" {
|
||||
return nil, fmt.Errorf("invalid frame magic: %s", hex.EncodeToString(f.Header.Magic[:]))
|
||||
}
|
||||
f.Header.Version = binary.BigEndian.Uint16(data[off:])
|
||||
off += 2
|
||||
f.Header.FrameID = binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
f.Header.PrevFrameID = binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
copy(f.Header.PrevHash[:], data[off:off+32])
|
||||
off += 32
|
||||
f.Header.Timestamp = int64(binary.BigEndian.Uint64(data[off:]))
|
||||
off += 8
|
||||
f.Header.NumRecords = binary.BigEndian.Uint32(data[off:])
|
||||
off += 4
|
||||
|
||||
for i := uint32(0); i < f.Header.NumRecords; i++ {
|
||||
if off+RecordHeaderSize > checksumStart {
|
||||
return nil, fmt.Errorf("record header %d exceeds frame bounds", i)
|
||||
}
|
||||
var rec Record
|
||||
rec.Header.RecType = types.RecType(data[off])
|
||||
off++
|
||||
rec.Header.Flags = data[off]
|
||||
off++
|
||||
rec.Header.TableID = binary.BigEndian.Uint16(data[off:])
|
||||
off += 2
|
||||
rec.Header.KeyHash = binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
rec.Header.PayloadSize = binary.BigEndian.Uint32(data[off:])
|
||||
off += 4
|
||||
copy(rec.Header.Nonce[:], data[off:off+12])
|
||||
off += 12
|
||||
copy(rec.Header.AuthTag[:], data[off:off+16])
|
||||
off += 16
|
||||
|
||||
if off+int(rec.Header.PayloadSize) > checksumStart {
|
||||
return nil, fmt.Errorf("record payload %d exceeds frame bounds", i)
|
||||
}
|
||||
rec.Payload = make([]byte, rec.Header.PayloadSize)
|
||||
copy(rec.Payload, data[off:off+int(rec.Header.PayloadSize)])
|
||||
off += int(rec.Header.PayloadSize)
|
||||
f.Records = append(f.Records, rec)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func ComputeFrameChecksum(frameData []byte) [32]byte {
|
||||
return sha256.Sum256(frameData)
|
||||
}
|
||||
|
||||
func constantTimeEqual(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
var v byte
|
||||
for i := 0; i < len(a); i++ {
|
||||
v |= a[i] ^ b[i]
|
||||
}
|
||||
return v == 0
|
||||
}
|
||||
127
internal/store/index.go
Normal file
127
internal/store/index.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
type IndexFrame struct {
|
||||
Entries map[uint64]IndexEntry
|
||||
FrameID uint64
|
||||
}
|
||||
|
||||
func BuildIndex(table *Table) (*IndexFrame, error) {
|
||||
table.mu.RLock()
|
||||
defer table.mu.RUnlock()
|
||||
|
||||
idx := &IndexFrame{
|
||||
Entries: make(map[uint64]IndexEntry),
|
||||
FrameID: table.nextID - 1,
|
||||
}
|
||||
|
||||
for keyHash, entry := range table.index {
|
||||
idx.Entries[keyHash] = entry
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func (idx *IndexFrame) MarshalBinary() ([]byte, error) {
|
||||
size := 8 + 8 + len(idx.Entries)*24
|
||||
buf := make([]byte, size)
|
||||
off := 0
|
||||
binary.BigEndian.PutUint64(buf[off:], idx.FrameID)
|
||||
off += 8
|
||||
binary.BigEndian.PutUint64(buf[off:], uint64(len(idx.Entries)))
|
||||
off += 8
|
||||
|
||||
for keyHash, entry := range idx.Entries {
|
||||
binary.BigEndian.PutUint64(buf[off:], keyHash)
|
||||
off += 8
|
||||
binary.BigEndian.PutUint64(buf[off:], entry.FrameID)
|
||||
off += 8
|
||||
binary.BigEndian.PutUint64(buf[off:], uint64(entry.Offset))
|
||||
off += 8
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func UnmarshalIndexFrame(data []byte) (*IndexFrame, error) {
|
||||
if len(data) < 16 {
|
||||
return nil, nil
|
||||
}
|
||||
idx := &IndexFrame{
|
||||
Entries: make(map[uint64]IndexEntry),
|
||||
}
|
||||
off := 0
|
||||
idx.FrameID = binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
count := binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
|
||||
for i := uint64(0); i < count; i++ {
|
||||
if off+24 > len(data) {
|
||||
break
|
||||
}
|
||||
keyHash := binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
frameID := binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
offset := binary.BigEndian.Uint64(data[off:])
|
||||
off += 8
|
||||
idx.Entries[keyHash] = IndexEntry{FrameID: frameID, Offset: uint32(offset)}
|
||||
}
|
||||
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
func AppendIndexToTable(table *Table, idx *IndexFrame) error {
|
||||
idxData, err := idx.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyHash := uint64(0xFFFFFFFFFFFFFFFF)
|
||||
header := RecordHeader{
|
||||
RecType: types.RecIndex,
|
||||
Flags: 0,
|
||||
TableID: 0,
|
||||
KeyHash: keyHash,
|
||||
PayloadSize: uint32(len(idxData)),
|
||||
}
|
||||
|
||||
frame := &Frame{
|
||||
Header: NewFrameHeader(table.nextID-1, table.lastHash),
|
||||
Records: []Record{
|
||||
{Header: header, Payload: idxData},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := MarshalFrame(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
frameName := pathForIndexFrame(table.name, table.nextID)
|
||||
framePath := filepath.Join(table.path, frameName)
|
||||
if err := os.WriteFile(framePath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checksum := ComputeFrameChecksum(data[:len(data)-32])
|
||||
table.engine.UpdateFrameState(table.name, table.nextID, checksum)
|
||||
table.lastHash = checksum
|
||||
table.nextID++
|
||||
table.frameCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathForIndexFrame(tableName string, frameID uint64) string {
|
||||
return filepath.Join(tableName, "indexes", fmt.Sprintf("%016x.idx", frameID))
|
||||
}
|
||||
176
internal/store/table.go
Normal file
176
internal/store/table.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/niko/qcc/internal/crypto"
|
||||
"github.com/niko/qcc/pkg/types"
|
||||
)
|
||||
|
||||
type IndexEntry struct {
|
||||
FrameID uint64
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
type Table struct {
|
||||
mu sync.RWMutex
|
||||
engine *Engine
|
||||
name string
|
||||
path string
|
||||
lastHash [32]byte
|
||||
nextID uint64
|
||||
frameCount int
|
||||
index map[uint64]IndexEntry
|
||||
}
|
||||
|
||||
func (t *Table) Insert(keyHash uint64, recType types.RecType, tableID uint16, payload []byte, key [32]byte) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
nonce, ciphertext, authTag, err := crypto.EncryptRecord(payload, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var authTagArr [16]byte
|
||||
copy(authTagArr[:], authTag)
|
||||
header := RecordHeader{
|
||||
RecType: recType,
|
||||
Flags: 0x01,
|
||||
TableID: tableID,
|
||||
KeyHash: keyHash,
|
||||
PayloadSize: uint32(len(ciphertext)),
|
||||
Nonce: nonce,
|
||||
AuthTag: authTagArr,
|
||||
}
|
||||
|
||||
frame := &Frame{
|
||||
Header: NewFrameHeader(t.nextID-1, t.lastHash),
|
||||
Records: []Record{
|
||||
{Header: header, Payload: ciphertext},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := MarshalFrame(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
frameName := fmt.Sprintf("%016x.frm", t.nextID)
|
||||
framePath := filepath.Join(t.path, frameName)
|
||||
if err := os.WriteFile(framePath, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checksum := ComputeFrameChecksum(data[:len(data)-32])
|
||||
t.engine.UpdateFrameState(t.name, t.nextID, checksum)
|
||||
t.lastHash = checksum
|
||||
t.index[keyHash] = IndexEntry{FrameID: t.nextID, Offset: 0}
|
||||
t.nextID++
|
||||
t.frameCount++
|
||||
|
||||
if t.frameCount%10 == 0 {
|
||||
return t.saveIndex()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) Get(keyHash uint64, key [32]byte) ([]byte, error) {
|
||||
t.mu.RLock()
|
||||
entry, ok := t.index[keyHash]
|
||||
t.mu.RUnlock()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key not found: %x", keyHash)
|
||||
}
|
||||
|
||||
return t.readRecord(entry, key)
|
||||
}
|
||||
|
||||
func (t *Table) readRecord(entry IndexEntry, key [32]byte) ([]byte, error) {
|
||||
frameName := fmt.Sprintf("%016x.frm", entry.FrameID)
|
||||
framePath := filepath.Join(t.path, frameName)
|
||||
data, err := os.ReadFile(framePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f, err := UnmarshalFrame(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(f.Records) == 0 {
|
||||
return nil, fmt.Errorf("empty frame")
|
||||
}
|
||||
|
||||
rec := f.Records[0]
|
||||
return crypto.DecryptRecord(rec.Payload, rec.Header.Nonce, rec.Header.AuthTag[:], key)
|
||||
}
|
||||
|
||||
func (t *Table) Has(keyHash uint64) bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
_, ok := t.index[keyHash]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (t *Table) Delete(keyHash uint64) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
delete(t.index, keyHash)
|
||||
}
|
||||
|
||||
func (t *Table) loadLastIndex() error {
|
||||
indexPath := filepath.Join(t.engine.DataDir, "indexes", t.name+".idx")
|
||||
data, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
t.index = make(map[uint64]IndexEntry)
|
||||
off := 0
|
||||
for off+16 <= len(data) {
|
||||
keyHash := binary.BigEndian.Uint64(data[off:])
|
||||
frameID := binary.BigEndian.Uint64(data[off+8:])
|
||||
t.index[keyHash] = IndexEntry{FrameID: frameID, Offset: 0}
|
||||
off += 16
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Table) saveIndex() error {
|
||||
indexPath := filepath.Join(t.engine.DataDir, "indexes", t.name+".idx")
|
||||
data := make([]byte, 0, len(t.index)*16)
|
||||
for keyHash, entry := range t.index {
|
||||
var buf [16]byte
|
||||
binary.BigEndian.PutUint64(buf[0:8], keyHash)
|
||||
binary.BigEndian.PutUint64(buf[8:16], entry.FrameID)
|
||||
data = append(data, buf[:]...)
|
||||
}
|
||||
return os.WriteFile(indexPath, data, 0600)
|
||||
}
|
||||
|
||||
func (t *Table) flushIndex() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
return t.saveIndex()
|
||||
}
|
||||
|
||||
func (t *Table) Iterate(fn func(keyHash uint64, payload []byte) bool, key [32]byte) error {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
for keyHash, entry := range t.index {
|
||||
payload, err := t.readRecord(entry, key)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !fn(keyHash, payload) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
141
internal/transport/server.go
Normal file
141
internal/transport/server.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/caddyserver/certmagic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
listener *quic.Listener
|
||||
tr *quic.Transport
|
||||
logger *zap.Logger
|
||||
handler ServerHandler
|
||||
}
|
||||
|
||||
type ServerHandler interface {
|
||||
OnConnect(ctx context.Context, conn *quic.Conn)
|
||||
}
|
||||
|
||||
func NewServer(listenAddr string, tlsConfig *tls.Config, logger *zap.Logger, handler ServerHandler) (*Server, error) {
|
||||
addr, err := net.ResolveUDPAddr("udp", listenAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve addr: %w", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen udp: %w", err)
|
||||
}
|
||||
|
||||
tr := &quic.Transport{Conn: conn}
|
||||
|
||||
listener, err := tr.Listen(tlsConfig, &quic.Config{
|
||||
Allow0RTT: true,
|
||||
KeepAlivePeriod: 15,
|
||||
EnableDatagrams: true,
|
||||
MaxIncomingStreams: 1000,
|
||||
MaxIncomingUniStreams: -1,
|
||||
})
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("quic listen: %w", err)
|
||||
}
|
||||
|
||||
return &Server{
|
||||
listener: listener,
|
||||
tr: tr,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewTLSConfig(domains []string, email, caType string, dnsSolver *certmagic.DNS01Solver, logger *zap.Logger) (*tls.Config, error) {
|
||||
dataDir := "data/acme"
|
||||
cmCfg := &certmagic.Config{
|
||||
RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio,
|
||||
KeySource: certmagic.DefaultKeyGenerator,
|
||||
Storage: &certmagic.FileStorage{Path: dataDir},
|
||||
Logger: logger,
|
||||
}
|
||||
|
||||
cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{
|
||||
Email: email,
|
||||
Agreed: true,
|
||||
Logger: logger,
|
||||
})
|
||||
|
||||
switch caType {
|
||||
case "letsencrypt", "le", "":
|
||||
cmIssuer.CA = certmagic.LetsEncryptProductionCA
|
||||
case "zerossl", "zero":
|
||||
cmIssuer.CA = certmagic.ZeroSSLProductionCA
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported CA: %s", caType)
|
||||
}
|
||||
|
||||
if dnsSolver != nil {
|
||||
cmIssuer.DisableHTTPChallenge = true
|
||||
cmIssuer.DisableTLSALPNChallenge = true
|
||||
cmIssuer.DNS01Solver = dnsSolver
|
||||
} else {
|
||||
cmIssuer.DisableHTTPChallenge = false
|
||||
cmIssuer.DisableTLSALPNChallenge = true
|
||||
}
|
||||
|
||||
cmCfg.Issuers = []certmagic.Issuer{cmIssuer}
|
||||
cmCache := certmagic.NewCache(certmagic.CacheOptions{
|
||||
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
||||
return cmCfg, nil
|
||||
},
|
||||
Logger: logger,
|
||||
})
|
||||
cmCfg = certmagic.New(cmCache, *cmCfg)
|
||||
|
||||
if len(domains) == 0 {
|
||||
return nil, fmt.Errorf("no domains configured for ACME")
|
||||
}
|
||||
|
||||
if err := cmCfg.ManageSync(context.Background(), domains); err != nil {
|
||||
return nil, fmt.Errorf("acme manage sync: %w", err)
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS13,
|
||||
GetCertificate: cmCfg.GetCertificate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.logger.Info("QUIC server listening",
|
||||
zap.String("addr", s.listener.Addr().String()))
|
||||
|
||||
for {
|
||||
conn, err := s.listener.Accept(ctx)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
s.logger.Error("Accept error", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
go s.handler.OnConnect(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
err1 := s.listener.Close()
|
||||
err2 := s.tr.Close()
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
return err2
|
||||
}
|
||||
27
internal/transport/stream.go
Normal file
27
internal/transport/stream.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/apernet/quic-go"
|
||||
"github.com/niko/qcc/internal/protocol"
|
||||
)
|
||||
|
||||
type Stream struct {
|
||||
q *quic.Stream
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewStream(s *quic.Stream) *Stream {
|
||||
return &Stream{q: s}
|
||||
}
|
||||
|
||||
func (s *Stream) ReadFrame() (*protocol.Frame, error) {
|
||||
return protocol.ReadFrame(s.q)
|
||||
}
|
||||
|
||||
func (s *Stream) WriteFrame(opCode byte, payload []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return protocol.WriteFrame(s.q, opCode, payload)
|
||||
}
|
||||
7
main.go
Normal file
7
main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/niko/qcc/cmd/qccd"
|
||||
|
||||
func main() {
|
||||
qccd.Execute()
|
||||
}
|
||||
21
pkg/types/opcode.go
Normal file
21
pkg/types/opcode.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package types
|
||||
|
||||
type OpCode byte
|
||||
|
||||
const (
|
||||
OpGetChallenge OpCode = 0x01
|
||||
OpChallenge OpCode = 0x02
|
||||
OpSolve OpCode = 0x03
|
||||
OpIdentity OpCode = 0x04
|
||||
OpReroll OpCode = 0x05
|
||||
|
||||
OpDial OpCode = 0x10
|
||||
OpRing OpCode = 0x11
|
||||
OpAccept OpCode = 0x12
|
||||
OpPeerAccept OpCode = 0x13
|
||||
OpReject OpCode = 0x14
|
||||
OpEnd OpCode = 0x15
|
||||
OpPeerEnd OpCode = 0x16
|
||||
|
||||
OpError OpCode = 0xFF
|
||||
)
|
||||
54
pkg/types/types.go
Normal file
54
pkg/types/types.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package types
|
||||
|
||||
type RecType uint8
|
||||
|
||||
const (
|
||||
RecIdentity RecType = 1
|
||||
RecCallLog RecType = 2
|
||||
RecIndex RecType = 6
|
||||
)
|
||||
|
||||
type ErrorCode uint16
|
||||
|
||||
const (
|
||||
ErrNone ErrorCode = 0x00
|
||||
ErrInternal ErrorCode = 0x01
|
||||
ErrInvalidRequest ErrorCode = 0x02
|
||||
ErrPoWInvalid ErrorCode = 0x03
|
||||
ErrPoWExpired ErrorCode = 0x04
|
||||
ErrPoWDifficulty ErrorCode = 0x05
|
||||
ErrNumberTaken ErrorCode = 0x06
|
||||
ErrCooldown ErrorCode = 0x07
|
||||
ErrNumberNotFound ErrorCode = 0x08
|
||||
ErrCallNotFound ErrorCode = 0x09
|
||||
ErrBusy ErrorCode = 0x0A
|
||||
ErrSelfCall ErrorCode = 0x0B
|
||||
ErrRateLimited ErrorCode = 0x0C
|
||||
)
|
||||
|
||||
type CodecType uint8
|
||||
|
||||
const (
|
||||
CodecOpus CodecType = 1
|
||||
CodecVP9 CodecType = 2
|
||||
CodecH264 CodecType = 3
|
||||
CodecH265 CodecType = 4
|
||||
)
|
||||
|
||||
type MediaFlags uint8
|
||||
|
||||
const (
|
||||
MediaKeyframe MediaFlags = 1 << 0
|
||||
MediaSilence MediaFlags = 1 << 1
|
||||
)
|
||||
|
||||
const (
|
||||
MaxFrameSize = 67_108_864
|
||||
MaxRecordsPerFrame = 1_000_000
|
||||
)
|
||||
|
||||
const (
|
||||
StreamAuth byte = 0x01
|
||||
StreamCall byte = 0x02
|
||||
DgramMedia byte = 0x10
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue