Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 92fcb1b929 | |||
| db2005a4a0 | |||
| 7d41b0d2ba | |||
| 4314f53139 | |||
| 3e3f305f0b | |||
| dc33f078a1 | |||
| d76d1bb876 | |||
| c9ea25a157 | |||
| a79b91ae00 | |||
| 8e855960f9 | |||
| 4545f7e3e0 | |||
| 89c97c684e | |||
| 776693b0ef | |||
| 0eac90783d | |||
| e0dd29898a | |||
| 8e43a5ce1d | |||
| ef30cafaa4 | |||
| 80f81f1c4a | |||
| 3e98fee06e | |||
| 099d23b568 | |||
| 874aecb0a4 | |||
| acf9de0f74 | |||
| 064624d8ec | |||
| ced32da018 | |||
| ad5c0991c0 | |||
| 2c63518735 | |||
| 2c32ce8cf0 |
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
build/*
|
||||
data/*
|
||||
dist/*
|
||||
target/*
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"svg.preview.background": "editor"
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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*
|
||||
@@ -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>.
|
||||
@@ -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 -
|
||||
'''
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 883 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 545 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 545 KiB |
@@ -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 |
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
../nymvpn.conf.toml
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 ¤t_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))?)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
@@ -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 {}
|
||||