Compare commits

...

27 Commits

Author SHA1 Message Date
Zane Schepke 92fcb1b929 fix rust install 2023-12-11 23:07:56 -05:00
Zane Schepke db2005a4a0 fix rust install 2023-12-11 23:04:28 -05:00
Zane Schepke 7d41b0d2ba fix rust install 2023-12-11 22:59:28 -05:00
Zane Schepke 4314f53139 fix rust install 2023-12-11 22:45:18 -05:00
Zane Schepke 3e3f305f0b fix script 2023-12-11 22:35:28 -05:00
Zane Schepke dc33f078a1 fix script 2023-12-11 22:26:40 -05:00
Zane Schepke d76d1bb876 fix script 2023-12-11 22:16:57 -05:00
Zane Schepke c9ea25a157 move ci scripts 2023-12-11 22:07:33 -05:00
Zane Schepke a79b91ae00 fix script 2023-12-11 22:01:50 -05:00
Zane Schepke 8e855960f9 Create ci_post_clone.sh 2023-12-11 21:50:33 -05:00
Zane Schepke 4545f7e3e0 add xcode proj 2023-12-11 05:57:20 -05:00
Zane Schepke 89c97c684e add keychain path again 2023-12-08 09:27:47 -05:00
Zane Schepke 776693b0ef remove keychain path 2023-12-08 07:41:11 -05:00
Zane Schepke 0eac90783d add keychain path signing arg 2023-12-06 10:11:45 -05:00
Mark Sinclair e0dd29898a Fix up some paths for MacOS package 2023-12-05 20:09:58 +00:00
Zane Schepke 8e43a5ce1d Update sign.sh 2023-11-27 04:44:43 -05:00
Zane Schepke ef30cafaa4 Update sign.sh 2023-11-27 03:31:23 -05:00
Zane Schepke 80f81f1c4a Update sign.sh 2023-11-27 00:02:31 -05:00
Zane Schepke 3e98fee06e Update sign.sh 2023-11-26 20:55:50 -05:00
Zane Schepke 099d23b568 update bundle id and package 2023-11-24 12:49:28 -05:00
Zane Schepke 874aecb0a4 Update sign.sh 2023-11-24 09:04:22 -05:00
Zane Schepke acf9de0f74 Add protoc optionals arg 2023-11-24 06:56:13 -05:00
Zane Schepke 064624d8ec use x86 for signtool 2023-11-24 06:39:28 -05:00
Zane Schepke ced32da018 Update sign.sh 2023-11-23 18:23:50 -05:00
Zane Schepke ad5c0991c0 Add desktop vpn app project 2023-11-20 08:51:23 -05:00
Zane Schepke 2c63518735 Merge branch 'develop' of https://github.com/nymtech/nym into develop 2023-11-16 08:57:21 -05:00
Zane Schepke 2c32ce8cf0 Update README.md 2023-11-15 09:39:18 -05:00
282 changed files with 45033 additions and 7195 deletions
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# ci_post_clone.sh
cd /nym-vpn/desktop && \
curl https://sh.rustup.rs -sSf | sh && \
cargo install cargo-deb;
cargo install --force cargo-make;
cargo install sd;
cargo install ripgrep;
cargo install cargo-about;
cargo install cargo-generate-rpm;
brew install protobuf;
APPLICATION_SIGNING_IDENTITY="Developer ID Application: Nym Technologies SA (VW5DZLFHM5)" \
INSTALLER_SIGNING_IDENTITY="3rd Party Mac Developer Installer: Nym Technologies SA (VW5DZLFHM5)" \
APPLE_TEAM_ID=VW5DZLFHM5 \
cargo make pkg;
+4
View File
@@ -0,0 +1,4 @@
build/*
data/*
dist/*
target/*
+32
View File
@@ -0,0 +1,32 @@
# Generated by Cargo
# will have compiled files and executables
target
# These are backup files generated by rustfmt
**/*.rs.bk
node_modules
data
build
.cargo
.DS_Store
# generated Typescript types from Rust
bindings
staging-config
dist
.nymvpn
windows/x86_64*
*.wixobj
*.wixpdb
*.msi
mkcert*
nymvpn-oss-licenses.html
nymvpn-oss-licenses-rust.html
third-party-licenses.txt
+3
View File
@@ -0,0 +1,3 @@
{
"svg.preview.background": "editor"
}
+121
View File
@@ -0,0 +1,121 @@
# Building nymvpn app
## Install build dependencies
### Common for all Platforms
```
cargo install cargo-deb
cargo install cargo-generate-rpm
cargo install --force cargo-make
cargo install sd
cargo install ripgrep
cargo install cargo-about
```
### Linux
```
apt install build-essential \
pkg-config \
libgtk-3-dev \
libssl-dev \
libsoup2.4-dev \
libjavascriptcoregtk-4.0-dev \
libwebkit2gtk-4.0-dev \
libmnl-dev \
libnftnl-dev \
protobuf-compiler \
zip \
```
Install protoc on x86_64/amd64 machines
```
# x86_64
curl -Lo protoc-3.19.1-linux-x86_64.zip \
https://github.com/protocolbuffers/protobuf/releases/download/v3.19.1/protoc-3.19.1-linux-x86_64.zip && \
unzip protoc-3.19.1-linux-x86_64.zip -d /tmp && \
mv /tmp/bin/protoc /usr/bin/protoc && \
rm protoc-3.19.1-linux-x86_64.zip
```
Install protoc on arm64 machines
```
# aarch64
curl -Lo protoc-3.19.1-linux-aarch_64.zip \
https://github.com/protocolbuffers/protobuf/releases/download/v3.19.1/protoc-3.19.1-linux-aarch_64.zip && \
unzip protoc-3.19.1-linux-aarch_64.zip -d /tmp && \
sudo mv /tmp/bin/protoc /usr/bin/protoc && \
rm protoc-3.19.1-linux-aarch_64.zip
```
### macOS
TODO
### Windows
TODO
## Build Debian package
```
cargo make deb
```
## Build RPM package
```
cargo make rpm
```
## Build macOS package
```
cargo make pkg
```
To codesign for distribution provide following environment variables:
```
APPLE_TEAM_ID=...
APPLICATION_SIGNING_IDENTITY=...
INSTALLER_SIGNING_IDENTITY=...
cargo make pkg
```
## Build installer for Windows
```
cargo make msi
```
To codesign for distribution:
```
SIGN=true cargo make msi
```
## Building for Production for Linux
### Build the Builder
Build the Docker image to build nymvpn app.
```
cd nymvpn-packages
cargo make builder
```
This will output `tag.txt`, commit it into source code.
### Build .deb and .rpm
This step uses builder Docker image with tag in `nymvpn-packages/tag.txt`. The final rpm and deb packages will be stored in `dist` directory.
```
# For host platform
cargo make linux
# For target platform
cargo make -e TARGET=aarch64-unknown-linux-gnu linux
```
+7
View File
@@ -0,0 +1,7 @@
# Contributing
Firstly, thank you for considering to contribute and make nymvpn-app better! Please note that all of your contributions would be under GPL-3.0.
- Please see issues with `help wanted` or `good first issue` labels.
- For bug fixes, Pull Requests or Github Issues are welcome.
- For new features its best to create a Github Issue with detailed description.
+8625
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
[workspace]
resolver = "2"
members = [
"nymvpn-cli",
"nymvpn-config",
"nymvpn-controller",
"nymvpn-daemon",
"nymvpn-entity",
"nymvpn-migration",
"nymvpn-packages",
"nymvpn-server",
"nymvpn-types",
"nymvpn-ui/src-tauri",
]
[profile.release]
#opt-level = 'z' # Optimize for size.
opt-level = 3
lto = true # Enable Link Time Optimization
#codegen-units = 1 # Reduce number of codegen units to increase optimizations.
#panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary*
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 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 General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is 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. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
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.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
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 General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
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 Affero 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 special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU 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 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 General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU 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 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 General Public License for more details.
You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
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 GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+331
View File
@@ -0,0 +1,331 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
PWD = {script = ["pwd"]}
REPO_ROOT = "${PWD}"
BUILDER_TAG = {script = ["cat nymvpn-packages/tag.txt"] }
UPSTREAM_REPO = "https://github.com/upvpn/mullvadvpn-app.git"
UPSTREAM_REV = "2023.3.upvpn"
UPSTREAM_REPO_PATH = "${PWD}/.upvpn/mullvadvpn-app"
RUSTUP_HOST = { script = ["rustup show | rg host | awk '{print $3}'"] }
APP_VERSION = { script = ["grep version nymvpn-packages/Cargo.toml | cut -d '\"' -f2"]}
TARGET = { script = ["echo ${RUSTUP_HOST}"], condition = { env_not_set = ["TARGET"] } }
STRIP = {script = ["([ ${TARGET} = \"aarch64-unknown-linux-gnu\" ] && echo \"--no-strip\") || echo \"--strip\""]}
[tasks.release-build]
workspace = false
command = "cargo"
args = ["build", "--release", "--target", "${TARGET}"]
[tasks.deb]
workspace = false
command = "cargo"
args = ["deb", "-p", "nymvpn-packages", "--target", "${TARGET}", "${STRIP}", "${@}"]
dependencies = ["oss", "ui", "wglib"]
[tasks.rpm]
workspace = false
command = "cargo"
args = ["generate-rpm", "-p", "nymvpn-packages", "--target", "${TARGET}", "${@}"]
dependencies = ["oss", "ui", "wglib", "release-build", "strip"]
[tasks.rpm-no-build]
workspace = false
command = "cargo"
args = ["generate-rpm", "-p", "nymvpn-packages", "--target", "${TARGET}", "${@}"]
dependencies = ["ui", "wglib", "strip"]
[tasks.strip]
workspace = false
script = '''
CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-target}
if [ "${STRIP:-}" = "--strip" ]; then
for file in nymvpn nymvpn-ui nymvpn-daemon
do
output="${CARGO_TARGET_DIR}/${TARGET}/release/${file}"
if [ -f "${output}" ]; then
strip "${output}"
fi
done
fi
'''
[tasks.pkg]
workspace = false
script = '''
tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir')
cp -R nymvpn-packages/macos ${tempdir}
cp nymvpn-assets/icons/icon.icns ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/Resources
cp nymvpn-packages/nymvpn-oss-licenses.html ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/Resources
cp target/${TARGET}/release/nymvpn ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/Resources
cp target/${TARGET}/release/nymvpn-daemon ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/Resources
cp target/${TARGET}/release/nymvpn-ui ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/MacOS
rm ${tempdir}/macos/pkg/root/Applications/nymvpn.app/Contents/MacOS/.gitkeep
pushd ${tempdir}/macos
./build.sh "${APP_VERSION}"
popd
mkdir -p target/pkg
AARCH=$(uname -m)
cp ${tempdir}/macos/nymvpn-${APP_VERSION}.pkg target/pkg/nymvpn-${APP_VERSION}-${AARCH}.pkg
echo "Output: target/pkg/nymvpn-${APP_VERSION}-${AARCH}.pkg"
rm -r ${tempdir}
'''
dependencies = ["oss", "ui", "wglib", "release-build"]
[tasks.setupupstream]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
mkdir -p ${UPSTREAM_REPO_PATH}
if [ ! -d ${UPSTREAM_REPO_PATH}/.git ]; then
git clone ${UPSTREAM_REPO} ${UPSTREAM_REPO_PATH}
cd ${UPSTREAM_REPO_PATH} && git submodule update --init && cd -
fi
cd ${UPSTREAM_REPO_PATH}
git fetch --all --tags
git checkout ${UPSTREAM_REV}
'''
[tasks.wglib]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
cd ${UPSTREAM_REPO_PATH}
./wireguard/build-wireguard-go.sh "${TARGET}"
cp -r ${UPSTREAM_REPO_PATH}/build ${REPO_ROOT}
'''
dependencies = ["cargo-config", "setupupstream"]
[tasks.winfw]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
output_dir=${REPO_ROOT}/windows/${TARGET}
mkdir -p ${output_dir}
cd ${UPSTREAM_REPO_PATH}
IS_RELEASE=true CPP_BUILD_MODES=Release ./build-windows-modules.sh
cp -r ${UPSTREAM_REPO_PATH}/windows/winfw/bin/x64-Release/ ${output_dir}
'''
[tasks.windows]
workspace = false
dependencies = ["oss", "cargo-config", "setupupstream", "wglib", "ui", "winfw", "release-build"]
[tasks.windows-sign]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
set -eu
./nymvpn-packages/windows/sign.sh \
target/${TARGET}/release/nymvpn.exe \
target/${TARGET}/release/nymvpn-ui.exe \
target/${TARGET}/release/nymvpn-daemon.exe \
build/lib/${TARGET}/libwg.dll \
windows/${TARGET}/X64-Release/winfw.dll
'''
[tasks.only-msi]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
cd nymvpn-packages/windows
candle -ext WixUtilExtension -dAppVersion=${APP_VERSION} -out nymvpn.wixobj -arch x64 nymvpn.wsx
light -ext WixUtilExtension -dAppVersion=${APP_VERSION} -out nymvpn-x64-${APP_VERSION}.msi nymvpn.wixobj
'''
[tasks.sign-msi]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
set -eu
./nymvpn-packages/windows/sign.sh nymvpn-packages/windows/nymvpn-x64-${APP_VERSION}.msi
'''
[tasks.msi]
workspace = false
dependencies = ["windows", "windows-sign", "only-msi", "sign-msi"]
[tasks.cargo-config]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
root_dir=$(pwd)
mkdir -p ${root_dir}/.cargo
cp ${root_dir}/nymvpn-packages/cargo-config.toml ${root_dir}/.cargo/config
sd __REPO_ROOT__ "${root_dir}" ${root_dir}/.cargo/config
'''
[tasks.ui]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
cd nymvpn-ui
npm install
npm run build
'''
[tasks.volumes-for-build]
workspace = false
script = '''
docker volume rm nymvpn-app-builder || true
docker volume create nymvpn-app-builder-cargo-git
docker volume create nymvpn-app-builder-cargo-registry
docker volume create nymvpn-app-builder-cargo-target
docker volume create nymvpn-app-builder-go
docker volume create nymvpn-app-builder
'''
[tasks.clear-and-copy-source]
workspace = false
script = '''
docker run --rm -v ${PWD}:/source \
-v nymvpn-app-builder:/build \
ghcr.io/nymvpn/nymvpn-app-builder:${BUILDER_TAG} \
cp -r \
/source/nymvpn-assets \
/source/nymvpn-cli \
/source/nymvpn-config \
/source/nymvpn-controller \
/source/nymvpn-daemon \
/source/nymvpn-entity \
/source/nymvpn-migration \
/source/nymvpn-packages \
/source/nymvpn-server \
/source/nymvpn-types \
/source/nymvpn-ui \
/source/Cargo.toml \
/source/Cargo.lock \
/source/about.toml \
/source/Makefile.toml \
/source/.dockerignore \
/build/
docker run --rm \
-v nymvpn-app-builder:/build \
ghcr.io/nymvpn/nymvpn-app-builder:${BUILDER_TAG} chown -R root:root .
'''
[tasks.builder-shell]
workspace = false
command = "docker"
args = [
"run",
"--rm",
"-it",
"-v", "nymvpn-app-builder-cargo-git:/root/.cargo/git",
"-v", "nymvpn-app-builder-cargo-registry:/root/.cargo/registry",
"-v", "nymvpn-app-builder-cargo-target:/root/.cargo/target",
"-v", "nymvpn-app-builder-go:/root/go",
"-v", "nymvpn-app-builder:/build",
# To clone github repo using ssh
"-v", "${HOME}/.ssh:/root/.ssh",
"-e", "CARGO_NET_GIT_FETCH_WITH_CLI=true",
"-e", "TARGET=${TARGET}",
# tag created by builder task
"ghcr.io/nymvpn/nymvpn-app-builder:${BUILDER_TAG}",
"bash"
]
dependencies = ["volumes-for-build", "clear-and-copy-source"]
[tasks.linux-packages]
workspace = false
# only need to build once for packaging both
dependencies = ["deb", "rpm-no-build"]
[tasks.build-in-container]
workspace = false
command = "docker"
args = [
"run",
"--rm",
"-it",
"-v", "nymvpn-app-builder-cargo-git:/root/.cargo/git",
"-v", "nymvpn-app-builder-cargo-registry:/root/.cargo/registry",
"-v", "nymvpn-app-builder-cargo-target:/root/.cargo/target",
"-v", "nymvpn-app-builder-go:/root/go",
"-v", "nymvpn-app-builder:/build",
# To clone github repo using ssh
"-v", "${HOME}/.ssh:/root/.ssh",
"-e", "CARGO_NET_GIT_FETCH_WITH_CLI=true",
"-e", "TARGET=${TARGET}",
# tag created by builder task
"ghcr.io/nymvpn/nymvpn-app-builder:${BUILDER_TAG}",
"cargo",
"make",
"-e", "TARGET=${TARGET}",
"linux-packages",
]
dependencies = ["volumes-for-build", "clear-and-copy-source"]
[tasks.output-dir]
workspace = false
script = '''
echo "TARGET: ${TARGET}"
mkdir -p ${PWD}/dist/${TARGET}
'''
[tasks.linux]
workspace = false
command = "docker"
args = [
"run",
"--rm",
"-it",
"-v", "nymvpn-app-builder-cargo-target:/root/.cargo/target",
"-v", "${PWD}/dist:/dist",
# tag created by builder task
"ghcr.io/nymvpn/nymvpn-app-builder:${BUILDER_TAG}",
"cp", "-r",
"/root/.cargo/target/${TARGET}/debian", "/root/.cargo/target/${TARGET}/generate-rpm",
"/dist/${TARGET}",
]
dependencies = ["output-dir", "build-in-container"]
[tasks.icon]
workspace = false
script = '''
cd nymvpn-ui
cargo tauri icon -o ../nymvpn-assets/icons ../nymvpn-assets/app-icon.png
'''
[tasks.oss]
workspace = false
script_runner = "sh"
script = '''
#!/usr/bin/env bash
set -eu
cargo about generate nymvpn-packages/nymvpn-oss-licenses-rust.hbs > nymvpn-packages/nymvpn-oss-licenses-rust.html
sd "(talpid-[\w-]+|tunnel-obfuscation) (\d+.\d+.\d+)" '$1 $2 Copyright (C) Mullvad VPN AB, GPL-3.0' nymvpn-packages/nymvpn-oss-licenses-rust.html
sd "https://crates.io/crates//(talpid-\w+|tunnel-obfuscation)" 'https://github.com/mullvad/mullvadvpn-app/tree/main/$1' nymvpn-packages/nymvpn-oss-licenses-rust.html
cd nymvpn-ui && npm i && cd ..
npx --yes generate-license-file --overwrite --ci --input nymvpn-ui/package.json --output nymvpn-packages/third-party-licenses.txt
cat nymvpn-packages/nymvpn-oss-licenses-header.hbs > nymvpn-packages/nymvpn-oss-licenses.html
cat nymvpn-packages/nymvpn-oss-licenses-rust.html >> nymvpn-packages/nymvpn-oss-licenses.html
cat nymvpn-packages/third-party-licenses.txt >> nymvpn-packages/nymvpn-oss-licenses.html
cat nymvpn-packages/nymvpn-oss-licenses-footer.hbs >> nymvpn-packages/nymvpn-oss-licenses.html
'''
[tasks.android-init]
workspace = false
script = '''
git clone https://github.com:WireGuard/wireguard-tools.git nymvpn-android/app/tunnel/wireguard-tools
cd nymvpn-android/app/tunnel/wireguard-tools && git checkout b4f6b4f229d291daf7c35c6f1e7f4841cc6d69bc && cd -
git clone https://github.com:termux/termux-elf-cleaner.git nymvpn-android/app/tunnel/elf-cleaner
cd nymvpn-android/app/tunnel/elf-cleaner && git checkout 7efc05090675ec6161b7def862728086a26c3b1f && cd -
'''
+12
View File
@@ -0,0 +1,12 @@
# Network Dependency
nymvpn takes its design inspiration from [mullvadvpn-app](https://github.com/mullvad/mullvadvpn-app).
For example Actor pattern is foundational in daemon.
However, nymvpn uses different design/technologies than mullvadvpn-app in many places: for persisting data in sqlite (sea-orm), desktop app (Tauri), packaging for Windows (Wix), Toml for configuration files, GRPC for backend API.
nymvpn uses "talpid" crates for all of the client side networking from mullvadvpn-app project, with minimal modification. For example, interface on Linux would be named "nymvpn".
The modifications to upstream mullvadvpn-app can be found [here](https://github.com/upvpn/mullvadvpn-app)
We're not affiliated with Mullvad, and we're grateful for their high quality open source projects. If nymvpn doesn't offer what you're looking for please see VPN offering from Mullvad at https://mullvad.net/
+63
View File
@@ -0,0 +1,63 @@
<div align="center">
<a href="https://nymvpn.net">
<img src="./nymvpn-assets/icons/Square71x71Logo.png" >
</a>
<h3 align="center">nymvpn</h3>
<h4 align="center">A Modern Serverless VPN</h4>
<img src="nymvpn-assets/cli.gif" />
</div>
# nymvpn
nymvpn (pronounced Up VPN) app is WireGuard VPN client for Linux, macOS, Windows, and Android.
For more information please visit https://nymvpn.net
nymvpn desktop app is made up of UI, CLI and background Daemon.
# Serverless
nymvpn uses Serverless computing model, where a Linux based WireGuard server is provisioned on public cloud providers when app requests to connect to VPN. And server is deprovisioned when app requests to disconnect from VPN.
All of it happens with a single click or tap on the UI, or a single command on terminal.
# Install
App for Linux, macOS, Windows, and Android is available for download on [Github Releases](https://github.com/nymvpn/nymvpn-app/releases) or on website at https://nymvpn.net/download
# Code
## Organization
| Crate or Directory | Description |
| --- | --- |
| nymvpn-android | Standalone app for Android. |
| nymvpn-cli | Code for `nymvpn` cli. |
| nymvpn-config | Configuration read from env vars, `nymvpn.conf.toml` are merged at runtime in `nymvpn-config` and is source of runtime configuration for `nymvpn-cli`, `nymvpn-daemon`, and `nymvpn-ui`. |
| nymvpn-controller | Defines GRPC protobuf for APIs exposed by `nymvpn-daemon` to be consumed by `nymvpn-cli` and `nymvpn-ui`. |
| nymvpn-daemon | Daemon is responsible for orchestrating a VPN session. It takes input from nymvpn-cli or nymvpn-ui via GRPC (defined in `nymvpn-controller`) and make calls to backend server via separate GRPC (defined in `nymvpn-server`). When backend informs that a server is ready daemon configures network tunnel, see [NetworkDependency.md](./NetworkDependency.md) for more info. |
| nymvpn-entity | Defines data models used by nymvpn-daemon to persist data on disk in sqlite database. |
| nymvpn-migration | Defines database migration from which `nymvpn-entity` is generated. |
| nymvpn-packages| Contains resources to package binaries for distribution on macOS (pkg), Linux (rpm & deb), and Windows (msi). |
|nymvpn-server| Contains GRPC protobuf definitions and code for communication with backend server. |
| nymvpn-types | Defines common Rust types for data types used in various crates. These are also used to generate Typescript types for nymvpn-ui for seamless serialization and deserialization across language boundaries. |
|nymvpn-ui| A Tauri based desktop app. GPRC communication with daemon is done in Rust. Typescript code interact with Rust code via Tauri commands. |
## Building Desktop Apps
Please see [Build.md](./Build.md)
## Building Android App
Please see [nymvpn-android/README.md](./nymvpn-android/README.md)
# License
Android app, and all Rust crates in this repository are [licensed under GPL version 3](./LICENSE).
Copyright (C) 2023 Nym Technologies S.A.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
+3
View File
@@ -0,0 +1,3 @@
# Security
If you find security issues/vulnerabilities, please report them via security@nymvpn.net with details about the vulnerability/issue. Security issue should not be reported via public Github Issue tracker.
+30
View File
@@ -0,0 +1,30 @@
accepted = [
"Apache-2.0",
"MIT",
"GPL-3.0",
"MPL-2.0",
"ISC",
"BSD-3-Clause",
"BSD-2-Clause",
"Unicode-DFS-2016",
"WTFPL",
"OpenSSL",
"NOASSERTION",
"Apache-2.0 WITH LLVM-exception",
]
workarounds = [
"ring",
"rustls",
]
targets = [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
]
ignore-build-dependencies = true
ignore-dev-dependencies = true
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.
+659
View File
@@ -0,0 +1,659 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="100%" viewBox="0 0 2480 2480" enable-background="new 0 0 2480 2480" xml:space="preserve">
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M2.000000,1156.000000
C2.000000,771.474121 2.000000,386.948273 2.000000,2.000000
C386.718231,2.000000 771.436646,2.000000 1157.575928,2.774093
C1156.895752,4.252493 1154.836182,5.386602 1152.687378,5.593789
C1133.805298,7.414357 1114.907104,9.069780 1095.226807,10.759283
C1084.722656,12.009069 1074.967529,13.036946 1065.296387,14.588613
C1046.388062,17.622320 1027.440552,20.518517 1008.669189,24.271704
C988.973267,28.209772 969.464844,33.085560 949.059448,37.567116
C946.606445,37.857994 944.916931,37.960854 943.338196,38.433754
C905.230347,49.848984 866.958191,60.757950 829.109192,72.977303
C809.630737,79.265816 790.909973,87.901482 771.094727,95.550079
C708.056335,120.873268 648.601501,151.681488 591.455200,186.981873
C581.890991,192.889832 572.727356,199.446106 562.717407,205.841736
C485.335510,255.803986 415.925385,314.480255 351.878967,379.639160
C350.056000,381.493774 348.766235,383.872528 346.598083,386.351074
C336.670746,396.616455 326.916168,406.152496 318.166016,416.535034
C296.527496,442.210358 274.620392,467.712158 254.140381,494.299805
C214.260880,546.072327 179.360336,601.135254 148.695190,658.878784
C118.683922,715.390991 92.776169,773.679565 71.820473,834.158081
C50.995865,894.258301 34.644413,955.509277 23.362127,1018.139221
C16.235693,1057.699341 10.648326,1097.425903 7.645359,1137.512329
C7.224460,1143.130981 6.797827,1148.756470 6.088929,1154.342651
C5.794058,1156.666138 4.936373,1159.359985 2.000000,1156.000000
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M1144.000000,2482.000000
C763.472534,2482.000000 382.945099,2482.000000 2.000000,2482.000000
C2.000000,2092.614990 2.000000,1703.229980 2.840354,1312.796387
C4.341088,1313.846558 5.403334,1315.909912 5.592262,1318.050293
C6.408852,1327.301758 7.018379,1336.572510 7.615459,1345.841919
C10.071630,1383.973022 15.361381,1421.746460 21.943558,1459.353882
C31.523199,1514.087402 44.837513,1567.894409 61.760971,1620.828613
C86.347641,1697.732544 118.251434,1771.449707 157.290771,1842.110718
C196.026291,1912.221802 241.077148,1977.993042 292.739014,2039.196167
C304.506073,2053.136963 317.056641,2066.416016 329.501801,2080.542725
C338.064240,2090.107910 345.990631,2099.520020 354.754700,2108.074463
C375.889771,2128.703857 397.016846,2149.373779 418.914001,2169.179443
C435.849579,2184.497559 453.947357,2198.531006 471.971558,2213.595947
C480.738647,2220.616211 488.968445,2227.299561 497.402283,2233.714600
C538.868896,2265.256348 582.676025,2293.294678 627.428223,2319.866211
C637.853210,2326.055908 649.016357,2331.002197 660.245605,2337.048828
C661.758423,2338.312500 662.805664,2339.164307 663.980896,2339.765869
C705.744995,2361.139404 748.015625,2381.423828 791.896851,2398.136475
C807.088379,2403.922363 822.536743,2409.033447 838.378052,2414.957031
C882.580322,2430.489990 926.889465,2443.370117 972.065918,2453.111572
C987.937622,2456.534180 1003.978516,2459.171875 1020.532410,2462.681152
C1053.641235,2467.597900 1086.152588,2472.055908 1118.684082,2476.362061
C1125.906372,2477.318115 1133.209839,2477.647217 1140.452515,2478.470947
C1142.249756,2478.675537 1145.452026,2478.209473 1144.000000,2482.000000
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M1330.000000,2.000000
C1713.857544,2.000000 2097.715088,2.000000 2482.000000,2.000000
C2482.000000,377.384888 2482.000000,752.769958 2481.270508,1129.683350
C2479.812988,1129.458130 2478.641357,1127.757446 2478.425049,1125.942993
C2476.303955,1108.153687 2474.590576,1090.313354 2472.288330,1072.548706
C2465.317139,1018.756775 2454.290039,965.764282 2439.924805,913.480469
C2410.317139,805.719543 2366.872070,703.805603 2309.638672,607.834778
C2279.080322,556.593994 2245.477295,507.387878 2207.102783,461.646027
C2185.742920,436.185822 2163.918457,411.044861 2141.083740,386.914246
C2120.303955,364.955353 2097.875244,344.556519 2075.900391,322.875854
C2049.050293,300.195190 2023.122681,277.266205 1995.772095,256.183319
C1919.572388,197.446091 1837.257935,148.730927 1749.337158,109.649597
C1727.213379,99.815414 1704.468994,91.377357 1681.312744,82.023506
C1661.278564,74.856789 1642.080688,67.535522 1622.562744,61.200005
C1605.227905,55.573128 1587.538086,51.039959 1569.348145,45.649433
C1535.447754,37.638149 1502.300781,29.539127 1468.921631,22.549688
C1450.818604,18.759012 1432.319946,16.858213 1413.201904,13.909479
C1398.085693,11.913866 1383.790894,9.888351 1369.441406,8.383907
C1357.681641,7.150996 1345.856567,6.545444 1334.061523,5.645791
C1331.152710,5.423922 1331.154907,5.395327 1330.000000,2.000000
z"/>
<path fill="#000000" opacity="1.000000" stroke="none"
d="
M2482.000000,1354.000000
C2482.000000,1729.859741 2482.000000,2105.719482 2482.000000,2482.000000
C2103.281738,2482.000000 1724.563354,2482.000000 1344.315430,2481.265137
C1344.194214,2479.820801 1345.551147,2478.641357 1347.019287,2478.481201
C1354.873779,2477.624268 1362.792847,2477.302979 1370.622192,2476.279785
C1398.529907,2472.632324 1426.410522,2468.776123 1455.144531,2464.862793
C1484.524414,2458.810791 1513.163086,2453.338135 1541.560669,2446.816162
C1559.249390,2442.753906 1576.573853,2437.106201 1594.844482,2432.142090
C1600.700806,2430.654297 1605.865479,2429.451660 1610.832886,2427.692139
C1641.329224,2416.889404 1671.780884,2405.960693 1702.995361,2394.897949
C1714.292969,2390.337891 1725.003662,2386.301025 1735.360352,2381.500488
C1760.088379,2370.039062 1784.854858,2358.629395 1809.236816,2346.459473
C1869.238037,2316.510742 1925.746704,2280.830078 1979.677734,2241.039062
C2015.833496,2214.363281 2050.923828,2186.328613 2083.269287,2155.113281
C2110.550537,2128.784912 2137.848877,2102.374756 2163.551514,2074.534912
C2197.400391,2037.871460 2228.250977,1998.649048 2257.070068,1957.831177
C2302.509521,1893.473022 2341.425049,1825.434570 2374.022217,1753.742676
C2402.992188,1690.027466 2426.233154,1624.269775 2443.892578,1556.565674
C2459.084717,1498.321045 2470.223145,1439.301270 2476.330078,1379.378052
C2477.070068,1372.116943 2477.632080,1364.836426 2478.475586,1357.588135
C2478.686279,1355.777344 2478.205322,1352.590210 2482.000000,1354.000000
z"/>
<path fill="#FD0059" opacity="1.000000" stroke="none"
d="
M2482.000000,1353.062744
C2478.205322,1352.590210 2478.686279,1355.777344 2478.475586,1357.588135
C2477.632080,1364.836426 2477.070068,1372.116943 2476.330078,1379.378052
C2470.223145,1439.301270 2459.084717,1498.321045 2443.892578,1556.565674
C2426.233154,1624.269775 2402.992188,1690.027466 2374.022217,1753.742676
C2341.425049,1825.434570 2302.509521,1893.473022 2257.070068,1957.831177
C2228.250977,1998.649048 2197.400391,2037.871460 2163.551514,2074.534912
C2137.848877,2102.374756 2110.550537,2128.784912 2083.269287,2155.113281
C2050.923828,2186.328613 2015.833496,2214.363281 1979.677734,2241.039062
C1925.746704,2280.830078 1869.238037,2316.510742 1809.236816,2346.459473
C1784.854858,2358.629395 1760.088379,2370.039062 1735.360352,2381.500488
C1725.003662,2386.301025 1714.292969,2390.337891 1702.934937,2393.890381
C1702.222534,2384.750732 1700.063599,2375.522217 1702.984741,2368.367676
C1706.637573,2359.420410 1703.389404,2351.443115 1703.707520,2343.142334
C1704.223755,2329.672607 1705.858032,2316.132568 1704.636475,2302.210449
C1706.131104,2301.343262 1706.999023,2300.838623 1707.911499,2300.435059
C1872.842163,2227.512939 2013.766113,2122.305664 2129.126953,1983.796509
C2295.175537,1784.427856 2384.085449,1554.437256 2397.438477,1295.429810
C2400.011230,1245.527954 2398.809570,1195.543945 2394.555176,1145.641113
C2389.225586,1083.130127 2379.285156,1021.439026 2363.923584,960.643433
C2319.840820,786.178650 2239.296875,630.459351 2122.668213,493.482666
C2104.113770,471.691101 2084.243408,451.019867 2065.204102,428.914398
C2066.549561,423.919678 2068.180664,419.901825 2068.675049,415.748596
C2069.418701,409.502869 2074.961914,403.035431 2069.638428,396.963745
C2062.773438,389.134186 2062.122070,381.922882 2069.412598,374.247559
C2069.738037,373.904999 2069.454346,372.983826 2069.454346,371.716705
C2065.993652,368.276642 2060.073975,365.611786 2062.831055,358.350494
C2064.850586,353.031799 2069.009521,354.874359 2072.047607,355.040344
C2072.672607,349.993500 2073.195801,345.550232 2073.778809,341.114838
C2074.552490,335.230011 2075.373047,329.351349 2076.173340,323.470032
C2097.875244,344.556519 2120.303955,364.955353 2141.083740,386.914246
C2163.918457,411.044861 2185.742920,436.185822 2207.102783,461.646027
C2245.477295,507.387878 2279.080322,556.593994 2309.638672,607.834778
C2366.872070,703.805603 2410.317139,805.719543 2439.924805,913.480469
C2454.290039,965.764282 2465.317139,1018.756775 2472.288330,1072.548706
C2474.590576,1090.313354 2476.303955,1108.153687 2478.425049,1125.942993
C2478.641357,1127.757446 2479.812988,1129.458130 2481.270508,1130.605713
C2482.000000,1204.041748 2482.000000,1278.083618 2482.000000,1353.062744
z"/>
<path fill="#FD6141" opacity="1.000000" stroke="none"
d="
M329.246643,2079.999756
C317.056641,2066.416016 304.506073,2053.136963 292.739014,2039.196167
C241.077148,1977.993042 196.026291,1912.221802 157.290771,1842.110718
C118.251434,1771.449707 86.347641,1697.732544 61.760971,1620.828613
C44.837513,1567.894409 31.523199,1514.087402 21.943558,1459.353882
C15.361381,1421.746460 10.071630,1383.973022 7.615459,1345.841919
C7.018379,1336.572510 6.408852,1327.301758 5.592262,1318.050293
C5.403334,1315.909912 4.341088,1313.846558 2.840354,1311.873901
C2.000000,1272.624878 2.000000,1233.249756 2.696669,1192.739746
C3.783891,1185.246948 4.191141,1178.889771 4.456792,1172.526733
C4.462687,1172.385498 2.854996,1172.177124 2.000000,1172.000000
C2.000000,1167.258911 2.000000,1162.517700 2.000000,1156.888306
C4.936373,1159.359985 5.794058,1156.666138 6.088929,1154.342651
C6.797827,1148.756470 7.224460,1143.130981 7.645359,1137.512329
C10.648326,1097.425903 16.235693,1057.699341 23.362127,1018.139221
C34.644413,955.509277 50.995865,894.258301 71.820473,834.158081
C92.776169,773.679565 118.683922,715.390991 148.695190,658.878784
C179.360336,601.135254 214.260880,546.072327 254.140381,494.299805
C274.620392,467.712158 296.527496,442.210358 318.166016,416.535034
C326.916168,406.152496 336.670746,396.616455 346.819336,387.298340
C349.111511,397.039673 350.702759,406.157318 351.959839,415.320801
C354.039307,430.478943 347.020142,445.613251 351.698639,460.841003
C352.030396,461.920807 351.116547,463.384277 350.778839,464.668579
C347.058563,478.817993 346.556641,492.851044 352.077698,507.275665
C288.410614,585.358521 235.344467,669.326538 193.521423,760.365295
C126.249481,906.800049 91.659683,1060.733765 88.462120,1221.678833
C86.775948,1306.550293 94.636253,1390.659058 111.634445,1473.912476
C141.932693,1622.306519 199.339981,1759.333130 283.605133,1885.092407
C298.609344,1907.485229 314.815765,1929.072510 330.144287,1951.775146
C329.368286,1953.753174 328.206390,1955.273804 328.571472,1956.177979
C332.204346,1965.176758 325.094452,1975.258179 331.436707,1983.996216
C332.000854,1984.773682 331.152161,1986.519043 331.084595,1987.821411
C330.446564,2000.127197 326.730957,2012.421387 331.588593,2024.754395
C332.597107,2027.314941 331.849121,2030.640747 331.636078,2033.593994
C331.187073,2039.818726 330.346924,2046.020508 330.049316,2052.250000
C329.607788,2061.491455 329.497742,2070.749023 329.246643,2079.999756
z"/>
<path fill="#FD2B4F" opacity="1.000000" stroke="none"
d="
M1329.062744,2.000000
C1331.154907,5.395327 1331.152710,5.423922 1334.061523,5.645791
C1345.856567,6.545444 1357.681641,7.150996 1369.441406,8.383907
C1383.790894,9.888351 1398.085693,11.913866 1413.202148,14.792110
C1414.666626,32.711697 1415.932007,49.558369 1415.786743,66.392868
C1415.692139,77.369125 1415.896729,88.638809 1410.759399,99.196487
C1365.071655,93.011742 1320.113159,88.816185 1274.882202,87.850662
C1265.919922,87.659348 1256.959106,87.391411 1247.998047,86.333542
C1248.799072,75.686798 1249.405029,65.844688 1250.443115,56.048374
C1252.278687,38.724487 1255.235596,21.364594 1246.212402,4.940386
C1245.782349,4.157671 1246.054688,2.988963 1246.000000,2.000000
C1273.375122,2.000000 1300.750244,2.000000 1329.062744,2.000000
z"/>
<path fill="#FE354D" opacity="1.000000" stroke="none"
d="
M1245.062744,2.000000
C1246.054688,2.988963 1245.782349,4.157671 1246.212402,4.940386
C1255.235596,21.364594 1252.278687,38.724487 1250.443115,56.048374
C1249.405029,65.844688 1248.799072,75.686798 1247.077271,86.517754
C1211.969727,88.932030 1177.771362,90.099472 1143.602539,91.846756
C1127.705322,92.659691 1111.865723,94.598114 1095.994629,95.169136
C1095.326050,86.458298 1094.384521,78.615601 1094.075439,70.748077
C1093.741943,62.257504 1093.748291,53.734406 1094.061401,45.242275
C1094.485352,33.744526 1095.342529,22.262749 1096.013794,10.774117
C1114.907104,9.069780 1133.805298,7.414357 1152.687378,5.593789
C1154.836182,5.386602 1156.895752,4.252493 1158.498291,2.774093
C1186.708496,2.000000 1215.416870,2.000000 1245.062744,2.000000
z"/>
<path fill="#FD394C" opacity="1.000000" stroke="none"
d="
M1144.929321,2482.000000
C1145.452026,2478.209473 1142.249756,2478.675537 1140.452515,2478.470947
C1133.209839,2477.647217 1125.906372,2477.318115 1118.684082,2476.362061
C1086.152588,2472.055908 1053.641235,2467.597900 1020.548401,2461.732910
C1020.201294,2432.329346 1019.038818,2404.386963 1020.798401,2376.385742
C1046.574341,2380.301758 1071.468262,2384.903809 1096.542603,2388.083008
C1123.607178,2391.513916 1150.840332,2393.614746 1177.999512,2397.110840
C1177.611816,2400.520264 1177.691772,2403.297852 1176.759033,2405.678467
C1171.955933,2417.938232 1173.241089,2430.170654 1176.160400,2442.541748
C1176.962402,2445.940430 1178.116089,2450.099609 1176.900391,2452.976562
C1172.930420,2462.370850 1172.538696,2472.150391 1172.000000,2482.000000
C1163.286255,2482.000000 1154.572388,2482.000000 1144.929321,2482.000000
z"/>
<path fill="#FE304F" opacity="1.000000" stroke="none"
d="
M1172.937256,2482.000000
C1172.538696,2472.150391 1172.930420,2462.370850 1176.900391,2452.976562
C1178.116089,2450.099609 1176.962402,2445.940430 1176.160400,2442.541748
C1173.241089,2430.170654 1171.955933,2417.938232 1176.759033,2405.678467
C1177.691772,2403.297852 1177.611816,2400.520264 1178.914795,2396.979980
C1203.020508,2396.593018 1226.212036,2397.644043 1249.400146,2397.574951
C1273.576660,2397.503174 1297.750000,2396.351562 1321.944946,2396.591064
C1321.475098,2404.433838 1319.269287,2411.799805 1320.912476,2418.173584
C1323.160034,2426.891846 1321.982422,2435.237061 1321.923340,2443.743896
C1321.896240,2447.656738 1320.806519,2451.571289 1320.874023,2455.473389
C1321.026978,2464.319580 1321.598633,2473.158203 1322.000000,2482.000000
C1272.624878,2482.000000 1223.249756,2482.000000 1172.937256,2482.000000
z"/>
<path fill="#FD2750" opacity="1.000000" stroke="none"
d="
M1322.916138,2482.000000
C1321.598633,2473.158203 1321.026978,2464.319580 1320.874023,2455.473389
C1320.806519,2451.571289 1321.896240,2447.656738 1321.923340,2443.743896
C1321.982422,2435.237061 1323.160034,2426.891846 1320.912476,2418.173584
C1319.269287,2411.799805 1321.475098,2404.433838 1322.672119,2396.228027
C1367.118774,2392.044434 1410.483154,2386.102783 1453.778076,2379.750977
C1454.581421,2384.850098 1455.577515,2389.028809 1455.946533,2393.262207
C1456.789307,2402.932617 1457.568604,2412.621094 1457.905762,2422.318604
C1458.157104,2429.546875 1459.443726,2437.461426 1457.050049,2443.866943
C1454.363892,2451.054932 1453.953247,2457.795410 1454.301392,2464.999023
C1426.410522,2468.776123 1398.529907,2472.632324 1370.622192,2476.279785
C1362.792847,2477.302979 1354.873779,2477.624268 1347.019287,2478.481201
C1345.551147,2478.641357 1344.194214,2479.820801 1343.393066,2481.265137
C1337.277466,2482.000000 1330.554810,2482.000000 1322.916138,2482.000000
z"/>
<path fill="#111726" opacity="1.000000" stroke="none"
d="
M2.000000,1172.908203
C2.854996,1172.177124 4.462687,1172.385498 4.456792,1172.526733
C4.191141,1178.889771 3.783891,1185.246948 2.696669,1191.802368
C2.000000,1185.938843 2.000000,1179.877563 2.000000,1172.908203
z"/>
<path fill="#FD5843" opacity="1.000000" stroke="none"
d="
M352.295471,506.658752
C346.556641,492.851044 347.058563,478.817993 350.778839,464.668579
C351.116547,463.384277 352.030396,461.920807 351.698639,460.841003
C347.020142,445.613251 354.039307,430.478943 351.959839,415.320801
C350.702759,406.157318 349.111511,397.039673 347.449158,386.954163
C348.766235,383.872528 350.056000,381.493774 351.878967,379.639160
C415.925385,314.480255 485.335510,255.803986 563.398560,206.183746
C569.218628,209.227905 571.037903,212.937622 568.957886,218.201599
C566.186829,225.214447 566.327942,232.258118 568.689331,239.435516
C569.473450,241.818924 569.378967,244.587112 569.243042,247.155945
C569.017273,251.420425 568.140991,255.659927 568.053833,259.920502
C567.772949,273.652466 570.280884,287.473389 566.584961,301.114685
C566.366821,301.919708 567.879028,303.193573 568.194397,304.815002
C505.921600,350.333984 448.781769,400.660156 396.817505,456.793304
C381.682434,473.142609 367.120239,490.022217 352.295471,506.658752
z"/>
<path fill="#FD4E47" opacity="1.000000" stroke="none"
d="
M568.585999,304.249237
C567.879028,303.193573 566.366821,301.919708 566.584961,301.114685
C570.280884,287.473389 567.772949,273.652466 568.053833,259.920502
C568.140991,255.659927 569.017273,251.420425 569.243042,247.155945
C569.378967,244.587112 569.473450,241.818924 568.689331,239.435516
C566.327942,232.258118 566.186829,225.214447 568.957886,218.201599
C571.037903,212.937622 569.218628,209.227905 564.059692,206.044830
C572.727356,199.446106 581.890991,192.889832 591.455200,186.981873
C648.601501,151.681488 708.056335,120.873268 771.135132,96.496933
C772.619385,105.286263 773.718445,113.192955 773.899719,121.120636
C774.125427,130.992645 774.821960,141.104767 773.079834,150.707703
C770.877075,162.849945 772.948120,173.603149 777.916504,184.683075
C718.252808,211.524277 661.518799,242.315765 607.511963,278.085876
C594.478088,286.718567 581.558289,295.523499 568.585999,304.249237
z"/>
<path fill="#FD4449" opacity="1.000000" stroke="none"
d="
M778.408386,184.169250
C772.948120,173.603149 770.877075,162.849945 773.079834,150.707703
C774.821960,141.104767 774.125427,130.992645 773.899719,121.120636
C773.718445,113.192955 772.619385,105.286263 771.887207,96.423393
C790.909973,87.901482 809.630737,79.265816 829.109192,72.977303
C866.958191,60.757950 905.230347,49.848984 943.338196,38.433754
C944.916931,37.960854 946.606445,37.857994 949.092712,38.511047
C950.955872,50.275467 951.446899,61.196079 953.177063,71.916702
C954.258545,78.617790 954.122559,84.451302 949.379639,89.509445
C947.113892,91.925690 946.649414,94.198669 947.378784,97.554909
C949.268311,106.248917 950.501343,115.085594 951.341187,124.241501
C909.276917,135.853394 868.375549,148.669510 828.369080,164.223862
C811.656860,170.721512 795.058533,177.511948 778.408386,184.169250
z"/>
<path fill="#FD3D4B" opacity="1.000000" stroke="none"
d="
M951.996399,123.865334
C950.501343,115.085594 949.268311,106.248917 947.378784,97.554909
C946.649414,94.198669 947.113892,91.925690 949.379639,89.509445
C954.122559,84.451302 954.258545,78.617790 953.177063,71.916702
C951.446899,61.196079 950.955872,50.275467 949.907776,38.495461
C969.464844,33.085560 988.973267,28.209772 1008.669189,24.271704
C1027.440552,20.518517 1046.388062,17.622320 1065.296387,14.588613
C1074.967529,13.036946 1084.722656,12.009069 1095.226807,10.759282
C1095.342529,22.262749 1094.485352,33.744526 1094.061401,45.242275
C1093.748291,53.734406 1093.741943,62.257504 1094.075439,70.748077
C1094.384521,78.615601 1095.326050,86.458298 1095.269531,95.504791
C1064.811768,101.782570 1035.014771,106.551987 1005.357117,112.070274
C987.458313,115.400620 969.776001,119.894356 951.996399,123.865334
z"/>
<path fill="#FE5345" opacity="1.000000" stroke="none"
d="
M659.839844,2336.520508
C649.016357,2331.002197 637.853210,2326.055908 627.428223,2319.866211
C582.676025,2293.294678 538.868896,2265.256348 497.402283,2233.714600
C488.968445,2227.299561 480.738647,2220.616211 472.323059,2212.748535
C472.497803,2210.835693 472.882416,2210.254639 473.013123,2209.621338
C475.315247,2198.465088 471.920898,2186.470947 477.984131,2175.864990
C476.047424,2154.467529 482.650940,2132.597900 474.373993,2111.514648
C473.574524,2109.478516 474.074158,2106.932129 474.551453,2104.885742
C512.278076,2137.957031 550.918823,2168.899414 592.145081,2196.422852
C614.526550,2211.364990 635.941956,2227.859131 659.997437,2241.080811
C660.670776,2249.479492 661.500549,2257.026611 661.963074,2264.596191
C662.749268,2277.463135 663.659729,2290.340088 663.812256,2303.220947
C663.893433,2310.079834 662.561646,2316.964355 661.739685,2323.821777
C661.230530,2328.069580 660.481384,2332.288574 659.839844,2336.520508
z"/>
<path fill="#FE5B43" opacity="1.000000" stroke="none"
d="
M473.970764,2104.622559
C474.074158,2106.932129 473.574524,2109.478516 474.373993,2111.514648
C482.650940,2132.597900 476.047424,2154.467529 477.984131,2175.864990
C471.920898,2186.470947 475.315247,2198.465088 473.013123,2209.621338
C472.882416,2210.254639 472.497803,2210.835693 471.879852,2212.288086
C453.947357,2198.531006 435.849579,2184.497559 418.914001,2169.179443
C397.016846,2149.373779 375.889771,2128.703857 354.754700,2108.074463
C345.990631,2099.520020 338.064240,2090.107910 329.501770,2080.542725
C329.497742,2070.749023 329.607788,2061.491455 330.049316,2052.250000
C330.346924,2046.020508 331.187073,2039.818726 331.636078,2033.593994
C331.849121,2030.640747 332.597107,2027.314941 331.588593,2024.754395
C326.730957,2012.421387 330.446564,2000.127197 331.084595,1987.821411
C331.152161,1986.519043 332.000854,1984.773682 331.436707,1983.996216
C325.094452,1975.258179 332.204346,1965.176758 328.571472,1956.177979
C328.206390,1955.273804 329.368286,1953.753174 330.713379,1952.173584
C360.199677,1988.093140 390.371094,2022.983643 423.096802,2055.587646
C439.779816,2072.208496 456.995758,2088.294678 473.970764,2104.622559
z"/>
<path fill="#FD4A48" opacity="1.000000" stroke="none"
d="
M660.245667,2337.048828
C660.481384,2332.288574 661.230530,2328.069580 661.739685,2323.821777
C662.561646,2316.964355 663.893433,2310.079834 663.812256,2303.220947
C663.659729,2290.340088 662.749268,2277.463135 661.963074,2264.596191
C661.500549,2257.026611 660.670776,2249.479492 660.676819,2241.211914
C707.186646,2267.078613 754.482605,2290.716064 803.502991,2310.861328
C817.633301,2316.668701 831.822876,2322.331299 845.989258,2328.935303
C845.614258,2345.645020 845.523987,2361.480225 840.857300,2376.891602
C839.839905,2380.251709 839.015564,2385.068359 840.636963,2387.574463
C846.152283,2396.099609 843.788452,2403.249268 838.863953,2410.620605
C838.171448,2411.657227 838.182434,2413.163818 837.867798,2414.452881
C822.536743,2409.033447 807.088379,2403.922363 791.896851,2398.136475
C748.015625,2381.423828 705.744995,2361.139404 663.980896,2339.765869
C662.805664,2339.164307 661.758423,2338.312500 660.245667,2337.048828
z"/>
<path fill="#FD414A" opacity="1.000000" stroke="none"
d="
M838.378052,2414.957031
C838.182434,2413.163818 838.171448,2411.657227 838.863953,2410.620605
C843.788452,2403.249268 846.152283,2396.099609 840.636963,2387.574463
C839.015564,2385.068359 839.839905,2380.251709 840.857300,2376.891602
C845.523987,2361.480225 845.614258,2345.645020 846.744873,2328.954102
C880.444214,2338.418213 913.206848,2349.388428 946.401489,2358.842041
C970.638367,2365.744385 995.443848,2370.649658 1019.995911,2376.445068
C1019.038818,2404.386963 1020.201294,2432.329346 1019.958191,2461.219727
C1003.978516,2459.171875 987.937622,2456.534180 972.065918,2453.111572
C926.889465,2443.370117 882.580322,2430.489990 838.378052,2414.957031
z"/>
<path fill="#FD0155" opacity="1.000000" stroke="none"
d="
M2075.900391,322.875854
C2075.373047,329.351349 2074.552490,335.230011 2073.778809,341.114838
C2073.195801,345.550232 2072.672607,349.993500 2072.047607,355.040344
C2069.009521,354.874359 2064.850586,353.031799 2062.831055,358.350494
C2060.073975,365.611786 2065.993652,368.276642 2069.454346,371.716705
C2069.454346,372.983826 2069.738037,373.904999 2069.412598,374.247559
C2062.122070,381.922882 2062.773438,389.134186 2069.638428,396.963745
C2074.961914,403.035431 2069.418701,409.502869 2068.675049,415.748596
C2068.180664,419.901825 2066.549561,423.919678 2064.628418,428.515320
C2060.484619,425.789642 2057.117188,422.569153 2053.797119,419.300537
C1977.195312,343.888367 1891.729736,280.400085 1797.524536,228.641983
C1759.367310,207.677673 1719.990845,189.289185 1679.997559,171.621979
C1681.336304,152.104416 1682.968262,133.457199 1683.564331,114.776932
C1683.908936,103.978073 1682.583618,93.125916 1682.009888,82.297729
C1704.468994,91.377357 1727.213379,99.815414 1749.337158,109.649597
C1837.257935,148.730927 1919.572388,197.446091 1995.772095,256.183319
C2023.122681,277.266205 2049.050293,300.195190 2075.900391,322.875854
z"/>
<path fill="#FD1E51" opacity="1.000000" stroke="none"
d="
M1411.654297,99.205811
C1415.896729,88.638809 1415.692139,77.369125 1415.786743,66.392868
C1415.932007,49.558369 1414.666626,32.711697 1413.999878,14.987562
C1432.319946,16.858213 1450.818604,18.759012 1468.921631,22.549688
C1502.300781,29.539127 1535.447754,37.638149 1569.348145,46.577469
C1570.998779,62.309551 1567.693237,76.806503 1571.695679,91.289131
C1573.305176,97.112778 1572.783813,103.855881 1569.305542,109.689880
C1568.507080,111.029327 1568.292358,112.849648 1568.221680,114.466690
C1567.949707,120.681519 1567.845459,126.903702 1566.871338,132.991791
C1555.177734,129.928696 1544.303467,126.951164 1533.402710,124.073380
C1493.290771,113.483772 1452.558472,105.921730 1411.654297,99.205811
z"/>
<path fill="#FD0D53" opacity="1.000000" stroke="none"
d="
M1567.678589,133.123138
C1567.845459,126.903702 1567.949707,120.681519 1568.221680,114.466690
C1568.292358,112.849648 1568.507080,111.029327 1569.305542,109.689880
C1572.783813,103.855881 1573.305176,97.112778 1571.695679,91.289131
C1567.693237,76.806503 1570.998779,62.309551 1570.004883,46.952065
C1587.538086,51.039959 1605.227905,55.573128 1622.562744,61.200005
C1642.080688,67.535522 1661.278564,74.856789 1681.312744,82.023514
C1682.583618,93.125916 1683.908936,103.978073 1683.564331,114.776932
C1682.968262,133.457199 1681.336304,152.104416 1679.263672,171.466797
C1673.169434,170.057678 1668.030029,167.717163 1662.718018,165.874619
C1631.060303,154.893616 1599.362793,144.027695 1567.678589,133.123138
z"/>
<path fill="#FD1C52" opacity="1.000000" stroke="none"
d="
M1455.144531,2464.862793
C1453.953247,2457.795410 1454.363892,2451.054932 1457.050049,2443.866943
C1459.443726,2437.461426 1458.157104,2429.546875 1457.905762,2422.318604
C1457.568604,2412.621094 1456.789307,2402.932617 1455.946533,2393.262207
C1455.577515,2389.028809 1454.581421,2384.850098 1454.471191,2379.482422
C1483.844116,2371.806641 1512.689697,2365.602051 1541.359741,2358.670654
C1559.048218,2354.394287 1576.468872,2349.010742 1594.001831,2344.965820
C1593.330078,2350.676758 1591.985107,2355.567627 1592.139038,2360.410889
C1592.497925,2371.700684 1597.676025,2382.670166 1594.467163,2394.243652
C1594.392456,2394.513428 1594.699585,2394.866699 1594.714233,2395.186523
C1594.955322,2400.392334 1595.461060,2405.606201 1595.335571,2410.804199
C1595.163330,2417.932129 1594.509277,2425.048340 1594.061768,2432.169678
C1576.573853,2437.106201 1559.249390,2442.753906 1541.560669,2446.816162
C1513.163086,2453.338135 1484.524414,2458.810791 1455.144531,2464.862793
z"/>
<path fill="#FD0D53" opacity="1.000000" stroke="none"
d="
M1594.844482,2432.142090
C1594.509277,2425.048340 1595.163330,2417.932129 1595.335571,2410.804199
C1595.461060,2405.606201 1594.955322,2400.392334 1594.714233,2395.186523
C1594.699585,2394.866699 1594.392456,2394.513428 1594.467163,2394.243652
C1597.676025,2382.670166 1592.497925,2371.700684 1592.139038,2360.410889
C1591.985107,2355.567627 1593.330078,2350.676758 1594.686768,2344.658936
C1631.596558,2329.880371 1667.813477,2316.250244 1704.030518,2302.620117
C1705.858032,2316.132568 1704.223755,2329.672607 1703.707520,2343.142334
C1703.389404,2351.443115 1706.637573,2359.420410 1702.984741,2368.367676
C1700.063599,2375.522217 1702.222534,2384.750732 1702.187012,2394.066406
C1671.780884,2405.960693 1641.329224,2416.889404 1610.832886,2427.692139
C1605.865479,2429.451660 1600.700806,2430.654297 1594.844482,2432.142090
z"/>
<path fill="#111726" opacity="1.000000" stroke="none"
d="
M1704.636475,2302.210449
C1667.813477,2316.250244 1631.596558,2329.880371 1594.694580,2343.817383
C1576.468872,2349.010742 1559.048218,2354.394287 1541.359741,2358.670654
C1512.689697,2365.602051 1483.844116,2371.806641 1454.379395,2378.586670
C1410.483154,2386.102783 1367.118774,2392.044434 1322.651855,2395.309082
C1297.750000,2396.351562 1273.576660,2397.503174 1249.400146,2397.574951
C1226.212036,2397.644043 1203.020508,2396.593018 1178.915283,2396.165039
C1150.840332,2393.614746 1123.607178,2391.513916 1096.542603,2388.083008
C1071.468262,2384.903809 1046.574341,2380.301758 1020.798401,2376.385742
C995.443848,2370.649658 970.638367,2365.744385 946.401489,2358.842041
C913.206848,2349.388428 880.444214,2338.418213 846.740845,2328.078857
C831.822876,2322.331299 817.633301,2316.668701 803.502991,2310.861328
C754.482605,2290.716064 707.186646,2267.078613 660.668091,2240.370605
C635.941956,2227.859131 614.526550,2211.364990 592.145081,2196.422852
C550.918823,2168.899414 512.278076,2137.957031 474.551453,2104.885742
C456.995758,2088.294678 439.779816,2072.208496 423.096802,2055.587646
C390.371094,2022.983643 360.199677,1988.093140 331.036163,1951.430176
C314.815765,1929.072510 298.609344,1907.485229 283.605133,1885.092407
C199.339981,1759.333130 141.932693,1622.306519 111.634445,1473.912476
C94.636253,1390.659058 86.775948,1306.550293 88.462120,1221.678833
C91.659683,1060.733765 126.249481,906.800049 193.521423,760.365295
C235.344467,669.326538 288.410614,585.358521 352.077698,507.275696
C367.120239,490.022217 381.682434,473.142609 396.817505,456.793304
C448.781769,400.660156 505.921600,350.333984 568.194397,304.815002
C581.558289,295.523499 594.478088,286.718567 607.511963,278.085876
C661.518799,242.315765 718.252808,211.524277 777.916504,184.683075
C795.058533,177.511948 811.656860,170.721512 828.369080,164.223862
C868.375549,148.669510 909.276917,135.853394 951.341187,124.241501
C969.776001,119.894356 987.458313,115.400620 1005.357117,112.070274
C1035.014771,106.551987 1064.811768,101.782570 1095.275024,96.362015
C1111.865723,94.598114 1127.705322,92.659691 1143.602539,91.846756
C1177.771362,90.099472 1211.969727,88.932030 1247.076904,87.343018
C1256.959106,87.391411 1265.919922,87.659348 1274.882202,87.850662
C1320.113159,88.816185 1365.071655,93.011742 1410.759399,99.196487
C1452.558472,105.921730 1493.290771,113.483772 1533.402710,124.073380
C1544.303467,126.951164 1555.177734,129.928696 1566.871338,132.991791
C1599.362793,144.027695 1631.060303,154.893616 1662.718018,165.874619
C1668.030029,167.717163 1673.169434,170.057678 1679.122314,172.323257
C1719.990845,189.289185 1759.367310,207.677673 1797.524536,228.641983
C1891.729736,280.400085 1977.195312,343.888367 2053.797119,419.300537
C2057.117188,422.569153 2060.484619,425.789642 2064.405762,429.431580
C2084.243408,451.019867 2104.113770,471.691101 2122.668213,493.482666
C2239.296875,630.459351 2319.840820,786.178650 2363.923584,960.643433
C2379.285156,1021.439026 2389.225586,1083.130127 2394.555176,1145.641113
C2398.809570,1195.543945 2400.011230,1245.527954 2397.438477,1295.429810
C2384.085449,1554.437256 2295.175537,1784.427856 2129.126953,1983.796509
C2013.766113,2122.305664 1872.842163,2227.512939 1707.911499,2300.435059
C1706.999023,2300.838623 1706.131104,2301.343262 1704.636475,2302.210449
M1543.000000,1475.916992
C1560.885010,1475.916992 1578.770142,1475.916992 1597.815063,1475.916992
C1597.815063,1343.276855 1597.815063,1211.767822 1597.815063,1080.258789
C1598.370850,1080.218994 1598.926636,1080.179199 1599.482544,1080.139404
C1633.850098,1211.957520 1668.217651,1343.775513 1702.546143,1475.443481
C1769.731934,1475.443481 1835.939819,1475.443481 1902.799194,1475.443481
C1911.464722,1442.042969 1920.041992,1408.918823 1928.654541,1375.803833
C1937.197021,1342.959106 1945.775513,1310.123657 1954.337402,1277.283936
C1962.983276,1244.122070 1971.464111,1210.916138 1980.320435,1177.810669
C1989.040771,1145.213989 1996.857056,1112.357666 2006.236450,1079.943237
C2006.922363,1080.078369 2007.608154,1080.213501 2008.293945,1080.348511
C2008.293945,1211.892090 2008.293945,1343.435669 2008.293945,1475.259277
C2045.304443,1475.259277 2081.697754,1475.259277 2118.277832,1475.259277
C2118.277832,1319.766602 2118.277832,1164.932129 2118.277832,1009.858276
C2048.318604,1009.858276 1978.869995,1009.858276 1909.208252,1009.858276
C1874.011719,1144.833252 1838.957642,1279.261963 1803.903564,1413.690552
C1803.296875,1413.673828 1802.690063,1413.657227 1802.083374,1413.640625
C1766.868286,1278.899902 1731.653076,1144.159302 1696.539429,1009.806763
C1625.816528,1009.806763 1556.285645,1009.806763 1486.763916,1009.806763
C1486.763916,1165.253540 1486.763916,1320.110352 1486.763916,1475.916992
C1505.179443,1475.916992 1523.089722,1475.916992 1543.000000,1475.916992
M652.718262,1192.136841
C627.059082,1131.158569 601.399841,1070.180298 575.927673,1009.646484
C540.850159,1007.638428 376.362366,1008.593018 368.720612,1010.721680
C368.720612,1165.601807 368.720612,1320.420776 368.720612,1475.558838
C405.586884,1475.558838 441.804169,1475.558838 479.239441,1475.558838
C479.239441,1471.639893 479.239471,1468.072998 479.239471,1464.505981
C479.239471,1428.844482 479.239441,1393.183105 479.239471,1357.521606
C479.239532,1266.534912 479.228546,1175.548096 479.284668,1084.561401
C479.286682,1081.310669 478.380066,1077.736694 481.084198,1073.635864
C537.977600,1208.396118 594.365112,1341.958008 650.639954,1475.253052
C671.545288,1477.250610 850.587708,1476.370483 857.275146,1474.384888
C857.275146,1319.501709 857.275146,1164.707520 857.275146,1009.877747
C820.111389,1009.877747 783.604797,1009.877747 747.287354,1009.877747
C747.018433,1010.861938 746.855103,1011.180908 746.854858,1011.499939
C746.746033,1143.139038 746.648804,1274.778076 746.538818,1406.417236
C746.538269,1407.072876 746.388184,1407.734131 746.252258,1408.381104
C746.194031,1408.657959 746.053467,1409.030518 745.840820,1409.133423
C745.570862,1409.264160 745.189697,1409.165039 744.160767,1409.165039
C713.989868,1337.451416 683.707458,1265.472534 652.718262,1192.136841
M959.001221,1009.216125
C944.459412,1009.216125 929.917542,1009.216125 913.991943,1009.216125
C917.005737,1014.547791 919.083618,1018.292542 921.224487,1022.000916
C985.469849,1133.285889 1049.688477,1244.586670 1114.062378,1355.797241
C1117.536865,1361.799561 1119.156494,1367.707397 1119.095215,1374.644775
C1118.830933,1404.632812 1118.982300,1434.624390 1118.982300,1464.614746
C1118.982300,1468.191528 1118.982178,1471.768188 1118.982178,1475.875977
C1156.657959,1475.942383 1193.117554,1475.921387 1230.004639,1475.903931
C1230.004639,1471.100220 1230.004517,1467.789429 1230.004517,1464.478516
C1230.004517,1434.821411 1230.214233,1405.161865 1229.861328,1375.508911
C1229.770020,1367.838135 1231.788940,1361.477295 1235.588745,1354.913086
C1300.632080,1242.552002 1365.488647,1130.082764 1430.359375,1017.621826
C1431.736206,1015.235107 1432.792358,1012.663391 1434.319092,1009.516113
C1391.183350,1009.516113 1349.285889,1009.516113 1306.819458,1009.516113
C1263.059082,1085.544434 1219.280884,1161.603882 1175.042847,1238.462158
C1172.418457,1234.160156 1170.285034,1230.816528 1168.299927,1227.386841
C1127.610229,1157.086060 1086.880249,1086.808472 1046.377808,1016.399963
C1043.363647,1011.160278 1040.141357,1008.979919 1033.975464,1009.070435
C1009.655273,1009.427551 985.326599,1009.216614 959.001221,1009.216125
z"/>
<path fill="#FEFEFE" opacity="1.000000" stroke="none"
d="
M1542.000000,1475.916992
C1523.089722,1475.916992 1505.179443,1475.916992 1486.763916,1475.916992
C1486.763916,1320.110352 1486.763916,1165.253540 1486.763916,1009.806763
C1556.285645,1009.806763 1625.816528,1009.806763 1696.539429,1009.806763
C1731.653076,1144.159302 1766.868286,1278.899902 1802.083374,1413.640625
C1802.690063,1413.657227 1803.296875,1413.673828 1803.903564,1413.690552
C1838.957642,1279.261963 1874.011719,1144.833252 1909.208252,1009.858276
C1978.869995,1009.858276 2048.318604,1009.858276 2118.277832,1009.858276
C2118.277832,1164.932129 2118.277832,1319.766602 2118.277832,1475.259277
C2081.697754,1475.259277 2045.304443,1475.259277 2008.293945,1475.259277
C2008.293945,1343.435669 2008.293945,1211.892090 2008.293945,1080.348511
C2007.608154,1080.213501 2006.922363,1080.078369 2006.236450,1079.943237
C1996.857056,1112.357666 1989.040771,1145.213989 1980.320435,1177.810669
C1971.464111,1210.916138 1962.983276,1244.122070 1954.337402,1277.283936
C1945.775513,1310.123657 1937.197021,1342.959106 1928.654541,1375.803833
C1920.041992,1408.918823 1911.464722,1442.042969 1902.799194,1475.443481
C1835.939819,1475.443481 1769.731934,1475.443481 1702.546143,1475.443481
C1668.217651,1343.775513 1633.850098,1211.957520 1599.482544,1080.139404
C1598.926636,1080.179199 1598.370850,1080.218994 1597.815063,1080.258789
C1597.815063,1211.767822 1597.815063,1343.276855 1597.815063,1475.916992
C1578.770142,1475.916992 1560.885010,1475.916992 1542.000000,1475.916992
z"/>
<path fill="#FDFDFD" opacity="1.000000" stroke="none"
d="
M653.071655,1192.815308
C683.707458,1265.472534 713.989868,1337.451416 744.160767,1409.165039
C745.189697,1409.165039 745.570862,1409.264160 745.840820,1409.133423
C746.053467,1409.030518 746.194031,1408.657959 746.252258,1408.381104
C746.388184,1407.734131 746.538269,1407.072876 746.538818,1406.417236
C746.648804,1274.778076 746.746033,1143.139038 746.854858,1011.499939
C746.855103,1011.180908 747.018433,1010.861938 747.287354,1009.877747
C783.604797,1009.877747 820.111389,1009.877747 857.275146,1009.877747
C857.275146,1164.707520 857.275146,1319.501709 857.275146,1474.384888
C850.587708,1476.370483 671.545288,1477.250610 650.639954,1475.253052
C594.365112,1341.958008 537.977600,1208.396118 481.084198,1073.635864
C478.380066,1077.736694 479.286682,1081.310669 479.284668,1084.561401
C479.228546,1175.548096 479.239532,1266.534912 479.239471,1357.521606
C479.239441,1393.183105 479.239471,1428.844482 479.239471,1464.505981
C479.239471,1468.072998 479.239441,1471.639893 479.239441,1475.558838
C441.804169,1475.558838 405.586884,1475.558838 368.720612,1475.558838
C368.720612,1320.420776 368.720612,1165.601807 368.720612,1010.721680
C376.362366,1008.593018 540.850159,1007.638428 575.927673,1009.646484
C601.399841,1070.180298 627.059082,1131.158569 653.071655,1192.815308
z"/>
<path fill="#FEFEFE" opacity="1.000000" stroke="none"
d="
M960.001160,1009.216187
C985.326599,1009.216614 1009.655273,1009.427551 1033.975464,1009.070435
C1040.141357,1008.979919 1043.363647,1011.160278 1046.377808,1016.399963
C1086.880249,1086.808472 1127.610229,1157.086060 1168.299927,1227.386841
C1170.285034,1230.816528 1172.418457,1234.160156 1175.042847,1238.462158
C1219.280884,1161.603882 1263.059082,1085.544434 1306.819458,1009.516113
C1349.285889,1009.516113 1391.183350,1009.516113 1434.319092,1009.516113
C1432.792358,1012.663391 1431.736206,1015.235107 1430.359375,1017.621826
C1365.488647,1130.082764 1300.632080,1242.552002 1235.588745,1354.913086
C1231.788940,1361.477295 1229.770020,1367.838135 1229.861328,1375.508911
C1230.214233,1405.161865 1230.004517,1434.821411 1230.004517,1464.478516
C1230.004517,1467.789429 1230.004639,1471.100220 1230.004639,1475.903931
C1193.117554,1475.921387 1156.657959,1475.942383 1118.982178,1475.875977
C1118.982178,1471.768188 1118.982300,1468.191528 1118.982300,1464.614746
C1118.982300,1434.624390 1118.830933,1404.632812 1119.095215,1374.644775
C1119.156494,1367.707397 1117.536865,1361.799561 1114.062378,1355.797241
C1049.688477,1244.586670 985.469849,1133.285889 921.224487,1022.000916
C919.083618,1018.292542 917.005737,1014.547791 913.991943,1009.216125
C929.917542,1009.216125 944.459412,1009.216125 960.001160,1009.216187
z"/>
</svg>

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "nymvpn-cli"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0"
authors = ["Nym Technologies S.A."]
description = "Cli to manage VPN session via nymvpn daemon"
homepage = "https://nymvpn.net"
repository = "https://github.com/nymvpn/nymvpn-app"
[[bin]]
name = "nymvpn"
path = "src/main.rs"
[dependencies]
async-trait = "0.1.68"
clap = { version = "4.2.3", features = ["derive"] }
tokio = { version = "1.27.0", features = ["rt-multi-thread", "macros", "signal"] }
validator = { version = "0.16.0", features = ["derive"] }
nymvpn-controller = {path = "../nymvpn-controller"}
nymvpn-types = {path = "../nymvpn-types"}
thiserror = "1.0.40"
tonic = "0.9.2"
dialoguer = { version = "0.10.4", features = ["fuzzy-select"] }
indicatif = "0.17.3"
tokio-stream = { version = "0.1.12", features = ["sync"] }
console = "0.15.5"
+1
View File
@@ -0,0 +1 @@
../nymvpn.conf.toml
+59
View File
@@ -0,0 +1,59 @@
use std::process::ExitCode;
use async_trait::async_trait;
use clap::{Parser, Subcommand};
use console::style;
use crate::commands::{
connect::Connect, disconnect::Disconnect, error::CliError, locations::ListLocations,
sign_in::SignIn, sign_out::SignOut, status::Status,
};
#[async_trait]
pub trait RunCommand {
async fn run(self) -> Result<(), CliError>;
}
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Sign in to your https://nymvpn.net account
SignIn(SignIn),
/// Sign out current device
SignOut(SignOut),
/// Current VPN status
Status(Status),
/// Available locations for VPN
Locations(ListLocations),
/// Connect VPN
Connect(Connect),
/// Disconnect VPN
Disconnect(Disconnect),
}
impl Cli {
pub async fn run(self) -> ExitCode {
let output = match self.command {
Commands::SignIn(sign_in) => sign_in.run().await,
Commands::SignOut(sign_out) => sign_out.run().await,
Commands::Locations(list_locations) => list_locations.run().await,
Commands::Connect(connect) => connect.run().await,
Commands::Disconnect(disconnect) => disconnect.run().await,
Commands::Status(status) => status.run().await,
};
match output {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}", style(e).for_stderr().red());
ExitCode::FAILURE
}
}
}
}
@@ -0,0 +1,156 @@
use std::time::Duration;
use clap::Args;
use console::style;
use dialoguer::{theme::ColorfulTheme, FuzzySelect};
use indicatif::{ProgressBar, ProgressStyle};
use tokio_stream::StreamExt;
use tonic::Request;
use nymvpn_types::{notification::Notification, vpn_session::VpnStatus};
use crate::cli::RunCommand;
use super::{error::CliError, locations::list_locations};
#[derive(Debug, Args)]
pub struct Connect {}
pub async fn start_signal_watch(message: String) {
tokio::spawn(async move {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install TERM signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
println!("{}", style(message).cyan());
std::process::exit(0);
});
}
#[async_trait::async_trait]
impl RunCommand for Connect {
async fn run(self) -> Result<(), CliError> {
// get locations
let locations = list_locations().await?;
let selection = FuzzySelect::with_theme(&ColorfulTheme::default())
.items(&locations)
.with_prompt("Location:")
.interact_opt()?;
if let Some(index) = selection {
// start signal watcher for user interrupts
start_signal_watch(
"You can continue to watch status using 'nymvpn status' cli or on the app.\nTo end current session use 'nymvpn disconnect' cli or the app".into(),
)
.await;
let location = locations.get(index).unwrap();
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
let mut stream = client.watch_events(()).await?.into_inner();
let vpn_status = client
.connect_vpn(Request::new(location.clone().into()))
.await
.map(|res| res.into_inner())
.map(VpnStatus::from)?;
// wait while vpn becomes active
let pb = ProgressBar::new(100);
let mut progress = 0;
let mut done = false;
pb.set_style(
ProgressStyle::with_template(
"{spinner:.blue} [{elapsed_precise}] [{bar:.cyan/blue}] {wide_msg}",
)
.unwrap(),
);
pb.set_position(progress);
pb.set_message(format!("{}", style(vpn_status.to_string()).yellow()));
pb.enable_steady_tick(Duration::from_secs(1));
while let Some(event) = stream.next().await {
match event {
Ok(event) => {
if let Some(event) = event.event {
match event {
nymvpn_controller::proto::daemon_event::Event::VpnStatus(
vpn_status,
) => {
let vpn_status: VpnStatus = vpn_status.into();
progress = match vpn_status {
VpnStatus::Accepted(_) => 25,
VpnStatus::Connected(_, _) => {
done = true;
100
}
VpnStatus::Connecting(_) => 95,
VpnStatus::Disconnected => {
done = true;
0
}
VpnStatus::Disconnecting(_) => progress,
VpnStatus::ServerCreated(_) => 50,
VpnStatus::ServerRunning(_) => 75,
VpnStatus::ServerReady(_) => 80,
};
pb.set_position(progress);
pb.set_message(format!(
"{}",
style(vpn_status.to_string()).yellow()
));
}
nymvpn_controller::proto::daemon_event::Event::Notification(
notification,
) => {
let id = notification.id.clone();
if let Ok(notification) = Notification::try_from(notification) {
pb.set_position(0);
pb.set_message(format!(
"{}",
style(notification.message).red(),
));
}
client.ack_notification(id).await?;
done = true;
}
}
}
}
Err(err) => Err(err)?,
}
if done {
break;
}
}
pb.finish();
}
Ok(())
}
}
@@ -0,0 +1,28 @@
use clap::Args;
use console::style;
use crate::cli::RunCommand;
use super::error::CliError;
#[derive(Debug, Args)]
pub struct Disconnect {}
#[async_trait::async_trait]
impl RunCommand for Disconnect {
async fn run(self) -> Result<(), CliError> {
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
let vpn_status = client
.disconnect_vpn(())
.await
.map(|res| res.into_inner())
.map(nymvpn_types::vpn_session::VpnStatus::from)?;
println!("{}", style(vpn_status).yellow());
Ok(())
}
}
@@ -0,0 +1,13 @@
use tonic::Status;
#[derive(Debug, thiserror::Error)]
pub enum CliError {
#[error("daemon is offline")]
DaemonUnavailable,
#[error("{}", .0.message())]
Grpc(#[from] Status),
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
InvalidArgument(String),
}
@@ -0,0 +1,47 @@
use async_trait::async_trait;
use clap::Args;
use console::style;
use validator::Validate;
use crate::cli::RunCommand;
use super::error::CliError;
#[derive(Args, Debug, Validate)]
pub struct ListLocations {}
pub async fn list_locations() -> Result<Vec<nymvpn_types::location::Location>, CliError> {
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
let locations = client.get_locations(()).await?;
Ok(locations.into_inner().into())
}
#[async_trait]
impl RunCommand for ListLocations {
async fn run(self) -> Result<(), CliError> {
let locations = list_locations().await?;
for location in locations {
if location.state.is_some() {
println!(
"{}, {}, {}",
style(location.city).white(),
style(location.state.unwrap()).white().dim(),
style(location.country).white().dim()
);
} else {
println!(
"{}, {}",
style(location.city).white(),
style(location.country).white().dim()
);
}
}
Ok(())
}
}
@@ -0,0 +1,7 @@
pub mod connect;
pub mod disconnect;
pub mod error;
pub mod locations;
pub mod sign_in;
pub mod sign_out;
pub mod status;
@@ -0,0 +1,49 @@
use async_trait::async_trait;
use clap::Args;
use console::style;
use dialoguer::{theme::ColorfulTheme, Input, Password};
use nymvpn_controller::proto::SignInRequest;
use validator::Validate;
use crate::cli::RunCommand;
use super::error::CliError;
#[derive(Args, Debug, Validate)]
pub struct SignIn {
email: Option<String>,
}
#[async_trait]
impl RunCommand for SignIn {
async fn run(self) -> Result<(), CliError> {
let email = match self.email {
Some(email) => email,
None => Input::<String>::with_theme(&ColorfulTheme::default())
.with_prompt("Email:")
.interact_text()?,
};
if !validator::validate_email(&email) {
return Err(CliError::InvalidArgument(format!(
"\"{email}\" is not a valid email"
)));
}
let password = Password::with_theme(&ColorfulTheme::default())
.with_prompt("Password:")
.interact()?;
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
client
.account_sign_in(SignInRequest { email, password })
.await?;
println!("{}", style("Successfully signed in").yellow());
Ok(())
}
}
@@ -0,0 +1,25 @@
use async_trait::async_trait;
use clap::Args;
use console::style;
use crate::cli::RunCommand;
use super::error::CliError;
#[derive(Args, Debug)]
pub struct SignOut;
#[async_trait]
impl RunCommand for SignOut {
async fn run(self) -> Result<(), CliError> {
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
client.account_sign_out(()).await?;
println!("{}", style("Successfully signed out").yellow());
Ok(())
}
}
@@ -0,0 +1,28 @@
use clap::Args;
use console::style;
use crate::cli::RunCommand;
use super::error::CliError;
#[derive(Debug, Args)]
pub struct Status {}
#[async_trait::async_trait]
impl RunCommand for Status {
async fn run(self) -> Result<(), CliError> {
let mut client = nymvpn_controller::new_grpc_client()
.await
.map_err(|_| CliError::DaemonUnavailable)?;
let vpn_status = client
.get_vpn_status(())
.await
.map(|res| res.into_inner())
.map(nymvpn_types::vpn_session::VpnStatus::from)?;
println!("{}", style(vpn_status).yellow());
Ok(())
}
}
+10
View File
@@ -0,0 +1,10 @@
pub mod cli;
pub mod commands;
use std::process::ExitCode;
use clap::Parser;
#[tokio::main]
async fn main() -> ExitCode {
cli::Cli::parse().run().await
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "nymvpn-config"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0"
authors = ["Nym Technologies S.A."]
homepage = "https://nymvpn.net"
repository = "https://github.com/nymvpn/nymvpn-app"
[dependencies]
figment = { version = "0.10.8", features = ["env", "toml"] }
once_cell = "1.17.1"
serde = { version = "1.0.160", features = ["derive"] }
thiserror = "1.0.40"
tokio = { version = "1.27.0", features = ["fs"] }
[build-dependencies]
toml = "0.7.3"
serde = "1.0.160"
+25
View File
@@ -0,0 +1,25 @@
use std::error::Error;
use serde::Deserialize;
#[derive(Deserialize)]
struct Package {
version: String,
}
#[derive(Deserialize)]
struct CargoToml {
package: Package,
}
fn main() -> Result<(), Box<dyn Error>> {
let path = "../nymvpn-packages/Cargo.toml";
println!("cargo:rerun-if-changed={path}");
let cargo_toml: CargoToml = toml::from_str(&std::fs::read_to_string(path)?)?;
println!(
"cargo:rustc-env=NYMVPN_VERSION={}",
cargo_toml.package.version
);
Ok(())
}
+160
View File
@@ -0,0 +1,160 @@
#[cfg(unix)]
use std::str::FromStr;
use std::{
net::{IpAddr, Ipv4Addr},
path::{Path, PathBuf},
};
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to create dir {0}: {1}")]
CreateDirError(PathBuf, std::io::Error),
#[error("failed to set permissions ({0:?}) on ({1}): {2:?}")]
DirPermissionError(std::fs::Permissions, PathBuf, std::io::Error),
}
static CONFIG: OnceCell<Config> = OnceCell::new();
static CONFIG_DIR: OnceCell<PathBuf> = OnceCell::new();
static LOG_DIR: OnceCell<PathBuf> = OnceCell::new();
static SOCKET_PATH: OnceCell<PathBuf> = OnceCell::new();
const CONFIG_FILENAME: &str = "nymvpn.conf.toml";
pub fn config() -> &'static Config {
#[cfg(windows)]
let program_data_path = PathBuf::from(std::env::var("ProgramData").unwrap_or(
std::env::var("PROGRAMDATA").expect("missing ProgramData and PROGRAMDATA env var"),
));
let config_dir = CONFIG_DIR.get_or_init(|| {
#[cfg(unix)]
return PathBuf::from("/etc/nymvpn");
#[cfg(windows)]
{
return program_data_path.join("nymvpn");
}
});
let _ = LOG_DIR.get_or_init(|| {
#[cfg(unix)]
return PathBuf::from("/var/log/nymvpn");
#[cfg(windows)]
return program_data_path.join("nymvpn").join("log");
});
let _ = SOCKET_PATH.get_or_init(|| {
#[cfg(unix)]
return PathBuf::from("/var/run/nymvpn.sock");
#[cfg(windows)]
return PathBuf::from("//./pipe/nymvpn");
});
CONFIG.get_or_init(|| {
Figment::from(Serialized::defaults(Config::default()))
.merge(Toml::file(PathBuf::from(config_dir).join(CONFIG_FILENAME)))
.merge(Toml::file(CONFIG_FILENAME))
.merge(Env::prefixed("NYMVPN_"))
.extract()
.unwrap()
})
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
config_dir: PathBuf,
log_dir: PathBuf,
// todo: non string types for grpc and rest api?
grpc_api_host_port: String,
socket_path: PathBuf,
daemon_log_filename: String,
allowed_endpoint_ipv4: IpAddr,
license_file_path: Option<PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Self {
config_dir: CONFIG_DIR.get().unwrap().into(),
log_dir: LOG_DIR.get().unwrap().into(),
grpc_api_host_port: "grpcs://api.nymvpn.net:44444".into(),
socket_path: SOCKET_PATH.get().unwrap().into(),
daemon_log_filename: "nymvpn-daemon.log".into(),
// IP of api.nymvpn.net
allowed_endpoint_ipv4: IpAddr::V4(Ipv4Addr::new(168, 220, 80, 137)),
license_file_path: None,
}
}
}
impl Config {
pub fn db_dir(&self) -> PathBuf {
self.config_dir.join("db")
}
pub fn db_url(&self) -> String {
format!("sqlite://{}/nymvpn.db?mode=rwc", self.db_dir().display())
}
pub fn grpc_api_host_port(&self) -> &str {
&self.grpc_api_host_port
}
pub fn allowed_endpoint_ipv4(&self) -> &IpAddr {
&self.allowed_endpoint_ipv4
}
pub fn log_dir(&self) -> &Path {
self.log_dir.as_path()
}
pub fn daemon_log_filename(&self) -> &str {
&self.daemon_log_filename
}
pub fn daemon_log_file_full_path(&self) -> PathBuf {
self.log_dir().join(self.daemon_log_filename())
}
pub fn socket_path(&self) -> &Path {
return &self.socket_path;
}
pub fn version(&self) -> &'static str {
env!("NYMVPN_VERSION")
}
pub fn license_file_path(&self) -> PathBuf {
if self.license_file_path.is_some() {
return self.license_file_path.clone().unwrap();
}
#[cfg(target_os = "linux")]
return PathBuf::from_str("/opt/nymvpn/nymvpn-oss-licenses.html").unwrap();
#[cfg(target_os = "macos")]
return PathBuf::from_str(
"/Applications/nymvpn.net/Contents/Resources/nymvpn-oss-licenses.html",
)
.unwrap();
#[cfg(target_os = "windows")]
return PathBuf::from(std::env::var("PROGRAMFILES").unwrap_or(
std::env::var("ProgramFiles").expect("missing PROGRAMFILES and ProgramFiles env var"),
))
.join("nymvpn")
.join("nymvpn-oss-licenses.html");
}
pub fn icon_path(&self) -> &'static str {
#[cfg(target_os = "linux")]
return "/usr/share/icons/hicolor/32x32/apps/nymvpn.png";
#[cfg(target_os = "macos")]
return "/Applications/nymvpn.net/Contents/Resources/icon.icns";
#[cfg(target_os = "windows")]
return "";
}
}
@@ -0,0 +1,26 @@
[package]
name = "nymvpn-controller"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0"
authors = ["Nym Technologies S.A."]
homepage = "https://nymvpn.net"
repository = "https://github.com/nymvpn/nymvpn-app"
[dependencies]
futures = "0.3.28"
hyper = "0.14.26"
parity-tokio-ipc = "0.9.0"
prost = "0.11.9"
prost-types = "0.11.9"
thiserror = "1.0.40"
tokio = "1.27.0"
tonic = "0.9.2"
tower = "0.4.13"
nymvpn-config = { path = "../nymvpn-config" }
nymvpn-types = {path = "../nymvpn-types"} # grpc types to nymvpn-types conversions
chrono = { version = "0.4.24", features = ["serde"] }
[build-dependencies]
tonic-build = "0.9.2"
@@ -0,0 +1,7 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
const PROTO_PATH: &str = "proto/nymvpn-controller.proto";
tonic_build::configure().protoc_arg("--experimental_allow_proto3_optional")
.compile(&[PROTO_PATH], &["proto"])?;
println!("cargo:rerun-if-changed=proto");
Ok(())
}
@@ -0,0 +1,126 @@
syntax = "proto3";
package nymvpn.controller;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
service ControllerService {
// Locations served
rpc GetLocations(google.protobuf.Empty) returns (Locations);
rpc RecentLocations(google.protobuf.Empty) returns (Locations);
// Account
rpc IsAuthenticated(google.protobuf.Empty) returns (google.protobuf.BoolValue);
rpc AccountSignIn(SignInRequest) returns (google.protobuf.Empty);
rpc AccountSignOut(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc GetAccountInfo(google.protobuf.Empty) returns (AccountInfo);
// Control VPN
rpc ConnectVpn(Location) returns (VpnStatus);
rpc DisconnectVpn(google.protobuf.Empty) returns (VpnStatus);
rpc GetVpnStatus(google.protobuf.Empty) returns (VpnStatus);
// Notifications
rpc GetNotifications(google.protobuf.Empty) returns (Notifications);
rpc AckNotification(google.protobuf.StringValue) returns (google.protobuf.Empty);
// Versions and Updates
rpc LatestAppVersion(google.protobuf.Empty) returns (google.protobuf.StringValue);
// Events
rpc WatchEvents(google.protobuf.Empty) returns (stream DaemonEvent);
}
message SignInRequest {
string email = 1;
string password = 2;
}
message AccountInfo {
string email = 1;
uint32 balance = 2;
}
message Notifications {
repeated Notification notification = 1;
}
enum NotificationType {
SERVER_FAILED = 0;
CLIENT_FAILED = 1;
}
message Notification {
string id = 1;
NotificationType notification_type = 2;
string message = 3;
google.protobuf.Timestamp timestamp = 4;
}
message VpnStatus {
message Accepted {
Location location = 1;
};
message ServerCreated {
Location location = 1;
}
message ServerRunning {
Location location = 1;
}
message ServerReady {
Location location = 1;
}
message Connecting {
Location location = 1;
}
message Connected {
Location location = 1;
google.protobuf.Timestamp timestamp = 2;
}
message Disconnecting {
Location location = 1;
}
message Disconnected {}
oneof vpn_status {
Accepted accepted = 1;
Connecting connecting = 2;
ServerCreated server_created = 3;
ServerRunning server_running = 4;
ServerReady server_ready = 5;
Connected connected = 6;
Disconnecting disconnecting = 7;
Disconnected disconnected = 8;
}
}
message Locations {
repeated Location location = 1;
}
message Location {
string code = 1;
string country = 2;
string country_code = 3;
string city = 4;
string city_code = 5;
optional string state = 6;
optional string state_code = 7;
}
message DaemonEvent {
oneof event {
VpnStatus vpn_status = 1;
Notification notification = 2;
}
}
@@ -0,0 +1,76 @@
use std::task::{Context, Poll};
use hyper::Body;
use tonic::body::BoxBody;
use tower::{Layer, Service};
#[tonic::async_trait]
pub trait Auth: Clone + Send + Sync {
async fn is_authenticated(&self) -> bool;
}
#[derive(Debug, Clone)]
pub struct ControllerAuthLayer<P: Auth> {
auth: P,
}
impl<P: Auth> ControllerAuthLayer<P> {
pub fn new(auth: P) -> Self {
Self { auth }
}
}
const ALLOWED_UNAUTHORIZED_PATHS: [&str; 2] = [
"/nymvpn.controller.ControllerService/AccountSignIn",
"/nymvpn.controller.ControllerService/IsAuthenticated",
];
#[derive(Debug, Clone)]
pub struct ControllerAuthMiddleware<S, P: Auth> {
auth: P,
inner: S,
}
impl<S, P: Auth> Layer<S> for ControllerAuthLayer<P> {
type Service = ControllerAuthMiddleware<S, P>;
fn layer(&self, inner: S) -> Self::Service {
ControllerAuthMiddleware {
auth: self.auth.clone(),
inner,
}
}
}
impl<S, P> Service<hyper::Request<Body>> for ControllerAuthMiddleware<S, P>
where
S: Service<hyper::Request<Body>, Response = hyper::Response<BoxBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
P: Auth + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = futures::future::BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: hyper::Request<Body>) -> Self::Future {
// This is necessary because tonic internally uses `tower::buffer::Buffer`.
// See https://github.com/tower-rs/tower/issues/547#issuecomment-767629149
// for details on why this is necessary
let clone = self.inner.clone();
let mut inner = std::mem::replace(&mut self.inner, clone);
let auth = self.auth.clone();
Box::pin(async move {
if ALLOWED_UNAUTHORIZED_PATHS.contains(&req.uri().path())
|| auth.is_authenticated().await
{
return inner.call(req).await;
}
Ok(tonic::Status::unauthenticated("please sign in first").to_http())
})
}
}
@@ -0,0 +1,8 @@
impl From<crate::proto::SignInRequest> for nymvpn_types::nymvpn_server::UserCredentials {
fn from(value: crate::proto::SignInRequest) -> Self {
Self {
email: value.email,
password: value.password,
}
}
}
@@ -0,0 +1,48 @@
impl From<nymvpn_types::location::Location> for crate::proto::Location {
fn from(value: nymvpn_types::location::Location) -> Self {
Self {
code: value.code,
country: value.country,
country_code: value.country_code,
city: value.city,
city_code: value.city_code,
state: value.state,
state_code: value.state_code,
}
}
}
impl From<crate::proto::Location> for nymvpn_types::location::Location {
fn from(value: crate::proto::Location) -> Self {
Self {
code: value.code,
country: value.country,
country_code: value.country_code,
city: value.city,
city_code: value.city_code,
state: value.state,
state_code: value.state_code,
}
}
}
impl From<Vec<nymvpn_types::location::Location>> for crate::proto::Locations {
fn from(value: Vec<nymvpn_types::location::Location>) -> Self {
Self {
location: value
.into_iter()
.map(crate::proto::Location::from)
.collect(),
}
}
}
impl From<crate::proto::Locations> for Vec<nymvpn_types::location::Location> {
fn from(value: crate::proto::Locations) -> Self {
value
.location
.into_iter()
.map(nymvpn_types::location::Location::from)
.collect()
}
}
@@ -0,0 +1,4 @@
pub mod account;
pub mod location;
pub mod notification;
pub mod vpn_status;
@@ -0,0 +1,77 @@
use crate::timestamp_to_datetime_utc;
impl From<crate::proto::NotificationType> for nymvpn_types::notification::NotificationType {
fn from(value: crate::proto::NotificationType) -> Self {
match value {
crate::proto::NotificationType::ServerFailed => {
nymvpn_types::notification::NotificationType::ServerFailed
}
crate::proto::NotificationType::ClientFailed => {
nymvpn_types::notification::NotificationType::ClientFailed
}
}
}
}
impl From<nymvpn_types::notification::NotificationType> for crate::proto::NotificationType {
fn from(value: nymvpn_types::notification::NotificationType) -> Self {
match value {
nymvpn_types::notification::NotificationType::ServerFailed => {
crate::proto::NotificationType::ServerFailed
}
nymvpn_types::notification::NotificationType::ClientFailed => {
crate::proto::NotificationType::ClientFailed
}
}
}
}
impl TryFrom<crate::proto::Notification> for nymvpn_types::notification::Notification {
type Error = String;
fn try_from(value: crate::proto::Notification) -> Result<Self, Self::Error> {
Ok(Self {
id: value.id,
message: value.message,
notification_type: value.notification_type.try_into()?,
timestamp: timestamp_to_datetime_utc(value.timestamp)?,
})
}
}
impl From<nymvpn_types::notification::Notification> for crate::proto::Notification {
fn from(value: nymvpn_types::notification::Notification) -> Self {
let seconds = value.timestamp.timestamp();
let nanos = value.timestamp.timestamp_subsec_nanos();
Self {
id: value.id,
notification_type: value.notification_type.into(),
message: value.message,
timestamp: Some(prost_types::Timestamp {
seconds,
nanos: nanos as i32,
}),
}
}
}
impl From<Vec<nymvpn_types::notification::Notification>> for crate::proto::Notifications {
fn from(value: Vec<nymvpn_types::notification::Notification>) -> Self {
Self {
notification: value
.into_iter()
.map(crate::proto::Notification::from)
.collect(),
}
}
}
impl TryFrom<crate::proto::Notifications> for Vec<nymvpn_types::notification::Notification> {
type Error = String;
fn try_from(value: crate::proto::Notifications) -> Result<Self, Self::Error> {
let mut notifications = vec![];
for notification in value.notification {
notifications.push(notification.try_into()?)
}
Ok(notifications)
}
}
@@ -0,0 +1,110 @@
use crate::{datetime_utc_to_timestamp, timestamp_to_datetime_utc};
impl From<nymvpn_types::vpn_session::VpnStatus> for crate::proto::VpnStatus {
fn from(value: nymvpn_types::vpn_session::VpnStatus) -> Self {
match value {
nymvpn_types::vpn_session::VpnStatus::Accepted(location) => crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::Accepted(
crate::proto::vpn_status::Accepted {
location: Some(location.into()),
},
)),
},
nymvpn_types::vpn_session::VpnStatus::Connecting(location) => crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::Connecting(
crate::proto::vpn_status::Connecting {
location: Some(location.into()),
},
)),
},
nymvpn_types::vpn_session::VpnStatus::ServerRunning(location) => {
crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerRunning(
crate::proto::vpn_status::ServerRunning {
location: Some(location.into()),
},
)),
}
}
nymvpn_types::vpn_session::VpnStatus::ServerReady(location) => crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerReady(
crate::proto::vpn_status::ServerReady {
location: Some(location.into()),
},
)),
},
nymvpn_types::vpn_session::VpnStatus::Connected(location, connected_time) => {
crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::Connected(
crate::proto::vpn_status::Connected {
location: Some(location.into()),
timestamp: Some(datetime_utc_to_timestamp(connected_time)),
},
)),
}
}
nymvpn_types::vpn_session::VpnStatus::Disconnecting(location) => {
crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::Disconnecting(
crate::proto::vpn_status::Disconnecting {
location: Some(location.into()),
},
)),
}
}
nymvpn_types::vpn_session::VpnStatus::Disconnected => crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::Disconnected(
crate::proto::vpn_status::Disconnected {},
)),
},
nymvpn_types::vpn_session::VpnStatus::ServerCreated(location) => {
crate::proto::VpnStatus {
vpn_status: Some(crate::proto::vpn_status::VpnStatus::ServerCreated(
crate::proto::vpn_status::ServerCreated {
location: Some(location.into()),
},
)),
}
}
}
}
}
impl From<crate::proto::VpnStatus> for nymvpn_types::vpn_session::VpnStatus {
fn from(value: crate::proto::VpnStatus) -> Self {
let vpn_status = value.vpn_status.unwrap();
match vpn_status {
crate::proto::vpn_status::VpnStatus::Accepted(accepted) => {
nymvpn_types::vpn_session::VpnStatus::Accepted(accepted.location.unwrap().into())
}
crate::proto::vpn_status::VpnStatus::Connecting(connecting) => {
nymvpn_types::vpn_session::VpnStatus::Connecting(connecting.location.unwrap().into())
}
crate::proto::vpn_status::VpnStatus::ServerRunning(srun) => {
nymvpn_types::vpn_session::VpnStatus::ServerRunning(srun.location.unwrap().into())
}
crate::proto::vpn_status::VpnStatus::ServerReady(sr) => {
nymvpn_types::vpn_session::VpnStatus::ServerReady(sr.location.unwrap().into())
}
crate::proto::vpn_status::VpnStatus::Connected(connected) => {
nymvpn_types::vpn_session::VpnStatus::Connected(
connected.location.unwrap().into(),
timestamp_to_datetime_utc(connected.timestamp).unwrap(),
)
}
crate::proto::vpn_status::VpnStatus::Disconnecting(disconnecting) => {
nymvpn_types::vpn_session::VpnStatus::Disconnecting(
disconnecting.location.unwrap().into(),
)
}
crate::proto::vpn_status::VpnStatus::Disconnected(_) => {
nymvpn_types::vpn_session::VpnStatus::Disconnected
}
crate::proto::vpn_status::VpnStatus::ServerCreated(server_created) => {
nymvpn_types::vpn_session::VpnStatus::ServerCreated(
server_created.location.unwrap().into(),
)
}
}
}
}
@@ -0,0 +1,153 @@
use std::future::Future;
use std::pin::Pin;
use std::task::Context;
use std::task::Poll;
use auth::Auth;
use parity_tokio_ipc::Endpoint as IpcEndpoint;
use prost_types::Timestamp;
use thiserror::Error;
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
use tokio::io::ReadBuf;
use tonic::transport::server::Connected;
use tonic::transport::Channel;
use tonic::transport::Endpoint as TonicEndpoint;
use tonic::transport::Server;
use tonic::transport::Uri;
use tower::service_fn;
pub mod proto {
tonic::include_proto!("nymvpn.controller");
}
pub mod auth;
pub mod conversions;
use chrono::{TimeZone, Utc};
pub use proto::controller_service_server::{ControllerService, ControllerServiceServer};
use nymvpn_types::DateTimeUtc;
use crate::auth::ControllerAuthLayer;
pub type ControllerServiceClient =
proto::controller_service_client::ControllerServiceClient<Channel>;
pub type GrpcServerJoinHandle = tokio::task::JoinHandle<Result<(), ControllerError>>;
#[derive(Debug, Error)]
pub enum ControllerError {
#[error("{0}")]
TonicTransportError(tonic::transport::Error),
#[error("security attributes error {0:#?}")]
SecurityAttributesError(std::io::Error),
#[error("incoming connection error {0:#?}")]
IncomingConnectionError(std::io::Error),
}
pub async fn new_grpc_client() -> Result<ControllerServiceClient, ControllerError> {
let ipc_path = nymvpn_config::config().socket_path();
// URI is unused
let channel = TonicEndpoint::from_static("http://[::]:50051")
.connect_with_connector(service_fn(move |_: Uri| {
IpcEndpoint::connect(ipc_path.clone())
}))
.await
.map_err(ControllerError::TonicTransportError)?;
Ok(ControllerServiceClient::new(channel))
}
pub async fn spawn_grpc_server<S, P, F>(
service: S,
auth: P,
shutdown: F,
) -> std::result::Result<GrpcServerJoinHandle, ControllerError>
where
S: proto::controller_service_server::ControllerService,
F: Future<Output = ()> + Send + 'static,
P: Auth + 'static,
{
use futures::stream::TryStreamExt;
use parity_tokio_ipc::SecurityAttributes;
let socket_path = nymvpn_config::config().socket_path();
let mut endpoint = IpcEndpoint::new(socket_path.to_string_lossy().to_string());
endpoint.set_security_attributes(
SecurityAttributes::allow_everyone_create()
.map_err(ControllerError::SecurityAttributesError)?
.set_mode(0o766)
.map_err(ControllerError::SecurityAttributesError)?,
);
let incoming = endpoint
.incoming()
.map_err(ControllerError::IncomingConnectionError)?;
Ok(tokio::spawn(async move {
Server::builder()
.layer(ControllerAuthLayer::new(auth))
.add_service(ControllerServiceServer::new(service))
.serve_with_incoming_shutdown(incoming.map_ok(StreamBox), shutdown)
.await
.map_err(ControllerError::TonicTransportError)
}))
}
#[derive(Debug)]
struct StreamBox<T: AsyncRead + AsyncWrite>(pub T);
impl<T: AsyncRead + AsyncWrite> Connected for StreamBox<T> {
type ConnectInfo = Option<()>;
fn connect_info(&self) -> Self::ConnectInfo {
None
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> AsyncRead for StreamBox<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_read(cx, buf)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin> AsyncWrite for StreamBox<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut self.0).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.0).poll_shutdown(cx)
}
}
pub fn timestamp_to_datetime_utc(timestamp: Option<Timestamp>) -> Result<DateTimeUtc, String> {
let timestamp = timestamp.ok_or(format!("no timestamp"))?;
let date_time_utc = match Utc.timestamp_opt(timestamp.seconds, timestamp.nanos as u32) {
chrono::LocalResult::Single(dtu) => dtu,
chrono::LocalResult::None => Err("invalid utc time none")?,
chrono::LocalResult::Ambiguous(a, _b) => {
//Err(format!("ambiguous utc time {a} {b}"))?
a
}
};
Ok(date_time_utc)
}
pub fn datetime_utc_to_timestamp(datetime_utc: DateTimeUtc) -> Timestamp {
let seconds = datetime_utc.timestamp();
let nanos = std::cmp::max(datetime_utc.timestamp_subsec_nanos() as i32, 0);
prost_types::Timestamp { seconds, nanos }
}
+57
View File
@@ -0,0 +1,57 @@
[package]
name = "nymvpn-daemon"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0"
authors = ["Nym Technologies S.A."]
homepage = "https://nymvpn.net"
repository = "https://github.com/nymvpn/nymvpn-app"
[dependencies]
clap = "4.2.7"
async-trait = "0.1.68"
futures = "0.3.28"
hyper = "0.14.26"
thiserror = "1.0.40"
tokio = { version = "1.27.0", features = ["rt-multi-thread", "signal", "macros", "sync", "fs"] }
tokio-stream = { version = "0.1.12", features = ["sync"] }
tonic = "0.9.2"
tower = "0.4.13"
tracing = "0.1.37"
tracing-appender = "0.2.2"
tracing-subscriber = { version = "0.3.16", features = ["default", "env-filter", "tracing-log"] }
nymvpn-config = {path = "../nymvpn-config"}
nymvpn-controller = {path = "../nymvpn-controller"}
nymvpn-server = {path = "../nymvpn-server"}
nymvpn-types = {path = "../nymvpn-types"}
nymvpn-entity = {path = "../nymvpn-entity"}
nymvpn-migration = {path = "../nymvpn-migration"}
sea-orm = { version = "0.11.2", features = ["sqlx-sqlite", "runtime-tokio-rustls"] }
chrono = "0.4.24"
talpid-core = {git = "https://github.com/upvpn/mullvadvpn-app.git", rev = "2023.3.upvpn"}
talpid-types = {git = "https://github.com/upvpn/mullvadvpn-app.git", rev = "2023.3.upvpn"}
talpid-platform-metadata = {git = "https://github.com/upvpn/mullvadvpn-app.git", rev = "2023.3.upvpn"}
futures-channel = "0.3.28"
uuid = { version = "1.3.1", features = ["v4", "serde"] }
libc = "0.2"
lazy_static = "1.0"
[target.'cfg(unix)'.dependencies]
nix = "0.26.2"
[target.'cfg(windows)'.dependencies]
windows-service = "0.6.0"
[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.45.0"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Security_Authentication_Identity",
"Win32_System_Diagnostics_Debug",
"Win32_System_Kernel",
"Win32_System_Memory",
"Win32_System_Threading",
]
+1
View File
@@ -0,0 +1 @@
../nymvpn.conf.toml
@@ -0,0 +1,10 @@
pub async fn remove_old_socket_file() {
if let Err(e) = tokio::fs::remove_file(nymvpn_config::config().socket_path()).await {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::error!(
"Failed to remove old socket file {}",
nymvpn_config::config().socket_path().display()
);
}
}
}
@@ -0,0 +1,297 @@
use std::sync::Arc;
use tokio::{sync::oneshot, task::JoinHandle};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tonic::{Request, Response, Status};
use nymvpn_controller::{
proto::{AccountInfo, Locations, Notifications, SignInRequest, VpnStatus},
spawn_grpc_server, ControllerError, ControllerService,
};
use nymvpn_migration::DbErr;
use nymvpn_types::notification::Notification;
use crate::{
daemon::{DaemonCommand, DaemonCommandSender, DaemonError, EventListener},
device::handler::DeviceHandler,
shutdown::ShutdownManager,
};
pub struct ControllerServer;
impl ControllerServer {
pub async fn start(
daemon_command_sender: DaemonCommandSender,
shutdown_manager: &ShutdownManager,
device_handler: DeviceHandler,
) -> Result<ControllerServerAndEventBroadcaster, ControllerError> {
let events_subscribers =
Arc::<tokio::sync::RwLock<Vec<DaemonEventsListenerSender>>>::default();
let controller_service = ControllerServiceImpl {
daemon_command_sender,
events_subscribers: events_subscribers.clone(),
};
let handle = spawn_grpc_server(
controller_service,
device_handler,
shutdown_manager.shutdown_received_future(),
)
.await?;
let controller_server_handle = tokio::spawn(async move {
if let Err(e) = handle.await {
tracing::error!("Controller GRPC server error: {e}")
}
tracing::info!("Controller server shut down")
});
Ok(ControllerServerAndEventBroadcaster {
events_subscribers,
controller_server_handle,
})
}
}
pub struct ControllerServerAndEventBroadcaster {
pub events_subscribers: Arc<tokio::sync::RwLock<Vec<DaemonEventsListenerSender>>>,
pub controller_server_handle: JoinHandle<()>,
}
impl ControllerServerAndEventBroadcaster {
async fn notify(&self, event: nymvpn_controller::proto::DaemonEvent) {
let mut subscribers = self.events_subscribers.write().await;
subscribers.retain(|tx| tx.send(Ok(event.clone())).is_ok());
}
}
#[async_trait::async_trait]
impl EventListener for ControllerServerAndEventBroadcaster {
async fn send_vpn_status(&self, status: nymvpn_types::vpn_session::VpnStatus) {
tracing::debug!("notifying new vpn status");
self.notify(nymvpn_controller::proto::DaemonEvent {
event: Some(nymvpn_controller::proto::daemon_event::Event::VpnStatus(
nymvpn_controller::proto::VpnStatus::from(status),
)),
})
.await
}
async fn send_notification(&self, notification: Notification) {
tracing::debug!("sending new notification");
self.notify(nymvpn_controller::proto::DaemonEvent {
event: Some(nymvpn_controller::proto::daemon_event::Event::Notification(
nymvpn_controller::proto::Notification::from(notification),
)),
})
.await
}
}
pub struct ControllerServiceImpl {
daemon_command_sender: DaemonCommandSender,
events_subscribers: Arc<tokio::sync::RwLock<Vec<DaemonEventsListenerSender>>>,
}
pub type ServiceResult<T> = std::result::Result<Response<T>, Status>;
pub type VpnStatusListenerSender =
tokio::sync::mpsc::UnboundedSender<Result<nymvpn_controller::proto::VpnStatus, Status>>;
pub type VpnStatusListenerReceiver =
UnboundedReceiverStream<Result<nymvpn_controller::proto::VpnStatus, Status>>;
pub type DaemonEventsListenerSender =
tokio::sync::mpsc::UnboundedSender<Result<nymvpn_controller::proto::DaemonEvent, Status>>;
pub type DaemonEventsListenerReceiver =
UnboundedReceiverStream<Result<nymvpn_controller::proto::DaemonEvent, Status>>;
#[tonic::async_trait]
impl ControllerService for ControllerServiceImpl {
type WatchEventsStream = DaemonEventsListenerReceiver;
/// Locations served
async fn get_locations(&self, _: Request<()>) -> ServiceResult<Locations> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::ListLocations(tx))?;
self.wait_for_result(rx)
.await?
.map(|locations| Response::new(locations.into()))
.map_err(map_daemon_error)
}
async fn recent_locations(&self, _: Request<()>) -> ServiceResult<Locations> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::RecentLocations(tx))?;
self.wait_for_result(rx)
.await?
.map(|locations| Response::new(locations.into()))
.map_err(map_daemon_error)
}
/// Account
async fn is_authenticated(&self, _req: Request<()>) -> ServiceResult<bool> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::IsAuthenticated(tx))?;
self.wait_for_result(rx)
.await?
.map(Response::new)
.map_err(map_daemon_error)
}
async fn account_sign_in(&self, req: Request<SignInRequest>) -> ServiceResult<()> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::AccountSignIn(tx, req.into_inner().into()))?;
self.wait_for_result(rx)
.await?
.map(Response::new)
.map_err(map_daemon_error)
}
async fn account_sign_out(&self, _: Request<()>) -> ServiceResult<()> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::AccountSignOut(tx))?;
self.wait_for_result(rx)
.await?
.map(Response::new)
.map_err(map_daemon_error)
}
async fn get_account_info(&self, _: Request<()>) -> ServiceResult<AccountInfo> {
todo!()
}
/// Control VPN
async fn connect_vpn(
&self,
req: Request<nymvpn_controller::proto::Location>,
) -> ServiceResult<VpnStatus> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::Connect(tx, req.into_inner().into()))?;
self.wait_for_result(rx)
.await?
.map(nymvpn_controller::proto::VpnStatus::from)
.map(Response::new)
.map_err(map_daemon_error)
}
async fn disconnect_vpn(&self, _: Request<()>) -> ServiceResult<VpnStatus> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::Disconnect(tx))?;
self.wait_for_result(rx)
.await?
.map(nymvpn_controller::proto::VpnStatus::from)
.map(Response::new)
.map_err(map_daemon_error)
}
async fn get_vpn_status(&self, _: Request<()>) -> ServiceResult<VpnStatus> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::GetVpnStatus(tx))?;
self.wait_for_result(rx)
.await?
.map(nymvpn_controller::proto::VpnStatus::from)
.map(Response::new)
.map_err(map_daemon_error)
}
/// Notifications
async fn get_notifications(&self, _: Request<()>) -> ServiceResult<Notifications> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::GetNotifications(tx))?;
self.wait_for_result(rx)
.await?
.map(nymvpn_controller::proto::Notifications::from)
.map(Response::new)
.map_err(map_daemon_error)
}
async fn ack_notification(&self, id: Request<String>) -> ServiceResult<()> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::AckNotification(tx, id.into_inner()))?;
self.wait_for_result(rx)
.await?
.map(Response::new)
.map_err(map_daemon_error)
}
// Versions and Updates
async fn latest_app_version(&self, _: Request<()>) -> ServiceResult<String> {
let (tx, rx) = oneshot::channel();
self.send_command_to_daemon(DaemonCommand::LatestAppVersion(tx))?;
self.wait_for_result(rx)
.await?
.map(Response::new)
.map_err(map_daemon_error)
}
/// Event stream
async fn watch_events(&self, _: Request<()>) -> ServiceResult<Self::WatchEventsStream> {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut subscribers = self.events_subscribers.write().await;
subscribers.push(tx);
Ok(Response::new(UnboundedReceiverStream::new(rx)))
}
}
impl ControllerServiceImpl {
fn send_command_to_daemon(&self, command: DaemonCommand) -> Result<(), Status> {
self.daemon_command_sender
.send(command)
.map_err(|_| Status::internal("the daemon channel receiver has been dropped"))
}
async fn wait_for_result<T>(&self, rx: tokio::sync::oneshot::Receiver<T>) -> Result<T, Status> {
rx.await.map_err(|_| Status::internal("sender was dropped"))
}
}
fn map_db_error(_db_err: DbErr) -> Status {
Status::internal("daemon is unable to manage its database")
}
pub const SERVER_UNAVAILABLE_PLEASE_TRY_AGAIN_LATER: &str =
"server is unavailable, please try again later";
pub const VPN_SESSION_SERVICE_UNAVAILABLE: &str =
"daemon is partially up: vpn session service unavailable";
fn map_daemon_error(error: DaemonError) -> Status {
tracing::error!("{:?}", error);
match error {
DaemonError::DaemonUnavailable => Status::internal("daemon is unavailable"),
DaemonError::AnotherVpnSessionInProgress(location) => Status::failed_precondition(format!(
"cannot start a new vpn session when another is in progress (to city {})",
location.city
)),
DaemonError::InvalidOpVpnSessionInProgress(message) => Status::failed_precondition(message),
DaemonError::DbErr(db_err) => map_db_error(db_err),
DaemonError::DeviceError(device_error) => match device_error {
crate::device::DeviceError::DeviceServiceUnavailable => {
Status::internal("daemon is partially up: device service unavailable")
}
crate::device::DeviceError::Server(status) => status,
crate::device::DeviceError::Connection(_) => {
Status::unavailable(SERVER_UNAVAILABLE_PLEASE_TRY_AGAIN_LATER)
}
crate::device::DeviceError::DbErr(db_err) => map_db_error(db_err),
crate::device::DeviceError::InitError(_) => {
Status::internal("failed to initialize device")
}
},
DaemonError::VpnSessionError(vpn_session_error) => match vpn_session_error {
crate::vpn_session::handler::VpnSessionError::VpnSessionServiceDown => {
Status::internal(VPN_SESSION_SERVICE_UNAVAILABLE)
}
crate::vpn_session::handler::VpnSessionError::Connection(_) => {
Status::unavailable(SERVER_UNAVAILABLE_PLEASE_TRY_AGAIN_LATER)
}
crate::vpn_session::handler::VpnSessionError::Server(status) => status,
},
}
}
+732
View File
@@ -0,0 +1,732 @@
use std::fmt::Display;
use talpid_core::tunnel_state_machine::{TunnelCommand, TunnelStateMachineHandle};
use talpid_types::tunnel::TunnelStateTransition;
use tokio::sync::oneshot;
use nymvpn_controller::auth::Auth;
use nymvpn_migration::DbErr;
use nymvpn_types::{
location::Location,
notification::Notification,
nymvpn_server::{ClientConnected, EndSession, NewSession, UserCredentials, VpnSessionStatus},
vpn_session::VpnStatus,
};
use crate::{
controller::ControllerServerAndEventBroadcaster,
device::{handler::DeviceHandler, storage::DeviceStorage, DeviceError},
location_storage::LocationStorage,
shutdown::Shutdown,
state::DaemonState,
vpn_session::{
handler::{VpnSessionError, VpnSessionHandler},
storage::{SessionInfo, VpnSessionStorage},
},
ResponseTx,
};
pub struct Daemon {
// in memory current state of the server + client side state
state: DaemonState,
controller_server_and_event_broadcaster: ControllerServerAndEventBroadcaster,
daemon_command_sender: DaemonCommandSender,
daemon_receiver: DaemonReceiver,
device_handler: DeviceHandler,
vpn_session_storage: VpnSessionStorage,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
tunnel_state_machine_handle: TunnelStateMachineHandle,
location_storage: LocationStorage,
shutdown: Option<Shutdown>,
}
#[derive(Debug, thiserror::Error)]
pub enum DaemonError {
#[error("daemon is offline")]
DaemonUnavailable,
#[error("another vpn session in progress: {0}")]
AnotherVpnSessionInProgress(Location),
#[error("invalid op when vpn session in progress: {0}")]
InvalidOpVpnSessionInProgress(String),
#[error("")]
DbErr(#[from] DbErr),
#[error("device error: {0}")]
DeviceError(#[from] DeviceError),
#[error("vpn session error: {0}")]
VpnSessionError(#[from] VpnSessionError),
}
#[derive(Debug)]
pub enum DaemonCommand {
IsAuthenticated(ResponseTx<bool, DaemonError>),
AccountSignIn(ResponseTx<(), DaemonError>, UserCredentials),
AccountSignOut(ResponseTx<(), DaemonError>),
ListLocations(ResponseTx<Vec<Location>, DaemonError>),
RecentLocations(ResponseTx<Vec<Location>, DaemonError>),
Connect(ResponseTx<VpnStatus, DaemonError>, Location),
Disconnect(ResponseTx<VpnStatus, DaemonError>),
GetVpnStatus(ResponseTx<VpnStatus, DaemonError>),
GetNotifications(ResponseTx<Vec<Notification>, DaemonError>),
AckNotification(ResponseTx<(), DaemonError>, String),
LatestAppVersion(ResponseTx<String, DaemonError>),
}
pub type DaemonReceiver = tokio::sync::mpsc::UnboundedReceiver<DaemonEvent>;
pub type DaemonSender = tokio::sync::mpsc::UnboundedSender<DaemonEvent>;
pub struct DaemonCommandChannel {
sender: DaemonCommandSender,
receiver: DaemonReceiver,
}
impl DaemonCommandChannel {
pub fn new() -> Self {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
Self {
sender: DaemonCommandSender(tx),
receiver: rx,
}
}
pub fn sender(&self) -> DaemonCommandSender {
self.sender.clone()
}
pub fn destructure(self) -> (DaemonCommandSender, DaemonReceiver) {
(self.sender, self.receiver)
}
}
#[derive(Clone)]
pub struct DaemonCommandSender(DaemonSender);
#[derive(Clone)]
pub struct DaemonEventSender(DaemonSender);
impl DaemonCommandSender {
pub fn send(&self, command: DaemonCommand) -> Result<(), DaemonError> {
self.0
.send(DaemonEvent::Command(command))
.map_err(|_| DaemonError::DaemonUnavailable)
}
}
impl DaemonEventSender {
pub fn send(&self, event: DaemonEvent) -> Result<(), DaemonError> {
self.0
.send(event)
.map_err(|_| DaemonError::DaemonUnavailable)
}
}
impl<E> talpid_core::mpsc::Sender<E> for DaemonEventSender
where
DaemonEvent: From<E>,
{
fn send(&self, event: E) -> Result<(), talpid_core::mpsc::Error> {
self.0
.send(DaemonEvent::from(event))
.map_err(|_| talpid_core::mpsc::Error::ChannelClosed)
}
}
impl From<DaemonCommandSender> for DaemonEventSender {
fn from(dcs: DaemonCommandSender) -> Self {
Self(dcs.0)
}
}
/// All possible events that can happen during the lifetime of a Daemon
#[derive(Debug)]
pub enum DaemonEvent {
/// Command for the Daemon
Command(DaemonCommand),
/// Initiated by signals like ctrl-c, SIGINT or SIGTERM
Shutdown,
/// Vpn Session Status received from Server
VpnSessionStatus(VpnSessionStatus),
/// Tunnel has changed state.
TunnelStateTransition(TunnelStateTransition),
}
impl From<TunnelStateTransition> for DaemonEvent {
fn from(tst: TunnelStateTransition) -> Self {
DaemonEvent::TunnelStateTransition(tst)
}
}
impl Display for DaemonEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let event = match self {
DaemonEvent::Command(command) => match command {
DaemonCommand::IsAuthenticated(_) => "IsAuthenticated".into(),
DaemonCommand::AccountSignIn(_, user_creds) => {
format!("AccountSignIn: {}", user_creds.email)
}
DaemonCommand::AccountSignOut(_) => "AccountSignOut".into(),
DaemonCommand::ListLocations(_) => "ListLocations".into(),
DaemonCommand::RecentLocations(_) => "RecentLocations".into(),
DaemonCommand::Connect(_, location) => format!("Connect: {}", location.code),
DaemonCommand::Disconnect(_) => "Disconnect".into(),
DaemonCommand::GetVpnStatus(_) => "GetVpnStatus".into(),
DaemonCommand::GetNotifications(_) => "GetNotifications".into(),
DaemonCommand::AckNotification(_, id) => format!("AckNotification: {id}"),
DaemonCommand::LatestAppVersion(_) => "LatestAppVersion".into(),
},
DaemonEvent::Shutdown => "Shutdown".into(),
DaemonEvent::VpnSessionStatus(status) => format!("VpnSessionStatus: {status}"),
DaemonEvent::TunnelStateTransition(transition) => match transition {
TunnelStateTransition::Disconnected => "TunnelStateTransition: Disconnected".into(),
TunnelStateTransition::Connecting(endpoint) => {
format!("TunnelStateTransition: {endpoint}")
}
TunnelStateTransition::Connected(endpoint) => {
format!("TunnelStateTransition: {endpoint}")
}
TunnelStateTransition::Disconnecting(action) => {
format!("TunnelStateTransition: {action:?}")
}
TunnelStateTransition::Error(e) => format!("TunnelStateTransition: {e:?}"),
},
};
write!(f, "{event}")
}
}
#[async_trait::async_trait]
pub trait EventListener {
async fn send_vpn_status(&self, status: VpnStatus);
async fn send_notification(&self, notification: Notification);
//todo: add other events
}
impl Daemon {
pub fn new(
dc: DaemonCommandChannel,
device_handler: DeviceHandler,
vpn_session_storage: VpnSessionStorage,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
controller_server_and_event_broadcaster: ControllerServerAndEventBroadcaster,
tunnel_state_machine_handle: TunnelStateMachineHandle,
location_storage: LocationStorage,
shutdown: Option<Shutdown>,
) -> Self {
let (daemon_command_sender, daemon_receiver) = dc.destructure();
Daemon {
state: DaemonState::new(),
daemon_command_sender,
daemon_receiver,
device_handler,
device_storage,
vpn_session_storage,
vpn_session_handler,
controller_server_and_event_broadcaster,
tunnel_state_machine_handle,
location_storage,
shutdown,
}
}
fn register_shutdown(&mut self) {
let mut shutdown = self.shutdown.take().unwrap();
let sender = DaemonEventSender::from(self.daemon_command_sender.clone());
tokio::spawn(async move {
shutdown.recv().await;
if let Err(e) = sender.send(DaemonEvent::Shutdown) {
//todo: Display trait
tracing::error!("failed to send shutdown event to Daemon: {e:#?}");
}
});
}
pub async fn run(mut self) {
self.register_shutdown();
while let Some(event) = self.daemon_receiver.recv().await {
if let DaemonEvent::Shutdown = event {
break;
}
self.handle_event(event).await;
}
self.handle_shutdown().await;
}
async fn handle_shutdown(mut self) {
tracing::info!("handling shutdown ...");
// if any, disconnect and end existing session
if let Err(err) = self.on_disconnect_inner("daemon shutdown".into()).await {
tracing::error!("when ending session during shutdown: {err}");
};
// wait for tunnel state machine to stop
self.tunnel_state_machine_handle.try_join().await;
if let Err(err) = self.vpn_session_handler.shutdown().await {
tracing::error!("error when vpn session handler was shutting down: {err}");
};
let ControllerServerAndEventBroadcaster {
events_subscribers,
controller_server_handle,
} = self.controller_server_and_event_broadcaster;
let mut guard = events_subscribers.write().await;
guard.clear();
drop(guard);
drop(events_subscribers);
let _ = tokio::join!(controller_server_handle);
// device handler last as controller depends on it for auth
if let Err(err) = self.device_handler.shutdown().await {
tracing::error!("error when device handler was shutting down: {err}");
};
}
async fn handle_event(&mut self, event: DaemonEvent) {
tracing::debug!("daemon event: {event}");
match event {
DaemonEvent::Command(command) => self.handle_command(command).await,
DaemonEvent::VpnSessionStatus(vpn_session_status) => {
self.handle_vpn_session_status(vpn_session_status).await
}
DaemonEvent::TunnelStateTransition(transition) => {
self.handle_tunnel_state_transition(transition).await
}
DaemonEvent::Shutdown => {}
}
}
async fn handle_tunnel_state_transition(&mut self, transition: TunnelStateTransition) {
tracing::info!("tunnel transition: {transition:?}");
let processed = self
.vpn_session_storage
.tunnel_state_transition(transition.clone(), self.state.vpn_status())
.await
.expect("failed to process tunnel state transition");
if let Some(reason) = processed.end_session {
tracing::info!("ending session after tunnel transition {reason}");
if let Err(err) = self.end_session(reason).await {
tracing::error!(
"failed to end session on state transition: {transition:?}: {err:?}"
);
};
}
if let Some(session_info) = processed.client_connected {
tracing::info!("client connected {}", session_info.request_id);
self.client_connected(session_info).await;
}
if let Some(tunnel_command) = processed.tunnel_command {
tracing::info!("sending tunnel command after tunnel state transition");
self.send_tunnel_command(tunnel_command);
}
if let Some(notification) = processed.notification {
tracing::info!("sending notification after tunnel state transition {notification:?}");
self.add_notification(notification).await;
}
// update vpn status and send device event
self.set_vpn_status(processed.vpn_status).await;
}
async fn handle_vpn_session_status(&mut self, vpn_session_status: VpnSessionStatus) {
// Update DB and get next set of actions
let processed = self
.vpn_session_storage
.updated_server_status(vpn_session_status.clone())
.await
.map_err(|e| {
tracing::error!(
"failed to process updated vpn session status {vpn_session_status}: {e}"
)
})
.expect("unrecoverable db error in handle_vpn_session_status");
if let Some(notification) = processed.notification {
self.add_notification(notification).await;
}
if let Some(new_vpn_status) = processed.vpn_status {
// Update in memory status and notify clients of new status
self.set_vpn_status(new_vpn_status.clone()).await;
// Start next state machine if this is ServerReady
self.update_tunnel_on_new_status(new_vpn_status).await;
}
}
async fn update_tunnel_on_new_status(&self, new_vpn_status: VpnStatus) {
if let VpnStatus::ServerReady(_) = new_vpn_status {
self.send_tunnel_command(TunnelCommand::Connect);
}
}
async fn handle_command(&mut self, command: DaemonCommand) {
match command {
DaemonCommand::IsAuthenticated(tx) => self.is_authenticated(tx).await,
DaemonCommand::AccountSignIn(tx, auth_input) => {
self.on_account_sign_in(tx, auth_input).await
}
DaemonCommand::AccountSignOut(tx) => self.on_account_sign_out(tx).await,
DaemonCommand::ListLocations(tx) => self.on_list_locations(tx).await,
DaemonCommand::RecentLocations(tx) => self.on_recent_locations(tx).await,
DaemonCommand::Connect(tx, location) => self.on_connect(tx, location).await,
DaemonCommand::Disconnect(tx) => self.on_disconnect(tx).await,
DaemonCommand::GetVpnStatus(tx) => self.on_get_vpn_status(tx).await,
DaemonCommand::GetNotifications(tx) => self.on_get_notifications(tx).await,
DaemonCommand::AckNotification(tx, id) => self.on_ack_notification(tx, id).await,
DaemonCommand::LatestAppVersion(tx) => self.on_latest_app_version(tx).await,
}
}
async fn on_latest_app_version(&self, tx: ResponseTx<String, DaemonError>) {
let device_handler = self.device_handler.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
device_handler
.latest_app_version()
.await
.map_err(DaemonError::DeviceError),
"on_latest_app_version",
);
});
}
async fn on_get_notifications(&self, tx: ResponseTx<Vec<Notification>, DaemonError>) {
let notifications = self.state.notifications();
tokio::spawn(async move {
Self::oneshot_send(tx, Ok(notifications), "on_get_notifications");
});
}
async fn on_ack_notification(&mut self, tx: ResponseTx<(), DaemonError>, id: String) {
self.state.remove_notification(id);
tokio::spawn(async move {
Self::oneshot_send(tx, Ok(()), "on_ack_notification");
});
}
async fn add_notification(&mut self, notification: Notification) {
// save in current state
self.state.add_notification(notification.clone());
// notify event listeners
self.controller_server_and_event_broadcaster
.send_notification(notification)
.await;
}
async fn set_vpn_status(&mut self, vpn_status: nymvpn_types::vpn_session::VpnStatus) {
// save current state
self.state.set_vpn_status(vpn_status.clone());
// and notify event listeners,
{
// todo: make broadcaster clone-able and send event on spawned task
self.controller_server_and_event_broadcaster
.send_vpn_status(vpn_status.into())
.await;
}
}
async fn on_connect_inner(&mut self, location: Location) -> Result<VpnStatus, DaemonError> {
if let Some(location) = self.state.vpn_session_in_progress() {
tracing::warn!("another vpn session in progress: {location}");
return Err(DaemonError::AnotherVpnSessionInProgress(location));
}
let request_id = self
.vpn_session_storage
.new_session(location.clone())
.await?;
let device_unique_id = self.device_storage.get_device_unique_id().await?;
let new_session = NewSession {
request_id,
device_unique_id,
location_code: location.code.clone(),
};
match self
.vpn_session_handler
.new_session(new_session.clone())
.await
{
Ok(accepted) => {
// update local record
self.vpn_session_storage
.update_on_accepted(accepted)
.await?;
// update state
self.state.accepted(location.clone());
Ok(VpnStatus::Accepted(location))
}
Err(err) => {
tracing::error!("cannot connect: {err}");
// remove local record
self.vpn_session_storage
.delete(new_session.request_id)
.await?;
// add notification about it
let notification = self
.state
.add_notification_for_failed_new_session(request_id, location, err);
self.add_notification(notification).await;
Ok(VpnStatus::Disconnected)
}
}
}
async fn on_connect(
&mut self,
tx: ResponseTx<VpnStatus, DaemonError>,
location: nymvpn_types::location::Location,
) {
tracing::info!("Connection requested to {location}");
let location_storage = self.location_storage.clone();
let location_to_add = location.clone();
tokio::spawn(async move {
if let Err(e) = location_storage.add_recent(location_to_add).await {
tracing::error!("failed to save recent location: {e}");
}
});
Self::oneshot_send(tx, self.on_connect_inner(location).await, "on_connect");
}
async fn tunnel_command_on_disconnect(&self) {
let current_state = self.state.vpn_status();
match &current_state {
VpnStatus::ServerReady(_) | VpnStatus::Connecting(_) | VpnStatus::Connected(_, _) => {
self.send_tunnel_command(TunnelCommand::Disconnect)
}
VpnStatus::Accepted(_)
| VpnStatus::ServerCreated(_)
| VpnStatus::ServerRunning(_)
| VpnStatus::Disconnecting(_)
| VpnStatus::Disconnected => {
tracing::info!("Not sending tunnel command to disconnect. State: {current_state}");
}
}
}
async fn end_session(&mut self, end_reason: String) -> Result<(), DaemonError> {
// Get session info and mark for deletion in DB
let session_info = self.vpn_session_storage.end_session().await?;
match session_info {
Some(session_info) => {
let device_unique_id = self.device_storage.get_device_unique_id().await?;
let end_session = EndSession {
request_id: session_info.request_id,
device_unique_id,
vpn_session_uuid: session_info.vpn_session_id,
reason: end_reason,
};
// make api call to server to end session,
// ignore errors as reclaimer should eventually cleanup
match self
.vpn_session_handler
.end_session(end_session.clone())
.await
{
Ok(ended) => {
tracing::info!("vpn session successfully ended on server: {ended}");
// on success delete record from DB
self.vpn_session_storage
.delete(session_info.request_id)
.await?;
}
Err(err) => {
tracing::error!("couldn't end session on server {end_session}: {err}");
}
};
}
None => {
tracing::warn!("No existing vpn session found in DB in end_session");
}
}
Ok(())
}
async fn client_connected(&self, session_info: SessionInfo) {
let device_storage = self.device_storage.clone();
let vpn_session_handler = self.vpn_session_handler.clone();
tokio::spawn(async move {
async fn call_client_connected(
session_info: SessionInfo,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
) -> Result<(), DaemonError> {
let device_unique_id = device_storage.get_device_unique_id().await?;
let client_connected = ClientConnected {
request_id: session_info.request_id,
device_unique_id,
vpn_session_uuid: session_info.vpn_session_id,
};
let _ = vpn_session_handler
.client_connected(client_connected.clone())
.await;
Ok(())
}
if let Err(e) =
call_client_connected(session_info.clone(), device_storage, vpn_session_handler)
.await
{
tracing::error!(
"couldn't make client connected call on server: {session_info}: {e}"
);
}
Ok::<(), DaemonError>(())
});
}
async fn on_disconnect_inner(&mut self, end_reason: String) -> Result<VpnStatus, DaemonError> {
self.tunnel_command_on_disconnect().await;
self.end_session(end_reason).await?;
let status = self.state.update_state_on_disconnect();
self.controller_server_and_event_broadcaster
.send_vpn_status(status.clone())
.await;
Ok(status)
}
async fn on_disconnect(&mut self, tx: ResponseTx<VpnStatus, DaemonError>) {
tracing::info!("Disconnect requested");
Self::oneshot_send(
tx,
self.on_disconnect_inner("client requested".into()).await,
"on_disconnect",
);
}
async fn on_get_vpn_status(&self, tx: ResponseTx<VpnStatus, DaemonError>) {
let vpn_status = self.state.vpn_status();
tokio::spawn(async move {
Self::oneshot_send(tx, Ok(vpn_status), "on_get_vpn_status_response")
});
}
async fn is_authenticated(&self, tx: ResponseTx<bool, DaemonError>) {
let device_handler = self.device_handler.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
Ok(device_handler.is_authenticated().await),
"is_authenticated_response",
)
});
}
async fn on_account_sign_in(
&self,
tx: ResponseTx<(), DaemonError>,
user_creds: UserCredentials,
) {
// if vpn session in progress do not sign in
if let Some(location) = self.state.vpn_session_in_progress() {
tracing::warn!("sign in attempt when vpn session in progress: {location}");
Self::oneshot_send(
tx,
Err(DaemonError::InvalidOpVpnSessionInProgress(format!(
"cannot sign in when a vpn session is in progress (to city {})",
location.city
))),
"on_account_sign_in error",
);
} else {
let device_handler = self.device_handler.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
device_handler
.sign_in(user_creds)
.await
.map_err(DaemonError::DeviceError),
"on_account_login response",
)
});
}
}
async fn on_account_sign_out(&self, tx: ResponseTx<(), DaemonError>) {
// if vpn session in progress do not sign out
if let Some(location) = self.state.vpn_session_in_progress() {
tracing::warn!("sign out attempt when vpn session in progress: {location}");
Self::oneshot_send(
tx,
Err(DaemonError::InvalidOpVpnSessionInProgress(format!(
"cannot sign out when a vpn session is in progress (to city {})",
location.city
))),
"on_account_sign_out error",
);
} else {
let device_handler = self.device_handler.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
device_handler
.sign_out()
.await
.map_err(DaemonError::DeviceError),
"on_account_sign_out response",
)
});
}
}
async fn on_list_locations(&self, tx: ResponseTx<Vec<Location>, DaemonError>) {
let vpn_session_handler = self.vpn_session_handler.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
vpn_session_handler
.list_locations()
.await
.map_err(DaemonError::VpnSessionError),
"on_list_locations response",
)
});
}
async fn on_recent_locations(&self, tx: ResponseTx<Vec<Location>, DaemonError>) {
let location_storage = self.location_storage.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
location_storage.recent().await.map_err(DaemonError::DbErr),
"on_recent_locations response",
)
});
}
fn send_tunnel_command(&self, command: TunnelCommand) {
self.tunnel_state_machine_handle
.command_tx()
.unbounded_send(command)
.expect("Tunnel state machine has stopped");
}
fn oneshot_send<T>(tx: oneshot::Sender<T>, t: T, msg: &'static str) {
if tx.send(t).is_err() {
tracing::warn!("Unable to send {} to the daemon command sender", msg);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
use nymvpn_config::config;
use nymvpn_migration::{
sea_orm::{ConnectOptions, Database, DatabaseConnection},
Migrator, MigratorTrait,
};
#[derive(Debug, Clone)]
pub struct Db {
connection: DatabaseConnection,
}
impl Db {
pub async fn new() -> Result<Self, String> {
let config = config();
tokio::fs::create_dir_all(config.db_dir())
.await
.map_err(|e| format!("Error creating DB directory {e}"))?;
let mut opts = ConnectOptions::new(config.db_url());
opts.sqlx_logging(false);
Ok(Self {
connection: Database::connect(opts)
.await
.map_err(|e| format!("Error connecting to database {e}"))?,
})
}
pub async fn migrate(&self) -> Result<(), String> {
Migrator::up(&self.connection, None)
.await
.map_err(|e| format!("failed to run db migration {e}"))
}
pub fn connection(&self) -> DatabaseConnection {
self.connection.clone()
}
}
@@ -0,0 +1,244 @@
use tokio::sync::{mpsc, oneshot};
use nymvpn_controller::auth::Auth;
use nymvpn_migration::sea_orm::DatabaseConnection;
use nymvpn_server::{ServerApi, ServerApiNoAuth};
use nymvpn_types::nymvpn_server::{AddDeviceRequest, UserCredentials};
use crate::{token_storage::TokenStorage, AckTx, ResponseTx};
use super::{storage::DeviceStorage, DeviceError};
#[derive(Debug, Clone)]
pub struct DeviceHandler {
tx: mpsc::UnboundedSender<DeviceCommand>,
}
impl DeviceHandler {
pub async fn start(db: DatabaseConnection) -> Result<Self, DeviceError> {
let (sender, receiver) = mpsc::unbounded_channel();
let mut device_service = DeviceService::new(db, receiver).await?;
tokio::spawn(async move { device_service.run().await });
Ok(Self { tx: sender })
}
pub async fn sign_in(&self, user_creds: UserCredentials) -> Result<(), DeviceError> {
self.send_command(move |tx| DeviceCommand::SignIn(tx, user_creds))
.await
}
pub async fn sign_out(&self) -> Result<(), DeviceError> {
self.send_command(move |tx| DeviceCommand::SignOut(tx))
.await
}
pub async fn latest_app_version(&self) -> Result<String, DeviceError> {
self.send_command(move |tx| DeviceCommand::LatestAppVersion(tx))
.await
}
pub async fn shutdown(&self) -> Result<(), DeviceError> {
let (tx, rx) = oneshot::channel();
self.tx
.send(DeviceCommand::Shutdown(tx))
.map_err(|_| DeviceError::DeviceServiceUnavailable)?;
rx.await
.map_err(|_| DeviceError::DeviceServiceUnavailable)?;
Ok(())
}
pub async fn send_command<T>(
&self,
make_cmd: impl FnOnce(oneshot::Sender<Result<T, DeviceError>>) -> DeviceCommand,
) -> Result<T, DeviceError> {
let (tx, rx) = oneshot::channel();
self.tx
.send(make_cmd(tx))
.map_err(|_| DeviceError::DeviceServiceUnavailable)?;
rx.await
.map_err(|_| DeviceError::DeviceServiceUnavailable)?
}
}
#[async_trait::async_trait]
impl Auth for DeviceHandler {
async fn is_authenticated(&self) -> bool {
let response = self
.send_command(|tx| DeviceCommand::IsAuthenticated(tx))
.await
.map_err(|e| tracing::error!("failed to check if device is authenticated: {e}"))
.ok();
response.is_some() && response.unwrap()
}
}
pub enum DeviceCommand {
SignIn(ResponseTx<(), DeviceError>, UserCredentials),
SignOut(ResponseTx<(), DeviceError>),
BearerToken(ResponseTx<Option<String>, DeviceError>),
IsAuthenticated(ResponseTx<bool, DeviceError>),
Shutdown(AckTx),
LatestAppVersion(ResponseTx<String, DeviceError>),
}
pub struct DeviceService {
token: Option<String>,
rx: mpsc::UnboundedReceiver<DeviceCommand>,
device_storage: DeviceStorage,
token_storage: TokenStorage,
}
impl DeviceService {
pub async fn new(
db: DatabaseConnection,
rx: mpsc::UnboundedReceiver<DeviceCommand>,
) -> Result<Self, DeviceError> {
let device_storage = DeviceStorage::new(db.clone());
let token_storage = TokenStorage::new(db);
let token = token_storage.get_token().await?;
Ok(Self {
token,
rx,
device_storage,
token_storage,
})
}
pub async fn run(&mut self) {
let mut shutdown_tx = None;
while let Some(msg) = self.rx.recv().await {
if let DeviceCommand::Shutdown(tx) = msg {
shutdown_tx = Some(tx);
break;
}
self.handle_message(msg).await;
}
tracing::info!("Device service shutting down");
if shutdown_tx.is_some() {
let _ = shutdown_tx.unwrap().send(());
}
}
async fn handle_message(&mut self, msg: DeviceCommand) {
match msg {
DeviceCommand::SignIn(tx, user_creds) => self.handle_sign_in(tx, user_creds).await,
DeviceCommand::SignOut(tx) => self.handle_sign_out(tx).await,
DeviceCommand::BearerToken(tx) => self.handle_bearer_token(tx).await,
DeviceCommand::IsAuthenticated(tx) => self.handle_is_authenticated(tx).await,
DeviceCommand::Shutdown(_) => {}
DeviceCommand::LatestAppVersion(tx) => self.handle_latest_app_version(tx).await,
}
}
async fn handle_latest_app_version_inner(&mut self) -> Result<String, DeviceError> {
let mut server_api = ServerApi::new(self.token_storage.clone()).await?;
let version = server_api.latest_app_version().await?;
Ok(version)
}
async fn handle_latest_app_version(&mut self, tx: ResponseTx<String, DeviceError>) {
Self::oneshot_send(
tx,
self.handle_latest_app_version_inner().await,
"handle_latest_app_version_inner",
);
}
async fn handle_sign_in_inner(
&mut self,
user_creds: UserCredentials,
) -> Result<(), DeviceError> {
let mut nymvpn_service = ServerApiNoAuth::new().await?;
self.device_storage
.init()
.await
.map_err(DeviceError::InitError)?;
let device_details = self.device_storage.get_device().await?.unwrap();
let add_device_request = AddDeviceRequest {
user_creds,
device_info: device_details.clone().into(),
};
// make API call
let add_device_response = nymvpn_service.add_device(add_device_request).await?;
// save token
self.token_storage
.save_token(add_device_response.token.clone())
.await?;
// update device ip addresses
let device_details = self
.device_storage
.update_ipv4_address(
device_details.unique_id,
add_device_response.device_addresses.ipv4_address,
)
.await?;
tracing::info!("Successfully signed in {device_details}");
// keep this new token in memory
self.token = Some(add_device_response.token);
Ok(())
}
async fn handle_sign_in(
&mut self,
tx: ResponseTx<(), DeviceError>,
user_creds: UserCredentials,
) {
Self::oneshot_send(
tx,
self.handle_sign_in_inner(user_creds).await,
"handle_sign_in",
);
}
async fn handle_sign_out_inner(&mut self) -> Result<(), DeviceError> {
// make API call to invalidate token
let mut server_api = ServerApi::new(self.token_storage.clone()).await?;
server_api.sign_out().await?;
// reinitialize device
self.device_storage.reinitialize("sign out").await?;
// remove from DB and memory
self.token_storage.remove_all().await?;
self.token = None;
Ok(())
}
async fn handle_sign_out(&mut self, tx: ResponseTx<(), DeviceError>) {
Self::oneshot_send(tx, self.handle_sign_out_inner().await, "handle_sign_out");
}
async fn handle_bearer_token(&self, tx: ResponseTx<Option<String>, DeviceError>) {
let token = self.token.clone();
tokio::spawn(async move {
Self::oneshot_send(tx, Ok(token), "handle_bearer_token");
});
}
async fn handle_is_authenticated(&self, tx: ResponseTx<bool, DeviceError>) {
let token = self.token.clone();
tokio::spawn(async move {
// todo: validate token from backend; if invalid purge from DB and memory
Self::oneshot_send(tx, Ok(token.is_some()), "handle_is_authenticated")
});
}
fn oneshot_send<T>(tx: oneshot::Sender<T>, t: T, msg: &'static str) {
if tx.send(t).is_err() {
tracing::warn!("Failed to respond from DeviceService {}", msg);
}
}
}
@@ -0,0 +1,9 @@
use nymvpn_migration::sea_orm::DatabaseConnection;
use nymvpn_types::device::DeviceDetails;
use super::storage::DeviceStorage;
pub async fn initialize_device(db: DatabaseConnection) -> Result<DeviceDetails, String> {
let device_storage = DeviceStorage::new(db);
device_storage.init().await
}
@@ -0,0 +1,20 @@
use nymvpn_migration::DbErr;
pub mod handler;
pub mod init;
pub mod name;
pub mod storage;
#[derive(Debug, thiserror::Error)]
pub enum DeviceError {
#[error("device service is unavailable")]
DeviceServiceUnavailable,
#[error("server error: {0}")]
Server(#[from] tonic::Status),
#[error("error connecting to server: {0}")]
Connection(#[from] tonic::transport::Error),
#[error("db error: {0}")]
DbErr(#[from] DbErr),
#[error("failed to initialize device: {0}")]
InitError(String),
}
@@ -0,0 +1,54 @@
use std::process::Command;
pub fn command_stdout_lossy(cmd: &str, args: &[&str]) -> Option<String> {
Command::new(cmd)
.args(args)
.output()
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
.ok()
}
pub fn logged_in_user() -> Option<String> {
#[cfg(target_os = "linux")]
let output = command_stdout_lossy("last", &["-n", "100"]);
#[cfg(target_os = "macos")]
let output = command_stdout_lossy("last", &["-100"]);
#[cfg(target_os = "windows")]
let output: Option<String> = None;
let mut found_name = None;
if let Some(output) = output {
for line in output.lines() {
if line.contains("logged in") {
if let Some(name) = line.split_whitespace().nth(0) {
found_name = Some(name.to_string());
break;
}
}
}
}
found_name
}
pub fn hostname() -> Option<String> {
command_stdout_lossy("hostname", &[])
}
pub fn device_name() -> String {
logged_in_user()
.or_else(hostname)
.map(|user| {
if user.is_empty() {
"[unknown]".into()
} else {
user
}
})
.unwrap_or("[unknown]".into())
}
pub fn device_version() -> String {
talpid_platform_metadata::version()
}
@@ -0,0 +1,132 @@
use std::net::Ipv4Addr;
use chrono::Utc;
use nymvpn_migration::{
sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, QuerySelect, Set},
DbErr,
};
use nymvpn_types::{
device::{DeviceDetails, ANDROID, IOS, LINUX, MACOS, WINDOWS},
nymvpn_server::DeviceType,
};
use uuid::Uuid;
use crate::device::name::{device_name, device_version};
#[derive(Clone)]
pub struct DeviceStorage {
db: DatabaseConnection,
}
fn create_new() -> Result<DeviceDetails, String> {
let device_type = match std::env::consts::OS {
LINUX => DeviceType::Linux,
MACOS => DeviceType::MacOS,
WINDOWS => DeviceType::Windows,
ANDROID => DeviceType::Android,
IOS => DeviceType::IOS,
os => Err(format!("OS not supported: {os}"))?,
};
let arch = std::env::consts::ARCH;
tracing::info!("detected device type: {device_type}, arch : {arch}");
Ok(DeviceDetails {
name: device_name(),
version: device_version(),
arch: arch.into(),
unique_id: uuid::Uuid::new_v4(),
device_type,
wireguard_meta: Default::default(),
created_at: Utc::now(),
})
}
impl DeviceStorage {
pub fn new(db: DatabaseConnection) -> DeviceStorage {
Self { db }
}
pub async fn init(&self) -> Result<DeviceDetails, String> {
tracing::info!("Initializing device");
let device = nymvpn_entity::device::Entity::find()
.one(&self.db)
.await
.map_err(|e| format!("unable to get device details: {e}"))?;
if let Some(device) = device {
tracing::info!("device already initialized");
let device_details = device.try_into()?;
tracing::info!("found: {device_details}");
return Ok(device_details);
}
// No device found create one
tracing::info!("creating new device");
let device_details = create_new()?;
// save to DB
let device: nymvpn_entity::device::Model = device_details.clone().into();
let device: nymvpn_entity::device::ActiveModel = device.into();
let _ = device
.insert(&self.db)
.await
.map_err(|e| format!("failed to save device detail {e}"))?;
tracing::info!("new device saved: {device_details}");
Ok(device_details)
}
pub async fn reinitialize(&self, reason: &str) -> Result<(), DbErr> {
tracing::info!("reinitializing device: {reason}");
let _ = nymvpn_entity::device::Entity::delete_many()
.exec(&self.db)
.await?;
let _ = self.init().await;
Ok(())
}
// Assumes device was initialized successfully
pub async fn get_device_unique_id(&self) -> Result<Uuid, DbErr> {
let id: (String,) = nymvpn_entity::device::Entity::find()
.select_only()
.column(nymvpn_entity::device::Column::UniqueId)
.into_tuple()
.one(&self.db)
.await?
.unwrap();
Ok(Uuid::parse_str(&id.0).unwrap())
}
pub async fn get_device(&self) -> Result<Option<DeviceDetails>, DbErr> {
let device = nymvpn_entity::device::Entity::find().one(&self.db).await?;
if let Some(device) = device {
let device_details = device.try_into().map_err(|e| DbErr::Custom(e))?;
return Ok(Some(device_details));
}
Ok(None)
}
pub async fn update_ipv4_address(
&self,
unique_id: Uuid,
ipv4_address: Ipv4Addr,
) -> Result<DeviceDetails, DbErr> {
let device = nymvpn_entity::device::Entity::find_by_id(unique_id.to_string())
.one(&self.db)
.await?
.ok_or(DbErr::RecordNotFound(format!(
"device with unique id {unique_id} not found"
)))?;
let mut device: nymvpn_entity::device::ActiveModel = device.into();
device.ipv4_address = Set(Some(ipv4_address.to_string()));
let device = device.update(&self.db).await?;
Ok(device.try_into().map_err(|e| DbErr::Custom(e))?)
}
}
+217
View File
@@ -0,0 +1,217 @@
use daemon::Daemon;
use shutdown::ShutdownManager;
use talpid_core::tunnel_state_machine;
use talpid_types::net::{AllowedEndpoint, Endpoint};
use tokio::sync::oneshot;
pub mod cleanup;
pub mod controller;
pub mod daemon;
pub mod db;
pub mod device;
pub mod location_storage;
pub mod logging;
#[cfg(target_os = "macos")]
pub mod macos;
pub mod runtime;
pub mod shutdown;
pub mod state;
#[cfg(windows)]
pub mod system_service;
pub mod token_storage;
pub mod tunnel;
pub mod unique;
pub mod vpn_session;
#[cfg(any(target_os = "macos", target_os = "linux"))]
use crate::cleanup::remove_old_socket_file;
use tokio_stream::StreamExt;
use nymvpn_config::config;
pub type ResponseTx<T, E> = oneshot::Sender<Result<T, E>>;
pub type AckTx = oneshot::Sender<()>;
use std::{error::Error, path::PathBuf};
use crate::{
controller::ControllerServer,
daemon::{DaemonCommandChannel, DaemonEventSender},
db::Db,
device::{handler::DeviceHandler, init::initialize_device, storage::DeviceStorage},
location_storage::LocationStorage,
token_storage::TokenStorage,
tunnel::ParameterGenerator,
unique::already_running,
vpn_session::{
handler::VpnSessionHandler, reclaimer::ReclaimerCreator, storage::VpnSessionStorage,
},
};
fn print_error(e: &dyn Error) {
tracing::error!("error: {}", e);
let mut cause = e.source();
while let Some(e) = cause {
tracing::error!("caused by: {}", e);
cause = e.source();
}
}
pub async fn create_daemon(shutdown_manager: &ShutdownManager) -> Result<Daemon, String> {
if already_running().await {
return Err("Nymvpn Daemon is already running".to_owned());
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
remove_old_socket_file().await;
// root user is required otherwise vpn setup will fail
user_check();
let db = Db::new().await?;
// Run DB migrations
tracing::info!("Running DB migration");
db.migrate().await?;
// Initialize device
let _ = initialize_device(db.connection()).await?;
let device_handler = DeviceHandler::start(db.connection())
.await
.map_err(|e| format!("failed to start device handler: {e:#?}"))?;
#[cfg(target_os = "macos")]
let exclusion_gid = {
macos::bump_filehandle_limit();
macos::set_exclusion_gid().map_err(|e| format!("failed to set exclusion gid: {e}"))?
};
let config = config();
let daemon_command_channel = DaemonCommandChannel::new();
let controller_server_and_event_broadcaster = ControllerServer::start(
daemon_command_channel.sender(),
shutdown_manager,
device_handler.clone(),
)
.await
.map_err(|e| e.to_string())?;
let token_storage = TokenStorage::new(db.connection());
let device_storage = DeviceStorage::new(db.connection());
let location_storage = LocationStorage::new(db.connection());
let vpn_session_storage = VpnSessionStorage::new(db.connection());
// Reclaim old sessions which were not gracefully ended completely
vpn_session_storage
.reclaim()
.await
.map_err(|e| format!("failed to reclaim {e}"))?;
let vpn_session_handler =
VpnSessionHandler::start(daemon_command_channel.sender().into(), token_storage).await;
// start reclaimer
ReclaimerCreator::start(
vpn_session_storage.clone(),
device_storage.clone(),
vpn_session_handler.clone(),
shutdown_manager.new_shutdown(),
)
.await;
let (offline_state_tx, mut offline_state_rx) = futures_channel::mpsc::unbounded();
//todo: make better use of offline_state_rx:
let mut offline_watcher_shutdown = shutdown_manager.new_shutdown();
tokio::spawn(async move {
while !offline_watcher_shutdown.is_shutdown() {
tokio::select! {
Some(offline) = offline_state_rx.next() => {
tracing::info!("Is offline {offline}");
},
_ = offline_watcher_shutdown.recv() => {
tracing::info!("shutting down offline watcher");
break;
}
}
}
});
let tunnel_parameters_generator = ParameterGenerator::new(db);
#[cfg(windows)]
let exclude_paths = vec![];
#[cfg(target_os = "windows")]
let (_volume_update_tx, volume_update_rx) = futures::channel::mpsc::unbounded();
let allowed_endpoint = AllowedEndpoint {
#[cfg(windows)]
clients: vec![std::env::current_exe().expect("daemon executable path not available")],
endpoint: Endpoint::new(
config.allowed_endpoint_ipv4().clone(),
44444,
talpid_types::net::TransportProtocol::Tcp,
),
};
let resource_dir: PathBuf = std::env::current_exe()
.expect("error getting current_exe path")
.parent()
.expect("cannot obtain parent path for current_exe")
.into();
#[cfg(windows)]
tracing::info!("Resource dir: {}", resource_dir.display());
let tunnel_state_machine_handle = tunnel_state_machine::spawn(
tunnel_state_machine::InitialTunnelState {
allow_lan: true,
block_when_disconnected: false,
dns_servers: Some(vec!["1.1.1.1".parse().unwrap()]),
allowed_endpoint,
reset_firewall: true,
#[cfg(windows)]
exclude_paths,
},
tunnel_parameters_generator,
Some(config.log_dir().into()),
resource_dir,
DaemonEventSender::from(daemon_command_channel.sender()),
offline_state_tx,
#[cfg(target_os = "windows")]
volume_update_rx,
#[cfg(target_os = "macos")]
exclusion_gid,
#[cfg(target_os = "android")]
None,
#[cfg(target_os = "linux")]
tunnel_state_machine::LinuxNetworkingIdentifiers {
fwmark: nymvpn_types::TUNNEL_FWMARK,
table_id: nymvpn_types::TUNNEL_TABLE_ID,
},
)
.await
.map_err(|e| {
print_error(&e);
e.to_string()
})?;
Ok(Daemon::new(
daemon_command_channel,
device_handler,
vpn_session_storage,
device_storage,
vpn_session_handler,
controller_server_and_event_broadcaster,
tunnel_state_machine_handle,
location_storage,
Some(shutdown_manager.new_shutdown()),
))
}
fn user_check() {
#[cfg(unix)]
{
if !nix::unistd::getuid().is_root() {
tracing::warn!("Running as non-root user, vpn setup will fail")
}
}
}
@@ -0,0 +1,76 @@
use nymvpn_migration::{
sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder,
QuerySelect, QueryTrait, Set,
},
DbErr,
};
use nymvpn_types::location::Location;
const RECENT_LIMIT: u64 = 2;
#[derive(Clone)]
pub struct LocationStorage {
db: DatabaseConnection,
}
impl LocationStorage {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn add_recent(&self, location: Location) -> Result<(), DbErr> {
let _ = nymvpn_entity::recent_locations::Entity::delete_many()
.filter(nymvpn_entity::recent_locations::Column::Code.eq(location.code.clone()))
.exec(&self.db)
.await?;
// insert new record
let recent_location = nymvpn_entity::recent_locations::ActiveModel {
code: Set(location.code),
city: Set(location.city),
city_code: Set(location.city_code),
country: Set(location.country),
country_code: Set(location.country_code),
state: Set(location.state),
state_code: Set(location.state_code),
..Default::default()
};
recent_location.insert(&self.db).await?;
// delete older ones
let deleted_result = nymvpn_entity::recent_locations::Entity::delete_many()
.filter(
nymvpn_entity::recent_locations::Column::Id.not_in_subquery(
nymvpn_entity::recent_locations::Entity::find()
.select_only()
.column(nymvpn_entity::recent_locations::Column::Id)
.order_by_desc(nymvpn_entity::recent_locations::Column::Id)
.limit(RECENT_LIMIT)
.into_query(),
),
)
.exec(&self.db)
.await?;
tracing::debug!(
"deleted from recent locations {}",
deleted_result.rows_affected
);
Ok(())
}
pub async fn recent(&self) -> Result<Vec<Location>, DbErr> {
Ok(nymvpn_entity::recent_locations::Entity::find()
.distinct()
.order_by_desc(nymvpn_entity::recent_locations::Column::Id)
.group_by(nymvpn_entity::recent_locations::Column::Code)
.all(&self.db)
.await?
.into_iter()
.map(nymvpn_types::location::Location::from)
.collect())
}
}
@@ -0,0 +1,104 @@
#[cfg(unix)]
use std::os::unix::prelude::PermissionsExt;
use std::{fs::Permissions, io, path::Path};
use talpid_core::logging::rotate_log;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use nymvpn_config::config;
const DEBUG: &[&str] = &["talpid_core"];
const INFO: &[&str] = &[
"h2",
"rustls",
"mio",
"netlink_sys",
"want",
"nftnl",
"mnl",
"netlink_proto",
"hyper",
"tower",
"tokio_util",
"tonic",
];
const ERROR: &[&str] = &[];
const WARN: &[&str] = &[];
fn log_level(crates: &[&str], level: &str) -> String {
crates
.iter()
.map(|c| format!("{c}={level}"))
.collect::<Vec<String>>()
.join(",")
}
pub fn init() -> Result<WorkerGuard, Box<dyn std::error::Error>> {
if !std::env::var("RUST_LOG").is_ok() {
let info = log_level(INFO, "info");
let debug = log_level(DEBUG, "debug");
let warn = log_level(WARN, "warn");
let error = log_level(ERROR, "error");
let all = [info, debug, warn, error];
let all = all.join(",");
std::env::set_var("RUST_LOG", format!("info,{all}"));
}
let config = config();
#[cfg(unix)]
let permissions = Some(PermissionsExt::from_mode(0o755));
#[cfg(not(unix))]
let permissions = None;
create_dir(config.log_dir(), permissions)?;
rotate_log(&config.log_dir().join(config.daemon_log_filename()))?;
let file_appender =
tracing_appender::rolling::never(config.log_dir(), config.daemon_log_filename());
let (file_writer, worker_guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::registry()
.with(EnvFilter::from_default_env())
.with(
fmt::Layer::default()
.with_writer(io::stdout)
.with_writer(io::stderr),
)
.with(
fmt::Layer::default()
.with_writer(file_writer)
.with_ansi(false),
)
.try_init()?;
std::panic::set_hook(Box::new(|panic| {
// If the panic has a source location, record it as structured fields.
if let Some(_location) = panic.location() {
tracing::error!(
message = %panic,
);
} else {
tracing::error!(message = %panic);
}
}));
tracing::info!("RUST_LOG: {}", std::env::var("RUST_LOG").unwrap());
Ok(worker_guard)
}
fn create_dir(path: &Path, perm: Option<Permissions>) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(path)?;
if perm.is_some() {
let perm = perm.unwrap();
std::fs::set_permissions(path, perm.clone())?
}
Ok(())
}
@@ -0,0 +1,75 @@
// Copyright (C) 2022 Mullvad VPN AB, GPL-3.0
use std::{ffi::CStr, io};
/// name of the group that should be excluded
const EXCLUSION_GROUP: &[u8] = b"nymvpn-exclusion\0";
/// Bump filehandle limit
pub fn bump_filehandle_limit() {
let mut limits = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
// SAFETY: `&mut limits` is a valid pointer parameter for the getrlimit syscall
let status = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limits) };
if status != 0 {
tracing::error!(
"Failed to get file handle limits: {}-{}",
io::Error::from_raw_os_error(status),
status
);
return;
}
const INCREASED_FILEHANDLE_LIMIT: u64 = 1024;
// if file handle limit is already big enough, there's no reason to decrease it.
if limits.rlim_cur >= INCREASED_FILEHANDLE_LIMIT {
return;
}
limits.rlim_cur = INCREASED_FILEHANDLE_LIMIT;
// SAFETY: `&limits` is a valid pointer parameter for the getrlimit syscall
let status = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limits) };
if status != 0 {
tracing::error!(
"Failed to set file handle limit to {}: {}-{}",
INCREASED_FILEHANDLE_LIMIT,
io::Error::from_raw_os_error(status),
status
);
}
}
/// Returns the GID of `nymvpn-exclusion` group if it exists.
pub fn get_exclusion_gid() -> io::Result<u32> {
let exclusion_group_name = CStr::from_bytes_with_nul(EXCLUSION_GROUP).unwrap();
get_group_id(exclusion_group_name)
}
/// Attempts to set the GID of the current process to `nymvpn-exclusion`.
pub fn set_exclusion_gid() -> io::Result<u32> {
let gid = get_exclusion_gid()?;
set_gid(gid)?;
Ok(gid)
}
/// Returns the GID of the specified group name
fn get_group_id(group_name: &CStr) -> io::Result<u32> {
// SAFETY: group_name is a valid CString
let group = unsafe { libc::getgrnam(group_name.as_ptr() as *const _) };
if group.is_null() {
return Err(io::Error::from(io::ErrorKind::NotFound));
}
// SAFETY: group is not null
let gid = unsafe { (*group).gr_gid };
Ok(gid)
}
/// Sets group ID for the current process
fn set_gid(gid: u32) -> io::Result<()> {
if unsafe { libc::setgid(gid) } == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
+81
View File
@@ -0,0 +1,81 @@
use std::process::ExitCode;
use clap::Parser;
use nymvpn_daemon::{create_daemon, logging, runtime::create_runtime, shutdown::ShutdownManager};
#[derive(Debug, Parser)]
struct Cli {
#[cfg(windows)]
#[arg(long)]
service: bool,
}
fn main() -> ExitCode {
#[cfg(windows)]
let cli = Cli::parse();
let _guard = match logging::init() {
Ok(guard) => guard,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let runtime = match create_runtime() {
Ok(rt) => rt,
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
};
let exit_code = match runtime.block_on(run_nymvpn(
#[cfg(windows)]
cli.service,
)) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!("{e}");
ExitCode::FAILURE
}
};
tracing::debug!("Daemon exiting {exit_code:?}");
exit_code
}
#[cfg(windows)]
async fn run_nymvpn(service: bool) -> Result<(), String> {
use nymvpn_daemon::system_service;
if service {
system_service::run()
} else {
run_nymvpn_inner().await
}
}
#[cfg(unix)]
async fn run_nymvpn() -> Result<(), String> {
run_nymvpn_inner().await
}
async fn run_nymvpn_inner() -> Result<(), String> {
let shutdown_manager = ShutdownManager::new();
let daemon = create_daemon(&shutdown_manager).await?;
let daemon_handle = tokio::spawn(async move {
daemon.run().await;
});
// register signal handler after broadcaster is setup for all
shutdown_manager.register_signal_handler().await;
if let (Err(err),) = tokio::join!(daemon_handle) {
tracing::error!("daemon: {err}");
};
tracing::info!("Daemon stopped");
Ok(())
}
@@ -0,0 +1,8 @@
// Copyright (C) 2023 Nym Technologies S.A., GPL-3.0
// Copyright (C) 2022 Mullvad VPN AB, GPL-3.0
pub fn create_runtime() -> std::io::Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
}
@@ -0,0 +1,122 @@
// Copyright (c) 2023 Nym Technologies S.A., GPL-3.0
//
// Based on mini-redis, Copyright (c) 2020 Tokio Contributors, MIT
// Copyright (c) 2020 Tokio Contributors
use std::{future::Future, pin::Pin};
use tokio::sync::broadcast;
#[derive(Debug)]
pub struct Shutdown {
/// `true` if the shutdown signal has been received
shutdown: bool,
/// The receive half of the channel used to listen for shutdown.
notify: broadcast::Receiver<()>,
}
impl Shutdown {
/// Create a new `Shutdown` backed by the given `broadcast::Receiver`.
pub fn new(notify: broadcast::Receiver<()>) -> Shutdown {
Shutdown {
shutdown: false,
notify,
}
}
/// Returns `true` if the shutdown signal has been received.
pub fn is_shutdown(&self) -> bool {
self.shutdown
}
/// Receive the shutdown notice, waiting if necessary.
pub async fn recv(&mut self) {
// If the shutdown signal has already been received, then return
// immediately.
if self.shutdown {
return;
}
// Cannot receive a "lag error" as only one value is ever sent.
let _ = self.notify.recv().await;
// Remember that the signal has been received.
self.shutdown = true;
}
}
pub struct ShutdownManager {
shutdown_notifier: tokio::sync::broadcast::Sender<()>,
}
impl ShutdownManager {
pub fn new() -> Self {
let (shutdown_notifier, _) = tokio::sync::broadcast::channel(1);
Self { shutdown_notifier }
}
pub fn shutdown_received_future(&self) -> Pin<Box<impl Future<Output = ()>>> {
let mut shutdown = self.new_shutdown();
Box::pin(async move { shutdown.recv().await })
}
pub fn new_shutdown(&self) -> Shutdown {
Shutdown::new(self.shutdown_notifier.subscribe())
}
// OS signal is broadcasted to rest of the system through broadcaster
pub async fn register_signal_handler(self) {
tracing::info!("registering signal handler");
tokio::spawn(async move {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install TERM signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
let _ = self.shutdown_notifier.send(());
tracing::info!("Shutdown signal received. Starting graceful shutdown");
});
}
// OS signal is broadcasted to rest of the system through broadcaster
#[cfg(windows)]
pub async fn register_signal_handler_windows(self, shutdown_rx: std::sync::mpsc::Receiver<()>) {
tracing::info!("registering signal handler");
tokio::spawn(async move {
let ctrl_c = async {
tokio::signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
let terminate = async move {
let _ = shutdown_rx.recv();
};
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
let _ = self.shutdown_notifier.send(());
tracing::info!("Shutdown signal received. Starting graceful shutdown");
});
}
}
+114
View File
@@ -0,0 +1,114 @@
use std::collections::HashMap;
use chrono::Utc;
use nymvpn_types::{
location::Location,
notification::{Notification, NotificationType},
vpn_session::VpnStatus,
};
use uuid::Uuid;
use crate::{
controller::{SERVER_UNAVAILABLE_PLEASE_TRY_AGAIN_LATER, VPN_SESSION_SERVICE_UNAVAILABLE},
vpn_session::handler::VpnSessionError,
};
pub struct DaemonState {
vpn_status: VpnStatus,
notifications: HashMap<String, Notification>,
}
impl DaemonState {
pub fn new() -> Self {
Self {
vpn_status: VpnStatus::Disconnected,
notifications: Default::default(),
}
}
pub fn set_vpn_status(&mut self, vpn_status: VpnStatus) {
self.vpn_status = vpn_status;
}
pub fn vpn_status(&self) -> VpnStatus {
self.vpn_status.clone()
}
pub fn update_state_on_disconnect(&mut self) -> VpnStatus {
let new_status = match &self.vpn_status {
VpnStatus::Accepted(_)
| VpnStatus::ServerCreated(_)
| VpnStatus::ServerRunning(_)
| VpnStatus::Disconnected => VpnStatus::Disconnected,
VpnStatus::ServerReady(location)
| VpnStatus::Connecting(location)
| VpnStatus::Connected(location, _)
| VpnStatus::Disconnecting(location) => VpnStatus::Disconnecting(location.clone()),
};
self.set_vpn_status(new_status.clone());
new_status
}
pub fn vpn_session_in_progress(&self) -> Option<Location> {
match &self.vpn_status {
VpnStatus::Accepted(location)
| VpnStatus::ServerCreated(location)
| VpnStatus::ServerRunning(location)
| VpnStatus::ServerReady(location)
| VpnStatus::Connecting(location)
| VpnStatus::Connected(location, _)
| VpnStatus::Disconnecting(location) => Some(location.clone()),
VpnStatus::Disconnected => None,
}
}
pub fn add_notification_for_failed_new_session(
&mut self,
request_id: Uuid,
_location: Location,
error: VpnSessionError,
) -> Notification {
let timestamp = Utc::now();
// user facing message of notification
let message = match error {
VpnSessionError::VpnSessionServiceDown => VPN_SESSION_SERVICE_UNAVAILABLE.to_string(),
VpnSessionError::Connection(_) => SERVER_UNAVAILABLE_PLEASE_TRY_AGAIN_LATER.to_string(),
VpnSessionError::Server(status) => status.message().to_string(),
};
let notification = Notification {
id: request_id.to_string(),
message,
notification_type: NotificationType::ServerFailed,
timestamp,
};
self.notifications
.insert(request_id.to_string(), notification.clone());
notification
}
pub fn accepted(&mut self, location: Location) {
self.vpn_status = VpnStatus::Accepted(location)
}
pub fn add_notification(&mut self, notification: Notification) {
self.notifications
.insert(notification.id.clone(), notification);
}
pub fn remove_notification(&mut self, id: String) {
self.notifications.remove(&id);
}
pub fn notifications(&self) -> Vec<Notification> {
let mut res: Vec<Notification> = self.notifications.values().map(|r| r.clone()).collect();
res.sort_by(|n1, n2| n2.timestamp.cmp(&n1.timestamp));
res
}
}
@@ -0,0 +1,271 @@
// Copyright (C) 2023 Nym Technologies S.A., GPL-3.0
// Copyright (C) 2022 Mullvad VPN AB, GPL-3.0
use crate::{runtime::create_runtime, shutdown::ShutdownManager};
use std::{
ffi::OsString,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc,
},
thread,
time::Duration,
};
use talpid_types::ErrorExt;
use windows_service::{
service::{
ServiceAccess, ServiceControl, ServiceControlAccept, ServiceExitCode, ServiceState,
ServiceStatus, ServiceType,
},
service_control_handler::{self, ServiceControlHandlerResult, ServiceStatusHandle},
service_dispatcher,
};
static SERVICE_NAME: &str = "nymvpnDaemonService";
static SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS;
lazy_static::lazy_static! {
static ref SERVICE_ACCESS: ServiceAccess = ServiceAccess::QUERY_CONFIG
| ServiceAccess::CHANGE_CONFIG
| ServiceAccess::START
| ServiceAccess::DELETE;
}
pub fn run() -> Result<(), String> {
// Start the service dispatcher.
// This will block current thread until the service stopped and spawn `service_main` on a
// background thread.
service_dispatcher::start(SERVICE_NAME, service_main)
.map_err(|e| e.display_chain_with_msg("Failed to start a service dispatcher"))
}
windows_service::define_windows_service!(service_main, handle_service_main);
pub fn handle_service_main(_arguments: Vec<OsString>) {
tracing::info!("Service started.");
let (event_tx, event_rx) = mpsc::channel();
// Register service event handler
let event_handler = move |control_event| -> ServiceControlHandlerResult {
match control_event {
// Notifies a service to report its current status information to the service
// control manager. Always return NO_ERROR even if not implemented.
ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
ServiceControl::Stop
| ServiceControl::Preshutdown
| ServiceControl::PowerEvent(_)
| ServiceControl::SessionChange(_) => {
event_tx.send(control_event).unwrap();
ServiceControlHandlerResult::NoError
}
_ => ServiceControlHandlerResult::NotImplemented,
}
};
let status_handle = match service_control_handler::register(SERVICE_NAME, event_handler) {
Ok(handle) => handle,
Err(error) => {
tracing::error!(
"{}",
error.display_chain_with_msg("Failed to register a service control handler")
);
return;
}
};
let mut persistent_service_status = PersistentServiceStatus::new(status_handle);
persistent_service_status
.set_pending_start(Duration::from_secs(1))
.unwrap();
let runtime = create_runtime();
let runtime = match runtime {
Err(error) => {
tracing::error!("{}", error.display_chain());
persistent_service_status
.set_stopped(ServiceExitCode::ServiceSpecific(1))
.unwrap();
return;
}
Ok(runtime) => runtime,
};
let shutdown_manager = ShutdownManager::new();
let result = runtime.block_on(crate::create_daemon(&shutdown_manager));
let result = if let Ok(daemon) = result {
let (sc_shutdown_tx, sc_shutdown_rx) = mpsc::channel();
// Register monitor that translates `ServiceControl` to Daemon events
start_event_monitor(persistent_service_status.clone(), sc_shutdown_tx, event_rx);
runtime.block_on(shutdown_manager.register_signal_handler_windows(sc_shutdown_rx));
persistent_service_status.set_running().unwrap();
Ok(runtime.block_on(daemon.run()))
} else {
result.map(|_| ())
};
let exit_code = match result {
Ok(()) => {
tracing::info!("Stopping service");
ServiceExitCode::default()
}
Err(error) => {
tracing::error!("{}", error);
ServiceExitCode::ServiceSpecific(1)
}
};
persistent_service_status.set_stopped(exit_code).unwrap();
}
/// Start event monitor thread that polls for `ServiceControl` and translates them into calls to
/// Daemon.
fn start_event_monitor(
persistent_service_status: PersistentServiceStatus,
sc_shutdown_tx: mpsc::Sender<()>,
event_rx: mpsc::Receiver<ServiceControl>,
) -> thread::JoinHandle<()> {
thread::spawn(move || {
let mut shutdown_handle = ServiceShutdownHandle {
persistent_service_status,
sc_shutdown_tx,
};
for event in event_rx {
match event {
ServiceControl::Stop | ServiceControl::Preshutdown => {
// If the daemon is closing due to the system shutting down,
// keep blocking traffic after the daemon exits.
shutdown_handle.shutdown(event == ServiceControl::Preshutdown);
}
_ => (),
}
}
})
}
#[derive(Clone)]
struct ServiceShutdownHandle {
persistent_service_status: PersistentServiceStatus,
sc_shutdown_tx: mpsc::Sender<()>,
}
impl ServiceShutdownHandle {
fn shutdown(&mut self, is_system_shutdown: bool) {
tracing::info!("is_system_shutdown: {is_system_shutdown}");
self.persistent_service_status
.set_pending_stop(Duration::from_secs(10))
.unwrap();
if let Err(e) = self.sc_shutdown_tx.send(()) {
tracing::error!("Failed to send shutdown event to daemon from service: {e}");
}
}
}
/// Service status helper with persistent checkpoint counter.
#[derive(Debug, Clone)]
struct PersistentServiceStatus {
status_handle: ServiceStatusHandle,
checkpoint_counter: Arc<AtomicUsize>,
}
impl PersistentServiceStatus {
fn new(status_handle: ServiceStatusHandle) -> Self {
PersistentServiceStatus {
status_handle,
checkpoint_counter: Arc::new(AtomicUsize::new(1)),
}
}
/// Tell the system that the service is pending start and provide the time estimate until
/// initialization is complete.
fn set_pending_start(&mut self, wait_hint: Duration) -> windows_service::Result<()> {
self.report_status(
ServiceState::StartPending,
wait_hint,
ServiceExitCode::default(),
)
}
/// Tell the system that the service is running.
fn set_running(&mut self) -> windows_service::Result<()> {
self.report_status(
ServiceState::Running,
Duration::default(),
ServiceExitCode::default(),
)
}
/// Tell the system that the service is pending stop and provide the time estimate until the
/// service is stopped.
fn set_pending_stop(&mut self, wait_hint: Duration) -> windows_service::Result<()> {
self.report_status(
ServiceState::StopPending,
wait_hint,
ServiceExitCode::default(),
)
}
/// Tell the system that the service is stopped and provide the exit code.
fn set_stopped(&mut self, exit_code: ServiceExitCode) -> windows_service::Result<()> {
self.report_status(ServiceState::Stopped, Duration::default(), exit_code)
}
/// Private helper to report the service status update.
fn report_status(
&mut self,
next_state: ServiceState,
wait_hint: Duration,
exit_code: ServiceExitCode,
) -> windows_service::Result<()> {
// Automatically bump the checkpoint when updating the pending events to tell the system
// that the service is making a progress in transition from pending to final state.
// `wait_hint` should reflect the estimated time for transition to complete.
let checkpoint = match next_state {
ServiceState::StartPending
| ServiceState::StopPending
| ServiceState::ContinuePending
| ServiceState::PausePending => self.checkpoint_counter.fetch_add(1, Ordering::SeqCst),
_ => 0,
};
let service_status = ServiceStatus {
service_type: SERVICE_TYPE,
current_state: next_state,
controls_accepted: accepted_controls_by_state(next_state),
exit_code,
checkpoint: checkpoint as u32,
wait_hint,
process_id: None,
};
tracing::debug!(
"Update service status: {:?}, checkpoint: {}, wait_hint: {:?}",
service_status.current_state,
service_status.checkpoint,
service_status.wait_hint
);
self.status_handle.set_service_status(service_status)
}
}
/// Returns the list of accepted service events at each stage of the service lifecycle.
fn accepted_controls_by_state(state: ServiceState) -> ServiceControlAccept {
let always_accepted = ServiceControlAccept::POWER_EVENT | ServiceControlAccept::SESSION_CHANGE;
match state {
ServiceState::StartPending | ServiceState::PausePending | ServiceState::ContinuePending => {
ServiceControlAccept::empty()
}
ServiceState::Running => {
always_accepted | ServiceControlAccept::STOP | ServiceControlAccept::PRESHUTDOWN
}
ServiceState::Paused => {
always_accepted | ServiceControlAccept::STOP | ServiceControlAccept::PRESHUTDOWN
}
ServiceState::StopPending | ServiceState::Stopped => ServiceControlAccept::empty(),
}
}
@@ -0,0 +1,68 @@
use nymvpn_entity::token::Entity as Token;
use nymvpn_migration::{
sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set},
DbErr,
};
use nymvpn_server::auth::TokenProvider;
#[derive(Debug, Clone)]
pub struct TokenStorage {
db: DatabaseConnection,
}
impl TokenStorage {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn get_token(&self) -> Result<Option<String>, DbErr> {
let token = Token::find().one(&self.db).await?;
Ok(token.map(|t| t.token))
}
pub async fn save_token(&self, token: String) -> Result<(), DbErr> {
let token = nymvpn_entity::token::ActiveModel {
token: Set(token),
..Default::default()
};
let new_token = token.insert(&self.db).await?;
// delete previous tokens
let deleted = Token::delete_many()
.filter(nymvpn_entity::token::Column::Id.lt(new_token.id))
.exec(&self.db)
.await?;
tracing::info!(
"new token saved; deleted old tokens #{}",
deleted.rows_affected
);
Ok(())
}
pub async fn remove_all(&self) -> Result<(), DbErr> {
let deleted = Token::delete_many().exec(&self.db).await?;
tracing::info!("deleted tokens #{}", deleted.rows_affected);
Ok(())
}
}
#[async_trait::async_trait]
impl TokenProvider for TokenStorage {
async fn bearer_token(&self) -> Option<String> {
let token = self
.get_token()
.await
.map_err(|e| tracing::error!("failed to get token from db: {e}"));
match token {
Ok(token) => token,
Err(_) => None,
}
}
}
+125
View File
@@ -0,0 +1,125 @@
use talpid_core::tunnel_state_machine::TunnelParametersGenerator;
use talpid_types::net::{
all_of_the_internet,
wireguard::{ConnectionConfig, PeerConfig, PublicKey, TunnelConfig, TunnelOptions},
GenericTunnelOptions, TunnelParameters,
};
use nymvpn_entity::device::Entity as Device;
use nymvpn_entity::vpn_session::Entity as VpnSession;
use nymvpn_migration::sea_orm::{DatabaseConnection, EntityTrait};
use nymvpn_types::device::DeviceDetails;
use crate::db::Db;
pub struct ParameterGenerator {
db: Db,
}
impl ParameterGenerator {
pub fn new(db: Db) -> Self {
Self { db }
}
}
async fn generate_parameter(
db: DatabaseConnection,
) -> Result<talpid_types::net::TunnelParameters, String> {
let vpn_session = VpnSession::find()
.one(&db)
.await
.map_err(|e| e.to_string())?
.ok_or("no vpn session found during parameter generation")?;
let device = Device::find()
.one(&db)
.await
.map_err(|e| e.to_string())?
.ok_or("no device found during parameter generation")?;
let device_details = DeviceDetails::try_from(device)?;
Ok(TunnelParameters::Wireguard(
talpid_types::net::wireguard::TunnelParameters {
connection: ConnectionConfig {
tunnel: TunnelConfig {
private_key: device_details.wireguard_meta.private_key,
addresses: vec![std::net::IpAddr::V4(
device_details
.wireguard_meta
.device_addresses
.ok_or("no device addresses found in wireguard metadata")?
.ipv4_address,
)],
},
peer: PeerConfig {
public_key: PublicKey::from_base64(
&vpn_session
.server_public_key
.ok_or("no server public key found in vpn session model")?,
)
.map_err(|e| format!("failed to convert public key from base64: {e:?}"))?,
allowed_ips: all_of_the_internet(),
endpoint: vpn_session
.server_ipv4_endpoint
.ok_or("no server ipv4 endpoint found in vpn session model")?
.parse()
.map_err(|e| {
format!(
"failed to parse server ipv4 endpoint in vpn session model: {e:?}"
)
})?,
psk: None,
},
exit_peer: None,
ipv4_gateway: vpn_session
.server_private_ipv4
.ok_or("no server private ipv4 found in vpn session model")?
.parse()
.map_err(|e| {
format!("failed to parse server ipv4 in vpn session model: {e:?}")
})?,
ipv6_gateway: None,
#[cfg(target_os = "linux")]
fwmark: Some(nymvpn_types::TUNNEL_FWMARK),
},
options: TunnelOptions {
mtu: None,
quantum_resistant: false,
#[cfg(windows)]
use_wireguard_nt: true,
},
generic_options: GenericTunnelOptions { enable_ipv6: true },
obfuscation: None,
},
))
}
impl TunnelParametersGenerator for ParameterGenerator {
fn generate(
&mut self,
_retry_attempt: u32,
) -> std::pin::Pin<
Box<
dyn futures::Future<
Output = Result<
talpid_types::net::TunnelParameters,
talpid_types::tunnel::ParameterGenerationError,
>,
>,
>,
> {
let db = self.db.connection();
Box::pin(async move {
let parameters = generate_parameter(db).await;
match parameters {
Ok(parameters) => Ok(parameters),
Err(e) => {
tracing::error!("TunnelParameterGenerator: {e}");
// return placeholder error
Err(talpid_types::tunnel::ParameterGenerationError::NoMatchingRelay)
}
}
})
}
}
@@ -0,0 +1,12 @@
pub async fn already_running() -> bool {
match nymvpn_controller::new_grpc_client().await {
Ok(_) => true,
Err(e) => {
tracing::info!(
"cannot connect to GRPC controller({}), assuming none running",
e.to_string()
);
false
}
}
}
@@ -0,0 +1,298 @@
use std::collections::HashMap;
use tokio::sync::{mpsc, oneshot};
use nymvpn_server::{auth::TokenProvider, ServerApi};
use nymvpn_types::{
location::Location,
nymvpn_server::{
Accepted, ClientConnected, EndSession, Ended, NewSession, VpnSessionStatusRequest,
},
};
use uuid::Uuid;
use crate::{daemon::DaemonEventSender, AckTx, ResponseTx};
use super::watcher::WatcherFactory;
pub enum VpnSessionCommand {
NewSession(ResponseTx<Accepted, VpnSessionError>, NewSession),
EndSession(ResponseTx<Ended, VpnSessionError>, EndSession),
ClientConnected(ResponseTx<(), VpnSessionError>, ClientConnected),
ListLocations(ResponseTx<Vec<Location>, VpnSessionError>),
Shutdown(AckTx),
}
#[derive(Debug, thiserror::Error)]
pub enum VpnSessionError {
#[error("vpn session service is unavailable")]
VpnSessionServiceDown,
#[error("error connecting to server: {0}")]
Connection(#[from] tonic::transport::Error),
#[error("server error: {0}")]
Server(#[from] tonic::Status),
}
#[derive(Debug, Clone)]
pub struct VpnSessionHandler {
tx: mpsc::UnboundedSender<VpnSessionCommand>,
}
impl VpnSessionHandler {
pub async fn start<P: TokenProvider + 'static>(
daemon_tx: DaemonEventSender,
token_provider: P,
) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let vpn_session_service = VpnSessionService::new(rx, daemon_tx, token_provider);
tokio::spawn(async move { vpn_session_service.run().await });
Self { tx }
}
pub async fn shutdown(&self) -> Result<(), VpnSessionError> {
let (tx, rx) = oneshot::channel();
self.tx
.send(VpnSessionCommand::Shutdown(tx))
.map_err(|_e| VpnSessionError::VpnSessionServiceDown)?;
rx.await
.map_err(|_e| VpnSessionError::VpnSessionServiceDown)?;
Ok(())
}
pub async fn new_session(
&self,
new_session: nymvpn_types::nymvpn_server::NewSession,
) -> Result<Accepted, VpnSessionError> {
self.send_command(|tx| VpnSessionCommand::NewSession(tx, new_session))
.await
}
pub async fn end_session(
&self,
end_session: nymvpn_types::nymvpn_server::EndSession,
) -> Result<Ended, VpnSessionError> {
self.send_command(|tx| VpnSessionCommand::EndSession(tx, end_session))
.await
}
pub async fn client_connected(
&self,
client_connected: ClientConnected,
) -> Result<(), VpnSessionError> {
self.send_command(|tx| VpnSessionCommand::ClientConnected(tx, client_connected))
.await
}
pub async fn list_locations(&self) -> Result<Vec<Location>, VpnSessionError> {
self.send_command(|tx| VpnSessionCommand::ListLocations(tx))
.await
}
pub async fn send_command<T>(
&self,
make_cmd: impl FnOnce(ResponseTx<T, VpnSessionError>) -> VpnSessionCommand,
) -> Result<T, VpnSessionError> {
let (tx, rx) = oneshot::channel();
self.tx
.send(make_cmd(tx))
.map_err(|_| VpnSessionError::VpnSessionServiceDown)?;
rx.await
.map_err(|_| VpnSessionError::VpnSessionServiceDown)?
}
}
pub struct VpnSessionService<P: TokenProvider> {
daemon_tx: DaemonEventSender,
receiver: mpsc::UnboundedReceiver<VpnSessionCommand>,
token_provider: P,
shutdown_tx: Option<AckTx>,
watcher_shutdown_txs: HashMap<Uuid, oneshot::Sender<()>>,
}
impl<P: TokenProvider + 'static> VpnSessionService<P> {
pub fn new(
receiver: mpsc::UnboundedReceiver<VpnSessionCommand>,
daemon_tx: DaemonEventSender,
token_provider: P,
) -> Self {
Self {
daemon_tx,
receiver,
token_provider,
shutdown_tx: None,
watcher_shutdown_txs: Default::default(),
}
}
pub async fn run(mut self) {
while let Some(command) = self.receiver.recv().await {
self.handle_command(command).await;
if self.shutdown_tx.is_some() {
break;
}
}
if self.shutdown_tx.is_some() {
// stop all watchers
let _ = self
.watcher_shutdown_txs
.into_iter()
.map(|(_request_id, tx)| tx.send(()));
// ack shutdown
if let Err(_) = self.shutdown_tx.unwrap().send(()) {
tracing::error!("failed to ack vpn session service shutdown");
};
}
tracing::info!("vpn session service stopped");
}
async fn handle_command(&mut self, command: VpnSessionCommand) {
match command {
VpnSessionCommand::NewSession(tx, new_session) => {
self.on_new_session(tx, new_session).await
}
VpnSessionCommand::EndSession(tx, end_session) => {
self.on_end_session(tx, end_session).await
}
VpnSessionCommand::ClientConnected(tx, client_connected) => {
self.on_client_connected(tx, client_connected).await
}
VpnSessionCommand::Shutdown(ack_tx) => self.shutdown_tx = Some(ack_tx),
VpnSessionCommand::ListLocations(tx) => self.on_list_locations(tx).await,
}
}
async fn on_new_session_inner(
token_provider: impl TokenProvider + 'static,
new_session: NewSession,
) -> Result<Accepted, VpnSessionError> {
let mut nymvpn_service = ServerApi::new(token_provider).await?;
Ok(nymvpn_service.new_session(new_session).await?)
}
async fn on_new_session(
&mut self,
tx: ResponseTx<Accepted, VpnSessionError>,
new_session: NewSession,
) {
let token_provider = self.token_provider.clone();
let accepted = Self::on_new_session_inner(token_provider, new_session.clone()).await;
// if accepted start a watcher
if let Ok(accepted) = &accepted {
let vpn_session_status_request = VpnSessionStatusRequest {
request_id: new_session.request_id,
vpn_session_uuid: accepted.vpn_session_uuid,
device_unique_id: new_session.device_unique_id,
};
self.start_watcher(vpn_session_status_request).await;
}
Self::oneshot_send(tx, accepted, "on_list_locations");
}
async fn on_end_session_inner(
token_provider: impl TokenProvider + 'static,
end_session: EndSession,
) -> Result<Ended, VpnSessionError> {
let mut nymvpn_service = ServerApi::new(token_provider).await?;
Ok(nymvpn_service.end_session(end_session).await?)
}
async fn start_watcher(&mut self, vpn_session_status_request: VpnSessionStatusRequest) {
let daemon_tx = self.daemon_tx.clone();
let token_provider = self.token_provider.clone();
let (shutdown_tx, shutdown_rx) = oneshot::channel();
self.watcher_shutdown_txs
.insert(vpn_session_status_request.request_id, shutdown_tx);
tokio::spawn(async move {
WatcherFactory::start(
vpn_session_status_request,
daemon_tx,
shutdown_rx,
token_provider,
)
.await;
});
}
fn stop_watcher(&mut self, request_id: &Uuid) {
let _ = self
.watcher_shutdown_txs
.remove_entry(request_id)
.map(|(_request_id, tx)| tx.send(()));
}
async fn on_end_session(
&mut self,
tx: ResponseTx<Ended, VpnSessionError>,
end_session: EndSession,
) {
// stop watcher if any
self.stop_watcher(&end_session.request_id);
let token_provider = self.token_provider.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
Self::on_end_session_inner(token_provider, end_session).await,
"on_end_session",
)
});
}
async fn on_client_connected_inner(
token_provider: impl TokenProvider + 'static,
client_connected: ClientConnected,
) -> Result<(), VpnSessionError> {
let mut nymvpn_service = ServerApi::new(token_provider).await?;
Ok(nymvpn_service.client_connected(client_connected).await?)
}
async fn on_client_connected(
&mut self,
tx: ResponseTx<(), VpnSessionError>,
client_connected: ClientConnected,
) {
// stop watcher if any
self.stop_watcher(&client_connected.request_id);
let token_provider = self.token_provider.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
Self::on_client_connected_inner(token_provider, client_connected).await,
"on_client_connected",
)
});
}
async fn on_list_locations_inner(
token_provider: impl TokenProvider + 'static,
) -> Result<Vec<Location>, VpnSessionError> {
let mut nymvpn_service = ServerApi::new(token_provider).await?;
let locations = nymvpn_service.list_locations().await?;
Ok(locations)
}
async fn on_list_locations(&self, tx: ResponseTx<Vec<Location>, VpnSessionError>) {
let token_provider = self.token_provider.clone();
tokio::spawn(async move {
Self::oneshot_send(
tx,
Self::on_list_locations_inner(token_provider).await,
"on_list_locations",
)
});
}
fn oneshot_send<T>(tx: oneshot::Sender<T>, t: T, msg: &'static str) {
if tx.send(t).is_err() {
tracing::warn!("Failed to respond from VpnSessionService {}", msg);
}
}
}
@@ -0,0 +1,4 @@
pub mod handler;
pub mod reclaimer;
pub mod storage;
pub mod watcher;
@@ -0,0 +1,107 @@
use tokio::time::{self, Duration};
use nymvpn_types::nymvpn_server::EndSession;
use crate::{device::storage::DeviceStorage, shutdown::Shutdown};
use super::{handler::VpnSessionHandler, storage::VpnSessionStorage};
pub struct ReclaimerCreator;
impl ReclaimerCreator {
pub async fn start(
session_storage: VpnSessionStorage,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
shutdown: Shutdown,
) {
let reclaimer = Reclaimer::new(
session_storage,
device_storage,
vpn_session_handler,
shutdown,
);
tokio::spawn(async move {
reclaimer.run().await;
});
}
}
pub struct Reclaimer {
session_storage: VpnSessionStorage,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
shutdown: Shutdown,
}
impl Reclaimer {
pub fn new(
session_storage: VpnSessionStorage,
device_storage: DeviceStorage,
vpn_session_handler: VpnSessionHandler,
shutdown: Shutdown,
) -> Self {
Self {
session_storage,
device_storage,
vpn_session_handler,
shutdown,
}
}
async fn reclaim(&mut self) {
// Delay to avoid race when the session was successfully ended just now.
time::sleep(Duration::from_millis(300)).await;
if let Ok(sessions) = self.session_storage.to_reclaim().await {
if let Ok(Some(device_details)) = self.device_storage.get_device().await {
for session in sessions {
let end_session = EndSession {
request_id: session.request_id,
device_unique_id: device_details.unique_id,
vpn_session_uuid: session.vpn_session_id,
reason: "reclaimed".into(),
};
match self.vpn_session_handler.end_session(end_session).await {
Ok(_) => {
tracing::info!("Reclaimed: {session}");
let _ = self.session_storage.delete(session.request_id).await;
}
Err(e) => {
match e {
crate::vpn_session::handler::VpnSessionError::VpnSessionServiceDown
| crate::vpn_session::handler::VpnSessionError::Connection(_) => {
// no-op reclaimer would re-run again
}
crate::vpn_session::handler::VpnSessionError::Server(status) => {
// did the best we can, delete it from local storage
// this could happen when device is signed out in middle of reclaiming
// signing out ends all sessions on server so its good to delete locally.
tracing::info!(
"Did best to reclaim session: {session}; server status: {status}"
);
let _ = self.session_storage.delete(session.request_id).await;
}
}
}
}
}
}
};
}
pub async fn run(mut self) {
let mut duration = Duration::from_secs(1);
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = time::sleep(duration) => {
self.reclaim().await;
duration = Duration::from_secs(60);
}
_ = self.shutdown.recv() => {
tracing::info!("Reclaimer shutting down");
}
}
}
}
}
@@ -0,0 +1,476 @@
use std::fmt::Display;
use chrono::Utc;
use talpid_core::tunnel_state_machine::TunnelCommand;
use talpid_types::tunnel::{ErrorState, TunnelStateTransition};
use nymvpn_migration::{
sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set},
DbErr, Expr,
};
use nymvpn_types::{
location::Location,
notification::{Notification, NotificationType},
nymvpn_server::{Accepted, VpnSessionStatus},
vpn_session::VpnStatus,
};
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct VpnSessionStorage {
db: DatabaseConnection,
}
pub struct VpnSessionStatusProcessed {
pub vpn_status: Option<VpnStatus>,
pub notification: Option<Notification>,
}
pub struct TunnelTransitionProcessed {
pub vpn_status: VpnStatus,
pub tunnel_command: Option<TunnelCommand>,
pub end_session: Option<String>,
pub notification: Option<Notification>,
pub client_connected: Option<SessionInfo>,
}
#[derive(Clone)]
pub struct SessionInfo {
pub request_id: Uuid,
pub vpn_session_id: Uuid,
}
pub enum StorageServerStatus {
Accepted,
Failed,
ServerCreated,
ServerRunning,
ServerReady,
ClientConnected,
Ended,
}
impl From<StorageServerStatus> for String {
fn from(value: StorageServerStatus) -> Self {
match value {
StorageServerStatus::Accepted => "Accepted",
StorageServerStatus::Failed => "Failed",
StorageServerStatus::ServerCreated => "ServerCreated",
StorageServerStatus::ServerRunning => "ServerRunning",
StorageServerStatus::ServerReady => "ServerReady",
StorageServerStatus::ClientConnected => "ClientConnected",
StorageServerStatus::Ended => "Ended",
}
.to_owned()
}
}
impl Display for SessionInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SessionInfo request_id: {}, vpn_session_uuid: {}",
self.request_id, self.vpn_session_id
)
}
}
impl VpnSessionStorage {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
pub async fn new_session(&self, location: Location) -> Result<Uuid, DbErr> {
let request_id = Uuid::new_v4();
let vpn_session = nymvpn_entity::vpn_session::ActiveModel {
request_id: Set(request_id.to_string()),
location_code: Set(location.code),
location_city: Set(location.city),
location_city_code: Set(location.city_code),
location_country: Set(location.country),
location_country_code: Set(location.country_code),
location_state: Set(location.state),
location_state_code: Set(location.state_code),
server_status: Set(None),
session_uuid: Set(None),
server_ipv4_endpoint: Set(None),
server_private_ipv4: Set(None),
server_public_key: Set(None),
requested_at: Set(Utc::now().to_rfc3339()),
mark_for_deletion: Set(false),
};
let _vpn_session = vpn_session.insert(&self.db).await?;
Ok(request_id)
}
pub async fn end_session(&self) -> Result<Option<SessionInfo>, DbErr> {
let vpn_session = nymvpn_entity::vpn_session::Entity::find()
.filter(nymvpn_entity::vpn_session::Column::MarkForDeletion.eq(false))
.one(&self.db)
.await?;
if let Some(vpn_session) = vpn_session {
// mark for deletion
let marked_result = nymvpn_entity::vpn_session::Entity::update_many()
.filter(
nymvpn_entity::vpn_session::Column::RequestId.eq(vpn_session.request_id.clone()),
)
.col_expr(
nymvpn_entity::vpn_session::Column::MarkForDeletion,
Expr::value(true),
)
.exec(&self.db)
.await?;
tracing::info!(
"marked for deletion: request_id: {} vpn_session_uuid: {:?}. Rows: {}",
&vpn_session.request_id,
&vpn_session.session_uuid,
marked_result.rows_affected
);
if let Some(vpn_session_uuid) = vpn_session.session_uuid {
return Ok(Some(SessionInfo {
request_id: Uuid::parse_str(&vpn_session.request_id).unwrap(),
vpn_session_id: Uuid::parse_str(&vpn_session_uuid).unwrap(),
}));
}
} else {
tracing::info!("No session found to mark for deletion");
}
Ok(None)
}
pub async fn reclaim(&self) -> Result<(), DbErr> {
let marked = nymvpn_entity::vpn_session::Entity::update_many()
.col_expr(
nymvpn_entity::vpn_session::Column::MarkForDeletion,
Expr::value(true),
)
.exec(&self.db)
.await?;
tracing::info!(
"reclaimer marked for deletion count: {}",
marked.rows_affected
);
Ok(())
}
pub async fn to_reclaim(&self) -> Result<Vec<SessionInfo>, DbErr> {
nymvpn_entity::vpn_session::Entity::find()
.filter(nymvpn_entity::vpn_session::Column::MarkForDeletion.eq(true))
.all(&self.db)
.await
.map(|sessions| {
sessions
.into_iter()
.filter(|session| {
session.session_uuid.is_some()
&& Uuid::parse_str(&session.session_uuid.as_ref().unwrap()).is_ok()
&& Uuid::parse_str(&session.request_id).is_ok()
})
.map(|session| SessionInfo {
request_id: Uuid::parse_str(&session.request_id).unwrap(),
vpn_session_id: Uuid::parse_str(&session.session_uuid.unwrap()).unwrap(),
})
.collect()
})
}
pub async fn update_on_accepted(&self, accepted: Accepted) -> Result<(), DbErr> {
let _update_result = nymvpn_entity::vpn_session::Entity::update_many()
.col_expr(
nymvpn_entity::vpn_session::Column::ServerStatus,
Expr::value(String::from(StorageServerStatus::Accepted)),
)
.col_expr(
nymvpn_entity::vpn_session::Column::SessionUuid,
Expr::value(accepted.vpn_session_uuid.to_string()),
)
.filter(
nymvpn_entity::vpn_session::Column::RequestId.eq(accepted.request_id.to_string()),
)
.exec(&self.db)
.await?;
Ok(())
}
pub async fn delete(&self, request_id: Uuid) -> Result<(), DbErr> {
let delete_result = nymvpn_entity::vpn_session::Entity::delete_many()
.filter(nymvpn_entity::vpn_session::Column::RequestId.eq(request_id.to_string()))
.exec(&self.db)
.await?;
tracing::info!(
"deleted rows for {request_id}: {}",
delete_result.rows_affected
);
Ok(())
}
fn get_session_info(vpn_session_status: &VpnSessionStatus) -> SessionInfo {
match vpn_session_status {
VpnSessionStatus::Accepted(accepted) => SessionInfo {
request_id: accepted.request_id,
vpn_session_id: accepted.vpn_session_uuid,
},
VpnSessionStatus::Failed(failed) => SessionInfo {
request_id: failed.request_id,
vpn_session_id: failed.vpn_session_uuid,
},
VpnSessionStatus::ServerCreated(server_created) => SessionInfo {
request_id: server_created.request_id,
vpn_session_id: server_created.vpn_session_uuid,
},
VpnSessionStatus::ServerRunning(server_running) => SessionInfo {
request_id: server_running.request_id,
vpn_session_id: server_running.vpn_session_uuid,
},
VpnSessionStatus::ServerReady(server_ready) => SessionInfo {
request_id: server_ready.request_id,
vpn_session_id: server_ready.vpn_session_uuid,
},
VpnSessionStatus::ClientConnected(client_connected) => SessionInfo {
request_id: client_connected.request_id,
vpn_session_id: client_connected.vpn_session_uuid,
},
VpnSessionStatus::Ended(ended) => SessionInfo {
request_id: ended.request_id,
vpn_session_id: ended.vpn_session_uuid,
},
}
}
// must be idempotent as same server update can arrive multiple times
pub async fn updated_server_status(
&self,
vpn_session_status: VpnSessionStatus,
) -> Result<VpnSessionStatusProcessed, DbErr> {
tracing::info!("Received updated status from server: {vpn_session_status}");
let session_info = Self::get_session_info(&vpn_session_status);
let vpn_session =
nymvpn_entity::vpn_session::Entity::find_by_id(session_info.request_id.to_string())
.filter(nymvpn_entity::vpn_session::Column::MarkForDeletion.eq(false))
.one(&self.db)
.await?;
if vpn_session.is_none() {
tracing::info!("vpn session not found locally");
tracing::info!("dropping status update from server: {vpn_session_status}");
Ok(VpnSessionStatusProcessed {
vpn_status: None,
notification: None,
})
} else {
let vpn_session = vpn_session.unwrap();
let location: Location = vpn_session.clone().into();
let mut vpn_session: nymvpn_entity::vpn_session::ActiveModel = vpn_session.into();
// update server status and other fields in DB
let (vpn_status, notification) = match vpn_session_status {
VpnSessionStatus::Accepted(_) => {
// This is initial state client knows so nothing to do here
vpn_session.server_status =
Set(Some(String::from(StorageServerStatus::Accepted)));
vpn_session.update(&self.db).await?;
(None, None)
}
VpnSessionStatus::Failed(_) => {
// server could not be provisioned, create client notification, delete record from DB
vpn_session.delete(&self.db).await?;
(
Some(VpnStatus::Disconnected),
Some(Notification {
id: format!("failed-{}", session_info.request_id),
message: "Server could not be provisioned, please try again later"
.into(),
notification_type:
nymvpn_types::notification::NotificationType::ServerFailed,
timestamp: Utc::now(),
}),
)
}
VpnSessionStatus::ServerCreated(_) => {
vpn_session.server_status =
Set(Some(String::from(StorageServerStatus::ServerCreated)));
vpn_session.update(&self.db).await?;
(Some(VpnStatus::ServerCreated(location)), None)
}
VpnSessionStatus::ServerRunning(_) => {
vpn_session.server_status =
Set(Some(String::from(StorageServerStatus::ServerRunning)));
vpn_session.update(&self.db).await?;
(Some(VpnStatus::ServerRunning(location)), None)
}
VpnSessionStatus::ServerReady(server_ready) => {
vpn_session.server_status =
Set(Some(String::from(StorageServerStatus::ServerReady)));
vpn_session.server_ipv4_endpoint =
Set(Some(server_ready.ipv4_endpoint.to_string()));
vpn_session.server_private_ipv4 =
Set(Some(server_ready.private_ipv4.to_string()));
vpn_session.server_public_key = Set(Some(server_ready.public_key));
vpn_session.update(&self.db).await?;
(Some(VpnStatus::ServerReady(location)), None)
}
VpnSessionStatus::ClientConnected(_) => {
vpn_session.server_status =
Set(Some(String::from(StorageServerStatus::ClientConnected)));
vpn_session.update(&self.db).await?;
(None, None)
}
VpnSessionStatus::Ended(_) => {
vpn_session.delete(&self.db).await?;
(Some(VpnStatus::Disconnected), None)
}
};
Ok(VpnSessionStatusProcessed {
vpn_status,
notification,
})
}
}
fn message_from_error(&self, error_state: &ErrorState) -> String {
format!("{}", error_state.cause())
}
// Process tunnel state transition to derive new state
// and possibly tunnel action in case of client side failures
pub async fn tunnel_state_transition(
&self,
transition: TunnelStateTransition,
current_state: VpnStatus,
) -> Result<TunnelTransitionProcessed, DbErr> {
// When a tunnel transition is received that means all vpn_session on server side
// transitioned to successfully state ServerReady. Process tunnel state knowing that
// if vpn_session is still not marked for delete, its ready.
let vpn_session = nymvpn_entity::vpn_session::Entity::find()
.filter(nymvpn_entity::vpn_session::Column::MarkForDeletion.eq(false))
.one(&self.db)
.await?;
let (vpn_status, tunnel_command, end_session, notification, client_connected) =
match vpn_session {
Some(vpn_session) => {
tracing::info!(
"vpn session status {:?} during tunnel transition",
vpn_session.server_status
);
let location: Location = vpn_session.clone().into();
match transition {
TunnelStateTransition::Disconnected => {
(VpnStatus::Disconnected, None, None, None, None)
}
TunnelStateTransition::Connecting(_) => {
(VpnStatus::Connecting(location), None, None, None, None)
}
TunnelStateTransition::Connected(_) => {
let mut vpn_session_updated: nymvpn_entity::vpn_session::ActiveModel =
vpn_session.clone().into();
vpn_session_updated.server_status =
Set(Some(String::from(StorageServerStatus::ClientConnected)));
vpn_session_updated.update(&self.db).await?;
(
VpnStatus::Connected(location, Utc::now()),
None,
None,
None,
Some(SessionInfo {
request_id: Uuid::parse_str(&vpn_session.request_id).unwrap(),
vpn_session_id: Uuid::parse_str(
&vpn_session.session_uuid.unwrap(),
)
.unwrap(),
}),
)
}
TunnelStateTransition::Disconnecting(_) => {
(VpnStatus::Disconnecting(location), None, None, None, None)
}
TunnelStateTransition::Error(error_state) => {
tracing::error!("tunnel errored: {error_state:?}");
(
VpnStatus::Disconnected,
Some(TunnelCommand::Disconnect),
Some(self.message_from_error(&error_state)),
Some(Notification {
id: format!("ce-{}", &vpn_session.request_id),
message: self.message_from_error(&error_state),
notification_type: NotificationType::ClientFailed,
timestamp: Utc::now(),
}),
None,
)
}
}
}
None => {
tracing::info!("No vpn session found during tunnel transition");
match transition {
TunnelStateTransition::Disconnected => {
(VpnStatus::Disconnected, None, None, None, None)
}
TunnelStateTransition::Connecting(_) => {
tracing::warn!(
"dropping connecting state transition as no vpn session found"
);
(VpnStatus::Disconnected, None, None, None, None)
}
TunnelStateTransition::Connected(_) => {
tracing::warn!(
"dropping connected state transition as no vpn session found"
);
(VpnStatus::Disconnected, None, None, None, None)
}
TunnelStateTransition::Disconnecting(_) => {
if let VpnStatus::Disconnecting(location) = current_state {
(VpnStatus::Disconnecting(location), None, None, None, None)
} else {
panic!("No vpn session found; current state is {current_state} and tunnel transitioned to disconnecting");
}
}
TunnelStateTransition::Error(error_state) => {
tracing::error!("tunnel errored: {error_state:?}");
(
VpnStatus::Disconnected,
Some(TunnelCommand::Disconnect),
Some(self.message_from_error(&error_state)),
Some(Notification {
id: "unknown".into(),
message: self.message_from_error(&error_state),
notification_type: NotificationType::ClientFailed,
timestamp: Utc::now(),
}),
None,
)
}
}
}
};
Ok(TunnelTransitionProcessed {
vpn_status,
tunnel_command,
end_session,
notification,
client_connected,
})
}
}
@@ -0,0 +1,123 @@
use tokio::sync::oneshot;
use tokio::time::{interval, Duration};
use nymvpn_server::auth::TokenProvider;
use nymvpn_server::ServerApi;
use nymvpn_types::nymvpn_server::{VpnSessionStatus, VpnSessionStatusRequest};
use crate::daemon::{DaemonEvent, DaemonEventSender};
pub struct Watcher<P: TokenProvider + 'static> {
daemon_tx: DaemonEventSender,
vpn_session_status_request: VpnSessionStatusRequest,
token_provider: P,
last_watch: Option<VpnSessionStatus>,
}
impl<P: TokenProvider + 'static> Watcher<P> {
pub fn new(
vpn_session_status_request: VpnSessionStatusRequest,
daemon_tx: DaemonEventSender,
token_provider: P,
) -> Self {
Watcher {
vpn_session_status_request,
daemon_tx,
token_provider,
last_watch: None,
}
}
// return true if watch output reached "final" state
async fn watch_ended(&mut self) -> bool {
let token_provider = self.token_provider.clone();
let vpn_session_status_request = self.vpn_session_status_request.clone();
let nymvpn_service = ServerApi::new(token_provider).await;
match nymvpn_service {
Ok(mut nymvpn_service) => {
match nymvpn_service
.get_status(vpn_session_status_request.clone())
.await
{
Ok(vpn_session_status) => {
let last_vpn_session_status = std::mem::replace(
&mut self.last_watch,
Some(vpn_session_status.clone()),
);
if let Some(last_vpn_session_status) = last_vpn_session_status {
if last_vpn_session_status == vpn_session_status {
return false;
}
}
if let Err(e) = self
.daemon_tx
.send(DaemonEvent::VpnSessionStatus(vpn_session_status.clone()))
{
tracing::error!("Failed to notify daemon from watcher; ending watch for {vpn_session_status_request}: {e}");
return true;
}
match vpn_session_status {
nymvpn_types::nymvpn_server::VpnSessionStatus::Failed(_)
| nymvpn_types::nymvpn_server::VpnSessionStatus::ServerReady(_)
| nymvpn_types::nymvpn_server::VpnSessionStatus::ClientConnected(_)
| nymvpn_types::nymvpn_server::VpnSessionStatus::Ended(_) => {
tracing::info!("watcher end state received: {vpn_session_status}");
return true;
}
nymvpn_types::nymvpn_server::VpnSessionStatus::ServerCreated(_)
| nymvpn_types::nymvpn_server::VpnSessionStatus::ServerRunning(_)
| nymvpn_types::nymvpn_server::VpnSessionStatus::Accepted(_) => {}
};
}
Err(err) => {
// todo: this could be transient error? so we don't end the watch here
tracing::error!(
"watch received error from server for {vpn_session_status_request}: {}",
err.message()
)
}
}
}
Err(err) => {
// transient error: don't end the watch here
tracing::error!("failed to connect to nymvpn service from watcher for {vpn_session_status_request}: {err}");
}
}
false
}
pub async fn run(mut self, mut shutdown_rx: oneshot::Receiver<()>) {
let mut interval = interval(Duration::from_millis(1000));
loop {
tokio::select! {
_ = interval.tick() => {
if self.watch_ended().await {
break;
}
}
_ = &mut shutdown_rx => {
tracing::info!("watcher received shutdown");
break;
}
}
}
tracing::info!("watcher stopped");
}
}
pub struct WatcherFactory;
impl WatcherFactory {
pub async fn start(
vpn_session_status_request: VpnSessionStatusRequest,
daemon_tx: DaemonEventSender,
shutdown_rx: oneshot::Receiver<()>,
token_provider: impl TokenProvider + 'static,
) {
let watcher = Watcher::new(vpn_session_status_request, daemon_tx, token_provider);
tokio::spawn(async move { watcher.run(shutdown_rx).await });
}
}
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "nymvpn-entity"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0"
authors = ["Nym Technologies S.A."]
homepage = "https://nymvpn.net"
repository = "https://github.com/nymvpn/nymvpn-app"
[lib]
name = "nymvpn_entity"
path = "src/lib.rs"
[dependencies.sea-orm]
version = "0.11.2"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
"runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-sqlite",
]
[dependencies]
nymvpn-types = {path = "../nymvpn-types"}
uuid = { version = "1.3.1", features = ["v4", "serde"] }
serde_json = "1.0.96"
@@ -0,0 +1,54 @@
use serde_json::json;
use nymvpn_types::device::DeviceDetails;
use uuid::Uuid;
use crate::device::Model as Device;
impl TryFrom<Device> for DeviceDetails {
type Error = String;
fn try_from(value: Device) -> Result<Self, Self::Error> {
let wireguard_meta: nymvpn_types::wireguard::WireguardMetadata = match value.ipv4_address {
Some(ipv4_address) => serde_json::from_value(json!({
"private_key": value.private_key,
"device_addresses": {
"ipv4_address": ipv4_address
}
})),
None => serde_json::from_value(json!({
"private_key": value.private_key,
})),
}
.map_err(|e| format!("failed to read wireguard meta from db: {e}"))?;
Ok(Self {
name: value.name,
version: value.version,
arch: value.arch,
unique_id: Uuid::parse_str(&value.unique_id).map_err(|e| e.to_string())?,
device_type: value.device_type.as_str().try_into()?,
wireguard_meta,
created_at: value
.created_at
.parse()
.map_err(|e| format!("cannot convert created_at for device: {e}"))?,
})
}
}
impl From<DeviceDetails> for Device {
fn from(value: DeviceDetails) -> Self {
Self {
name: value.name,
version: value.version,
arch: value.arch,
unique_id: value.unique_id.to_string(),
device_type: value.device_type.into(),
private_key: value.wireguard_meta.private_key.to_base64(),
ipv4_address: value
.wireguard_meta
.device_addresses
.map(|da| da.ipv4_address.to_string()),
created_at: value.created_at.to_rfc3339(),
}
}
}
@@ -0,0 +1,27 @@
impl From<crate::vpn_session::Model> for nymvpn_types::location::Location {
fn from(value: crate::vpn_session::Model) -> Self {
Self {
code: value.location_code,
country: value.location_country,
country_code: value.location_country_code,
city: value.location_city,
city_code: value.location_city_code,
state: value.location_state,
state_code: value.location_state_code,
}
}
}
impl From<crate::recent_locations::Model> for nymvpn_types::location::Location {
fn from(value: crate::recent_locations::Model) -> Self {
Self {
code: value.code,
country: value.country,
country_code: value.country_code,
city: value.city,
city_code: value.city_code,
state: value.state,
state_code: value.state_code,
}
}
}
@@ -0,0 +1,2 @@
pub mod device;
pub mod location;
@@ -0,0 +1,22 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "device")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub unique_id: String,
pub name: String,
pub version: String,
pub arch: String,
pub device_type: String,
pub private_key: String,
pub ipv4_address: Option<String>,
pub created_at: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
+10
View File
@@ -0,0 +1,10 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
pub mod prelude;
pub mod device;
pub mod recent_locations;
pub mod token;
pub mod vpn_session;
pub mod conversions;
+5
View File
@@ -0,0 +1,5 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.3
pub mod prelude;
pub mod vpn_session;
@@ -0,0 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
pub use super::device::Entity as Device;
pub use super::recent_locations::Entity as RecentLocations;
pub use super::token::Entity as Token;
pub use super::vpn_session::Entity as VpnSession;
@@ -0,0 +1,22 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "recent_locations")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub code: String,
pub city: String,
pub city_code: String,
pub country: String,
pub country_code: String,
pub state: Option<String>,
pub state_code: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,16 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "token")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub token: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
@@ -0,0 +1,29 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "vpn_session")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub request_id: String,
pub location_code: String,
pub location_city: String,
pub location_city_code: String,
pub location_country: String,
pub location_country_code: String,
pub location_state: Option<String>,
pub location_state_code: Option<String>,
pub server_status: Option<String>,
pub session_uuid: Option<String>,
pub server_ipv4_endpoint: Option<String>,
pub server_private_ipv4: Option<String>,
pub server_public_key: Option<String>,
pub requested_at: String,
pub mark_for_deletion: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

Some files were not shown because too many files have changed in this diff Show More