Compare commits

...

10 Commits

Author SHA1 Message Date
durch ea271d720d Naively comment out stuff 2023-09-06 19:06:22 +02:00
pierre 8d2e8b3d26 init 2023-09-06 18:36:01 +02:00
Jędrzej Stuczyński 2a87533b12 added 'open_proxy', 'enabled_statistics' and 'statistics_recipient' to NR config (#3839)
* added 'open_proxy', 'enabled_statistics' and 'statistics_recipient' to NR config

* Update template.rs

fixed missing quotation marks
2023-09-06 12:58:39 +02:00
Jon Häggblad 499fd8a91d Fix clippy unused import (#3848) 2023-09-06 11:24:17 +02:00
Mark Sinclair e083bfcfe4 Docs: post process to adjust URLs in index.html files for hosting in subdirectories (#3842)
* Docs: post process output to fix paths so that many mdbooks can be served from sub-directories

* Prevent theme from being modified

* Upload docs to Vercel

* Post process docs

* Process local links

* Docs: only process `index.html` files from the root,

All other files have the correct relative paths to serve assets properly and link to files relatively.

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2023-09-05 14:57:23 +01:00
Tommy Verrall 22c59be82c Release/v1.1.30 twix (#3836)
* changed last vers. checkout to master

* corrected path of config

* make binaries executable

* docs: typescript.md - changing variables

* docs: rust.md - changing variables

* docs: vesting-contract.md - changing variables

* docs: mixnet-contract.md - changing variables

* docs: all variables changed

* operators: all variables finished

* dev-portal: mixnet-integration.md - variable changed

* dev-portal: faq.md - variable changed

* dev-portal: moredo.md up to date w NC default

* dev-portal: telegram.md - added banner & minor fix

* dev-portal: matrix.md - added banner

* PR finished - ready for review and merge

* removed all instances of platform_release_version var

* removed all instances of platform_release_version var

* changed version bumper script: removed platform_release_version references

* changed comment

* updating changelog and bumping versions

---------

Co-authored-by: serinko <97586125+serinko@users.noreply.github.com>
Co-authored-by: mfahampshire <maxhampshire@pm.me>
Co-authored-by: mx <33262279+mfahampshire@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2023-09-05 11:58:21 +02:00
Tommy Verrall f17e7378f7 Merge pull request #3835 from nymtech/feature/vpn-dirs
feat: init vpn clients directories
2023-09-05 09:51:02 +02:00
pierre 1bb455675e init dirs 2023-09-05 09:24:24 +02:00
Mark Sinclair 73076a2b26 Merge branch 'feature/gh-actions-hash-release' into develop 2023-09-04 20:49:54 +01:00
Jon Häggblad 1e1bf25514 gateway: disconnect inactive duplicate clients (#3796)
* gateway: disconnect inactive duplicate clients

* wip: see if we can switch to single ping at a time

* Finish reworking ping pong request flow

* Use workspace version of tokio

* Bundle active client channels into struct

* Fix typo
2023-09-04 16:41:58 +02:00
181 changed files with 7442 additions and 205 deletions
+40
View File
@@ -38,6 +38,7 @@ jobs:
- name: Build all projects in documentation/ & move to ~/dist/docs/
run: cd documentation && ./build_all_to_dist.sh
continue-on-error: false
- name: Deploy branch master to dev
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -49,6 +50,7 @@ jobs:
REMOTE_USER: ${{ secrets.CD_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CD_WWW_REMOTE_TARGET }}/
EXCLUDE: "/node_modules/"
- name: Deploy branch master to prod
if: github.ref == 'refs/heads/master'
uses: easingthemes/ssh-deploy@main
@@ -60,6 +62,44 @@ jobs:
REMOTE_USER: ${{ secrets.CD_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CD_WWW_REMOTE_TARGET }}/
EXCLUDE: "/node_modules/"
- name: Post process
run: cd documentation && ./post_process.sh
continue-on-error: false
- name: Create Vercel project file
uses: mobiledevops/secret-to-file-action@v1
with:
base64-encoded-secret: ${{ secrets.VERCEL_PROJECT_JSON_BASE64 }}
filename: "project.json"
is-executable: true
working-directory: "./dist/docs/.vercel"
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment Information (preview)
if: github.ref != 'refs/heads/master'
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
working-directory: dist/docs
- name: Pull Vercel Environment Information (production)
if: github.ref == 'refs/heads/master'
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
working-directory: dist/docs
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
working-directory: dist/docs
- name: Deploy Project Artifacts to Vercel (preview)
if: github.ref != 'refs/heads/master'
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
working-directory: dist/docs
- name: Deploy Project Artifacts to Vercel (master)
if: github.ref == 'refs/heads/master'
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
working-directory: dist/docs
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
+14
View File
@@ -4,6 +4,20 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [v1.1.30-twix] (2023-09-05)
- geo_aware_provider: fix too much filtering of gateways ([#3826])
- network-requester: add description to config ([#3799])
- Speedy mode - selects gateway based on latency in medium / speedy mode ([#3770])
- Chore/enable versioning ([#3768])
- Create explorer-client and use in geo aware provider ([#3824])
[#3826]: https://github.com/nymtech/nym/pull/3826
[#3799]: https://github.com/nymtech/nym/pull/3799
[#3770]: https://github.com/nymtech/nym/issues/3770
[#3768]: https://github.com/nymtech/nym/pull/3768
[#3824]: https://github.com/nymtech/nym/pull/3824
## [v1.1.29-snickers] (2023-08-29)
- Add EXPLORER_API configurable url ([#3810])
Generated
+9 -9
View File
@@ -2768,7 +2768,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "explorer-api"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"chrono",
"clap 4.3.21",
@@ -5638,7 +5638,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.28"
version = "1.1.29"
dependencies = [
"actix-web",
"anyhow",
@@ -5785,7 +5785,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"anyhow",
"base64 0.13.1",
@@ -5858,7 +5858,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"clap 4.3.21",
"dirs 4.0.0",
@@ -6166,7 +6166,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"anyhow",
"async-trait",
@@ -6321,7 +6321,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.1.28"
version = "1.1.29"
dependencies = [
"anyhow",
"bs58 0.4.0",
@@ -6439,7 +6439,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"anyhow",
"async-file-watcher",
@@ -6486,7 +6486,7 @@ dependencies = [
[[package]]
name = "nym-network-statistics"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"dirs 4.0.0",
"log",
@@ -6651,7 +6651,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.27"
version = "1.1.28"
dependencies = [
"clap 4.3.21",
"lazy_static",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.27"
version = "1.1.28"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.27"
version = "1.1.28"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -224,6 +224,6 @@ impl RealMessagesController<OsRng> {
debug!("The reply controller has finished execution!");
});
ack_control.start_with_shutdown(shutdown, packet_type);
// ack_control.start_with_shutdown(shutdown, packet_type);
}
}
@@ -102,12 +102,12 @@ impl PacketRouter {
}
}
if !received_acks.is_empty() {
trace!("routing acks");
if let Err(err) = self.ack_sender.unbounded_send(received_acks) {
error!("failed to send ack: {err}");
};
}
// if !received_acks.is_empty() {
// trace!("routing acks");
// if let Err(err) = self.ack_sender.unbounded_send(received_acks) {
// error!("failed to send ack: {err}");
// };
// }
Ok(())
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ Each directory contains a readme with more information about running and contrib
* `operators` contains node setup and maintenance guides hosted at [https://nymtech.net/operators](https://nymtech.net/operators)
## Scripts
* `bump_versions.sh` allows you to update the `platform_release_version` and `wallet_release_version` variables in the `book.toml` of each mdbook project at once. You can also optionally update the `minimum_rust_version` as well. Helpful for lazy-updating when cutting a new version of the docs.
* `build_all_to_dist.sh` is used by the `ci-dev.yml` and `cd-dev.yml` scripts for building all mdbook projects and moving the rendered html to `../dist/` to be rsynced with various servers.
* `bump_versions.sh` allows you to update the ~~`platform_release_version` and~~ `wallet_release_version` variable~~s~~ in the `book.toml` of each mdbook project at once. You can also optionally update the `minimum_rust_version` as well. Helpful for lazy-updating when cutting a new version of the docs.
* `build_all_to_dist.sh` is used by the `ci-dev.yml` and `cd-dev.yml` scripts for building all mdbook projects and moving the rendered html to `../dist/` to be rsynced with various servers.
+9 -10
View File
@@ -1,21 +1,21 @@
#!/usr/bin/env bash
# this takes two args: platform release version and wallet release version.
# takes one manadatory arg and one optional arg: wallet release and minimum rust versions
# it then uses sed to bump them in the three book.toml files.
#
# e.g if the upcoming platform release was v1.1.29 and the release version 1.2.9 you'd run this as:
# `./bump_versions.sh "1.1.29" "1.2.9"`
# e.g if the upcoming wallet release version was 1.2.9 you'd run this as:
# `./bump_versions.sh "1.2.9"`
#
# you can also set the minumum rust version by passing an optional 3rd argument:
# `./bump_versions.sh "1.1.29" "1.2.9" "1.67"`
# you can also set the minumum rust version by passing an optional additional argument:
# `./bump_versions.sh "1.2.9" "1.67"`
# array of project dirs
declare -a projects=("docs" "dev-portal" "operators")
# check number of args passed
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ];
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ];
then
echo "failure: please pass at least 2 and at most 3 args: "
echo "./bump_version.sh <new platform_release_version> <new wallet_release_version> [OPTIONAL]<new minimum_rust_version>"
echo "failure: please pass at least 1 and at most 2 args: "
echo "./bump_version.sh <new wallet_release_version> [OPTIONAL]<new minimum_rust_version>"
exit 0
fi
@@ -29,8 +29,7 @@ else
for i in "${projects[@]}"
do
# sed the vars in the book.toml file for each project
echo "setting platform and wallet versions in $i/"
sed -i 's/platform_release_version =.*/platform_release_version = "'$1'"/' "$i"/book.toml
echo "setting wallet version in $i/"
sed -i 's/wallet_release_version =.*/wallet_release_version = "'$2'"/' "$i"/book.toml
if [ "$3" ]
then
+1 -1
View File
@@ -12,7 +12,7 @@ If you have built a project with Nym or are compiling and writing resources abou
## Variables
There are some variables that are shared across this book, such as the current latest software version.
Variables are denoted in the `.md` files wrapped in `{{}}` (e.g `{{platform_release_version}}` is the most recent release), and are located in the `book.toml` file under the `[preprocessor.variables.variables]` heading. If you are changing something like the software release version, minimum code versions in prerequisites, etc, **check in here first!**
Variables are denoted in the `.md` files wrapped in `{{}}` (e.g `{{wallet_release_version}}`), and are located in the `book.toml` file under the `[preprocessor.variables.variables]` heading. If you are changing something like the software release version, minimum code versions in prerequisites, etc, **check in here first!**
## Building
When working locally, it is recommended that you use `mdbook serve` to have a local version of the docs served on `localhost:3000`, with hot reloading on any changes made to files in the `src/` directory.
+1 -2
View File
@@ -38,7 +38,7 @@ chapter-line-height = "2em"
section-line-height = "1.5em"
# if true, never read and touch the files in theme dir
turn-off = false
turn-off = true
[preprocessor.admonish]
@@ -49,7 +49,6 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
# https://gitlab.com/tglman/mdbook-variables/
[preprocessor.variables.variables]
minimum_rust_version = "1.66"
platform_release_version = "1.1.29"
wallet_release_version = "1.2.8"
[preprocessor.last-changed]
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

@@ -110,7 +110,7 @@ Yes, it is supported.
Yes. Follow the instructions in the [Ledger support for Nyx documentation](https://nymtech.net/docs/nyx/ledger-live.html).
### Where can I find network details such as deployed smart contract addresses?
In the [`network defaults`](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/common/network-defaults/src/mainnet.rs) file.
In the [`network defaults`](https://github.com/nymtech/nym/blob/master/common/network-defaults/src/mainnet.rs) file.
## `NYM` Token
The token used to reward mixnet infrastructure operators - `NYM` - is one of the native tokens of the Nyx blockchain. The other token is `NYX`.
@@ -198,4 +198,4 @@ For the moment then yes, the mixnet is free to use. There are no limits on the a
No, although we do recommend that apps that wish to integrate look into running some of their own infrastructure such as gateways in order to assure uptime.
### How can I find out if an application is already supported by network requester services?
You can check the [default allowed list](https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt) file to see which application traffic is whitelisted by default. If the domain is present on that list, it means that existing [network requesters](https://nymtech.net/docs/nodes/network-requester-setup.html) can be used to privacy-protect your application traffic. Simply use [NymConnect](../quickstart/nymconnect-gui.md) to connect to this service through the mixnet.
You can check the [default allowed list](https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt) file to see which application traffic is whitelisted by default. If the domain is present on that list, it means that existing [network requesters](https://nymtech.net/docs/nodes/network-requester-setup.html) can be used to privacy-protect your application traffic. Simply use [NymConnect](../quickstart/nymconnect-gui.md) to connect to this service through the mixnet.
@@ -22,7 +22,7 @@ Its packaged and available on the npm registry, so you can npm install it int
The webassembly client is most easily used via the [typescript sdk](https://nymtech.net/docs/sdk/typescript.html).
You can find example code in the [examples section](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/typescript/examples) of the codebase, and in the [typescript sdk docs](https://nymtech.net/docs/sdk/typescript.html).
You can find example code in the [examples section](https://github.com/nymtech/nym/tree/master/sdk/typescript/examples) of the codebase, and in the [typescript sdk docs](https://nymtech.net/docs/sdk/typescript.html).
#### SOCKS client
This client is useful for allowing existing applications to use the Nym mixnet without any code changes. All thats necessary is that they can use one of the SOCKS5, SOCKS4a, or SOCKS4 proxy protocols (which many applications can - crypto wallets, browsers, chat applications etc).
@@ -11,7 +11,7 @@ Install NymConnect and select an application that you want to privacy-enhance fr
**Please note that NymConnect is currently released in beta. Please report bugs via Github**.
## Usage instuctions
* [Download](https://github.com/nymtech/nym/releases/tag/nym-connect-{{platform_release_version}}) and install NymConnect.
* [Download](https://github.com/nymtech/nym/releases/) and install NymConnect.
* Select your service provider from the dropdown menu.
* Click `connect` - NymConnect will connect to a service provider and its SOCKS Proxy (IP) and Port will be displayed.
* Click on IP or Port to copy their values to the clipboard.
@@ -1,5 +1,6 @@
# Matrix NymConnect Integration
![](../images//matrix.png)
Chat applications became an essential part of human communication. Matrix chat has end to end encryption on protocol level and Element app users can sort their communication into spaces and rooms. Now the Matrix communities can rely on network privacy as NymConnect supports Matrix chat protocol.
@@ -10,33 +10,35 @@ A team made up of Monero community members have successfully set up a service pr
## How can I use Monero over the Nym mixnet?
> Any syntax in `<>` brackets is a user's unique variable. Exchange with a corresponding name without the `<>` brackets.
The mainnet service provider to Monero over the Nym mixnet is now ready for use via [NymConnect](https://nymtech.net/download-nymconnect/).
* Download and open the latest version of [NymConnect](https://nymtech.net/download-nymconnect/).
* Click on the top left options and go to Settings
* Go to “Select service provider” and turn it on
* For Mainnet, search for this provider or insert it manually:
* **Download** the latest version of [**NymConnect**](https://nymtech.net/download-nymconnect/).
* Make sure your NymConnect is executable.
```sh
i1TiuoNp4jp9weffCW7tPnkb4hRTPydRjX8iXFVaYDG.88Z1hruuvbzWpdCE2xYnTbPNrr49j4s7mmUQC5wvRRLZ@3EPuxwGn2WP2HdxybzoDa5QsohYSP76aQQRUJuPMvk23
# in Linux open terminal in the same folder and run:
chmod +x ./nym-connect_<YOUR_VERSION>.AppImage
```
* **Open NymConnect app**
* **Turn it on** - Monero wallet is listed in the apps supported by default, no need for any setup
* **Copy** the **Socks5 address** and **Port**
* Go to the main NymConnect interface and connect to the mixnet
Then go to your Monero wallet (desktop or CLI) and change the settings to run over socks5 proxy:
Then go to your Monero wallet (gui or otherwise) and change the settings to run over socks5 proxy:
**Monero desktop wallet:**
**Monero desktop:**
* Settings -> Interface -> Socks5 proxy -> Add values: IP address `localhost`, Port `1080`
* Settings -> Interface -> Socks5 proxy -> Add values: IP address `127.0.0.1`, Port `1080` (the values copied from NymConnect)
<!---commenting the video as it has a redundant part about manual NR setup
<iframe width="700" height="400" src="https://www.youtube.com/embed/oSHnk1BG_f0" title="Demo: Connect Your Monero Wallet to the Nym Mixnet via NymConnect" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
**CLI**
--->
**CLI wallet**
* **Monerod:** add `--proxy 127.0.0.1:1080 --bootstrap-daemon-proxy 127.0.0.1:1080` to args
* **Monero-wallet-{rpc, cli}:** add `--proxy 127.0.0.1:1080 --daemon-ssl-allow-any-cert` to args
Follow the instructions and the Monero mainnet will be connected through to the Nym mixnet.
For those who want to try it out in testnet, a stagenet service provider is also available: [https://nymtech.net/.wellknown/connect/service-providers.json](https://nymtech.net/.wellknown/connect/service-providers.json)
Now your Monero traffic is protected by the network privacy of Nym Mixnet.
@@ -2,6 +2,8 @@
*This is a shortened version of a [Nym Community post](https://blog.nymtech.net/how-to-use-telegram-in-iraq-with-nymconnect-106a3b8dd050) written by Saliveja.*
![](../images/telegram.png)
The purpose of the following manual is not to promote Telegram but so people can use it with the Nym mixnet if they wish to, should a situation ask for that. This privacy-enhances Telegram at the network level and allows users to access the application from locations like where the application was banned.
See also: [Element (Matrix) over the Nym mixnet](./matrix.md): private, decentralised and secure messaging.
@@ -10,7 +12,7 @@ See also: [Element (Matrix) over the Nym mixnet](./matrix.md): private, decentra
Heres how to configure Telegram with NymConnect:
1. Download and install NymConnect ().**
1. **Download and install NymConnect(https://nymtech.net/download-nymconnect/).**
For more releases, check out [Github](https://github.com/nymtech/nym/tags). NymConnect is available for Linux, Windows, and MacOS.
On Linux make sure NymConnect is executable. Opening a terminal in the same directory and run:
```sh
+1 -7
View File
@@ -16,7 +16,7 @@ To contribute tranlsations in a new language, please get in touch via [Matrix](h
### Variables
There are some variables that are shared across the entire docs site, such as the current latest software version.
Variables are denoted in the `.md` files wrapped in `{{}}` (e.g `{{platform_release_version}}` is the most recent release), and are located in the `book.toml` file under the `[preprocessor.variables.variables]` heading. If you are changing something like the software release version, minimum code versions in prerequisites, etc, **check in here first!**
Variables are denoted in the `.md` files wrapped in `{{}}` (e.g `{{wallet_release_version}}`), and are located in the `book.toml` file under the `[preprocessor.variables.variables]` heading. If you are changing something like the software release version, minimum code versions in prerequisites, etc, **check in here first!**
### Diagrams
Most diagrams are simply ascii. Copies are kept in `/diagrams/` for ease of reproducability. Created using [textik](https://textik.com/#).
@@ -27,12 +27,6 @@ Example files are inserted as per normal with mdbook.
Some binary command outputs are generated using the [`cmdrun`](https://docs.rs/mdbook-cmdrun/latest/mdbook_cmdrun/) mdbook plugin.
### Updating platform version
When updating the version, make sure to change **both** the version in the `title` on line 2 of `book.toml`, as well as the `platform_release_version` variable in the same file.
> In the future this will be dealt with something like a preprocessor widget (todo).
## Building
When working locally, it is recommended that you use `mdbook serve` to have a local version of the docs served on `localhost:3000`, with hot reloading on any changes made to files in the `src/` directory.
+2 -3
View File
@@ -38,7 +38,7 @@ chapter-line-height = "2em"
section-line-height = "1.5em"
# if true, never read and touch the files in theme dir: this is used to stop the looping reload issue referred to in the readme
turn-off = false
turn-off = true
[preprocessor.admonish]
command = "mdbook-admonish"
@@ -48,7 +48,6 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
# https://gitlab.com/tglman/mdbook-variables/
[preprocessor.variables.variables]
minimum_rust_version = "1.66"
platform_release_version = "1.1.29"
wallet_release_version = "1.2.8"
[preprocessor.last-changed]
@@ -91,7 +90,7 @@ git-repository-icon = "fa-github"
# edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path}"
# site-url = "/docs/"
# cname = "nymtech.net"
input-404 = "not-found.md"
input-404 = "not-found.md"
[output.html.fold]
enable = true # whether or not to enable section folding
+1 -1
View File
@@ -9,8 +9,8 @@
# Binaries
- [Pre-built Binaries](binaries/pre-built-binaries.md)
- [Binary Initialisation and Configuration](binaries/init-and-config.md)
- [Building from Source](binaries/building-nym.md)
- [Binary Initialisation and Configuration](binaries/init-and-config.md)
<!-- - [Version Compatibility Table](binaries/version-compatiblity.md) -->
# Nodes
@@ -39,7 +39,7 @@ If you really don't want to use the shell script installer, the [Rust installati
## Download and build Nym binaries
The following commands will compile binaries into the `nym/target/release` directory:
```
```sh
rustup update
git clone https://github.com/nymtech/nym.git
cd nym
@@ -47,10 +47,9 @@ cd nym
git reset --hard # in case you made any changes on your branch
git pull # in case you've checked it out before
git checkout release/{{platform_release_version}} # checkout to the latest release branch: `develop` will most likely be incompatible with deployed public networks
git checkout master # master branch has the latest release version: `develop` will most likely be incompatible with deployed public networks
cargo build --release # build your binaries with **mainnet** configuration
NETWORK=sandbox cargo build --release # build your binaries with **sandbox** configuration
```
Quite a bit of stuff gets built. The key working parts are:
@@ -1,6 +1,13 @@
# Binary Initialisation and Configuration
All Nym binaries must first be initialised with `init` before being `run`.
All Nym binaries must first be made executable and initialised with `init` before being `run`.
To make a binary executable, open terminal in the same directory and run:
```sh
chmod +x <BINARY_NAME>
# for example: chmod +x nym-mixnode
```
The `init` command is usually where you pass flags specifying configuration arguments such as the gateway you wish to communicate with, the ports you wish your binary to listen on, etc.
@@ -160,7 +160,7 @@ Create a service file for the socks5 client at `/etc/systemd/system/nym-socks5-c
```ini
[Unit]
Description=Nym Socks5 Client ({{platform_release_version}})
Description=Nym Socks5 Client
StartLimitInterval=350
StartLimitBurst=10
@@ -109,7 +109,7 @@ Alternatively, a custom host can be set in the `config.toml` file under the `soc
### Connecting to the local websocket
The Nym native client exposes a websocket interface that your code connects to. To program your app, choose a websocket library for whatever language you're using. The **default** websocket port is `1977`, you can override that in the client config if you want.
The Nym monorepo includes websocket client example code for Rust, Go, Javacript, and Python, all of which can be found [here](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/clients/native/examples).
The Nym monorepo includes websocket client example code for Rust, Go, Javacript, and Python, all of which can be found [here](https://github.com/nymtech/nym/tree/master/clients/native/examples).
> Rust users can run the examples with `cargo run --example <rust_file>.rs`, as the examples are not organised in the same way as the other examples, due to already being inside a Cargo project.
@@ -183,7 +183,7 @@ Nym [`ClientRequest`](https://github.com/nymtech/nym/blob/develop/clients/native
As a response the `native-client` will send a `ServerResponse` to be decoded.
You can find examples of sending and receiving binary data in the Rust, Python and Go [code examples](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/clients/native/examples), and an example project from the Nym community [BTC-BC](https://github.com/sgeisler/btcbc-rs/): Bitcoin transaction transmission via Nym, a client and service provider written in Rust.
You can find examples of sending and receiving binary data in the Rust, Python and Go [code examples](https://github.com/nymtech/nym/tree/master/clients/native/examples), and an example project from the Nym community [BTC-BC](https://github.com/sgeisler/btcbc-rs/): Bitcoin transaction transmission via Nym, a client and service provider written in Rust.
#### Getting your own address
Sometimes, when you start your app, it can be convenient to ask the native client to tell you what your own address is (from the saved configuration files). To do this, send:
@@ -1,6 +1,6 @@
# Mixnet Contract
The Mixnet smart contract is a core piece of the Nym system, functioning as the mixnet directory and keeping track of delegations and rewards: the core functionality required by an incentivised mixnet. You can find the code and build instructions [here](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/contracts/mixnet).
The Mixnet smart contract is a core piece of the Nym system, functioning as the mixnet directory and keeping track of delegations and rewards: the core functionality required by an incentivised mixnet. You can find the code and build instructions [here](https://github.com/nymtech/nym/tree/master/contracts/mixnet).
### Functionality
The Mixnet contract has multiple functions:
@@ -9,5 +9,5 @@ The Mixnet contract has multiple functions:
* storing delegation and bond amounts.
* storing reward amounts.
The addresses of deployed smart contracts can be found in the [`network-defaults`](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/common/network-defaults/src/mainnet.rs) directory of the codebase alongside other network default values.
The addresses of deployed smart contracts can be found in the [`network-defaults`](https://github.com/nymtech/nym/blob/master/common/network-defaults/src/mainnet.rs) directory of the codebase alongside other network default values.
@@ -1,11 +1,11 @@
# Vesting Contract
The vesting contract allows for the creation of vesting accounts, allowing `NYM` tokens to vest over time, and for users to minimally interact with the Mixnet using their unvested tokens. You can find the code and build instructions [here](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/contracts/vesting).
The vesting contract allows for the creation of vesting accounts, allowing `NYM` tokens to vest over time, and for users to minimally interact with the Mixnet using their unvested tokens. You can find the code and build instructions [here](https://github.com/nymtech/nym/tree/master/contracts/vesting).
### Functionality
The Vesting contract has multiple functions:
* Creating and storing vesting `NYM` token vesting accounts.
* Interacting with the Mixnet using vesting (i.e. non-transferable) tokens, allowing users to delegate their unvested tokens.
The addresses of deployed smart contracts can be found in the [`network-defaults`](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/common/network-defaults/src/mainnet.rs) directory of the codebase alongside other network default values.
The addresses of deployed smart contracts can be found in the [`network-defaults`](https://github.com/nymtech/nym/blob/master/common/network-defaults/src/mainnet.rs) directory of the codebase alongside other network default values.
+2 -2
View File
@@ -27,7 +27,7 @@ The `mixnet` component currently exposes the logic of two clients: the [websocke
The `coconut` component is currently being worked on. Right now it exposes logic allowing for the creation of coconut credentials on the Sandbox testnet.
## Websocket client examples
> All the codeblocks below can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there. If you wish to run these outside of the workspace - such as if you want to use one as the basis for your own project - then make sure to import the `sdk`, `tokio`, and `nym_bin_common` crates.
> All the codeblocks below can be found in the `nym-sdk` [examples directory](https://github.com/nymtech/nym/tree/master/sdk/rust/nym-sdk/examples) in the monorepo. Just navigate to `nym/sdk/rust/nym-sdk/examples/` and run the files from there. If you wish to run these outside of the workspace - such as if you want to use one as the basis for your own project - then make sure to import the `sdk`, `tokio`, and `nym_bin_common` crates.
### Different message types
There are two methods for sending messages through the mixnet using your client:
@@ -82,7 +82,7 @@ If you're integrating mixnet functionality into an existing app and want to inte
### Anonymous replies with SURBs
Both functions used to send messages through the mixnet (`send_message` and `send_plain_message`) send a pre-determined number of SURBs along with their messages by default.
The number of SURBs is set [here](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/sdk/rust/nym-sdk/src/mixnet/client.rs#L34):
The number of SURBs is set [here](https://github.com/nymtech/nym/blob/master/sdk/rust/nym-sdk/src/mixnet/client.rs#L33):
```rust,noplayground
{{#include ../../../../sdk/rust/nym-sdk/src/mixnet/client.rs:34}}
+5 -5
View File
@@ -1,7 +1,7 @@
# Typescript SDK
The Typescript SDK allows developers to start building browser-based mixnet applications quickly, by simply importing the SDK into their code via NPM as they would any other Typescript library.
You can find the source code [here](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk) and the library on NPM [here](https://www.npmjs.com/package/@nymproject/sdk).
You can find the source code [here](https://github.com/nymtech/nym/tree/master/sdk) and the library on NPM [here](https://www.npmjs.com/package/@nymproject/sdk).
Currently developers can use the SDK to do the following **entirely in the browser**:
* Create a client
@@ -19,7 +19,7 @@ In the future the SDK will be made up of several components, each of which will
| Validator | Sign & broadcast Nyx blockchain transactions, query the blockchain | ❌ |
### How it works
The SDK can be thought of as a 'wrapper' around the compiled [WebAssembly client](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/clients/webassembly) code: it runs the client (a Wasm blob) in a web worker. This allows us to keep the work done by the client - such as the heavy lifting of creating and multiply-encrypting Sphinx packets - in a seperate thread from our UI, enabling you to build reactive frontends without worrying about the work done under the hood by the client eating your processing power.
The SDK can be thought of as a 'wrapper' around the compiled [WebAssembly client](https://github.com/nymtech/nym/tree/master/clients/webassembly) code: it runs the client (a Wasm blob) in a web worker. This allows us to keep the work done by the client - such as the heavy lifting of creating and multiply-encrypting Sphinx packets - in a seperate thread from our UI, enabling you to build reactive frontends without worrying about the work done under the hood by the client eating your processing power.
The SDK exposes an interface that allows developers to interact with the Wasm blob inside the webworker from frontend code.
@@ -41,7 +41,7 @@ Support for environments with different bundlers will be added in subsequent rel
### Using the SDK
There are multiple example projects in [`nym/sdk/typescript/examples/`](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/typescript/examples/), each for a different frontend framework.
There are multiple example projects in [`nym/sdk/typescript/examples/`](https://github.com/nymtech/nym/tree/master/sdk/typescript/examples/), each for a different frontend framework.
#### Vanilla HTML
The best place to start if you just want to quickly get a basic frontend up and running with which to experiment is `examples/plain-html`:
@@ -53,10 +53,10 @@ The best place to start if you just want to quickly get a basic frontend up and
As you can see, all that is required to create an ephemeral keypair and connect to the mixnet is creating a client and then subscribing to the mixnet events coming down the websocket, and adding logic to deal with them.
#### Parcel
If you don't want to use `Webpack` as your app bundler, we have an example with `Parcel` located at [`examples/parcel/`](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/typescript/examples/react-webpack-with-theme-example/parcel/).
If you don't want to use `Webpack` as your app bundler, we have an example with `Parcel` located at [`examples/parcel/`](https://github.com/nymtech/nym/tree/master/sdk/typescript/examples/chat-app/parcel).
#### Create React App
For React developers we have an example which is a basic React app scaffold with the additional logic for creating a client and subscribing to mixnet events in [`examples/react-webpack-with-theme-example/`](https://github.com/nymtech/nym/tree/release/{{platform_release_version}}/sdk/typescript/examples/react-webpack-with-theme-example/).
For React developers we have an example which is a basic React app scaffold with the additional logic for creating a client and subscribing to mixnet events in [`examples/react-webpack-with-theme-example/`](https://github.com/nymtech/nym/tree/master/sdk/typescript/examples/chat-app/react-webpack-with-theme-example).
### Developers: think about what you're sending (and importing)!
Think about what information your app sends. That goes for whatever you put into your Sphinx packet messages as well as what your app's environment may leak.
+1 -2
View File
@@ -38,7 +38,7 @@ chapter-line-height = "2em"
section-line-height = "1.5em"
# if true, never read and touch the files in theme dir: this is used to stop the looping reload issue referred to in the readme
turn-off = false
turn-off = true
[preprocessor.admonish]
command = "mdbook-admonish"
@@ -48,7 +48,6 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
# https://gitlab.com/tglman/mdbook-variables/
[preprocessor.variables.variables]
minimum_rust_version = "1.66"
platform_release_version = "1.1.29"
wallet_release_version = "1.2.8"
[preprocessor.last-changed]
+1 -1
View File
@@ -4,8 +4,8 @@
# Binaries
- [Pre-built Binaries](./binaries/pre-built-binaries.md)
- [Binary Initialisation and Configuration](./binaries/init-and-config.md)
- [Building from Source](./binaries/building-nym.md)
- [Binary Initialisation and Configuration](./binaries/init-and-config.md)
<!-- - [Version Compatibility Table](binaries/version-compatiblity.md) -->
# Operators Guides
@@ -39,7 +39,7 @@ If you really don't want to use the shell script installer, the [Rust installati
## Download and build Nym binaries
The following commands will compile binaries into the `nym/target/release` directory:
```
```sh
rustup update
git clone https://github.com/nymtech/nym.git
cd nym
@@ -47,10 +47,9 @@ cd nym
git reset --hard # in case you made any changes on your branch
git pull # in case you've checked it out before
git checkout release/{{platform_release_version}} # checkout to the latest release branch: `develop` will most likely be incompatible with deployed public networks
git checkout master # master branch has the latest release version: `develop` will most likely be incompatible with deployed public networks
cargo build --release # build your binaries with **mainnet** configuration
NETWORK=sandbox cargo build --release # build your binaries with **sandbox** configuration
```
Quite a bit of stuff gets built. The key working parts are:
@@ -1,6 +1,13 @@
# Binary Initialisation and Configuration
All Nym binaries must first be initialised with `init` before being `run`.
All Nym binaries must first be made executable and initialised with `init` before being `run`.
To make a binary executable, open terminal in the same directory and run:
```sh
chmod +x <BINARY_NAME>
# for example: chmod +x nym-mixnode
```
The `init` command is usually where you pass flags specifying configuration arguments such as the gateway you wish to communicate with, the ports you wish your binary to listen on, etc.
@@ -97,7 +97,7 @@ It will look something like this:
|_| |_|\__, |_| |_| |_|
|___/
(nym-gateway - version {{platform_release_version}})
(nym-gateway - version v1.1.29)
>>> attempting to sign 2Mf8xYytgEeyJke9LA7TjhHoGQWNBEfgHZtTyy2krFJfGHSiqy7FLgTnauSkQepCZTqKN5Yfi34JQCuog9k6FGA2EjsdpNGAWHZiuUGDipyJ6UksNKRxnFKhYW7ri4MRduyZwbR98y5fQMLAwHne1Tjm9cXYCn8McfigNt77WAYwBk5bRRKmC34BJMmWcAxphcLES2v9RdSR68tkHSpy2C8STfdmAQs3tZg8bJS5Qa8pQdqx14TnfQAPLk3QYCynfUJvgcQTrg29aqCasceGRpKdQ3Tbn81MLXAGAs7JLBbiMEAhCezAr2kEN8kET1q54zXtKz6znTPgeTZoSbP8rzf4k2JKHZYWrHYF9JriXepuZTnyxAKAxvGFPBk8Z6KAQi33NRQkwd7MPyttatHna6kG9x7knffV6ebGzgRBf7NV27LurH8x4L1uUXwm1v1UYCA1WSBQ9Pp2JW69k5v5v7G9gBy8RUcZnMbeL26Qqb8WkuGcmuHhaFfoqSfV7PRHPpPT4M8uRqUyR4bjUtSJJM1yh6QSeZk9BEazzoJqPeYeGoiFDZ3LMj2jesbJweQR4caaYuRczK92UGSSqu9zBKmE45a
@@ -19,7 +19,7 @@ For example `./target/debug/nym-network-requester --no-banner build-info --outpu
> The process is the similar for mix node, gateway and network requester. In the following steps we use a placeholder `<NODE>` in the commands, please change it for the type of node you want to upgrade. Any particularities for the given type of node are included.
Upgrading your node is a two-step process:
* Updating the binary and `~/.nym/<NODE>/<YOUR_ID>/config.toml` on your VPS
* Updating the binary and `~/.nym/<NODE>/<YOUR_ID>/config/config.toml` on your VPS
* Updating the node information in the [mixnet smart contract](https://nymtech.net/docs/nyx/mixnet-contract.html). **This is the information that is present on the [mixnet explorer](https://explorer.nymtech.net)**.
### Step 1: Upgrading your binary
@@ -230,7 +230,7 @@ Here's a systemd service file to do that:
```ini
[Unit]
Description=Nym Mixnode ({{platform_release_version}})
Description=Nym Mixnode <VERSION>
StartLimitInterval=350
StartLimitBurst=10
@@ -252,7 +252,7 @@ WantedBy=multi-user.target
```ini
[Unit]
Description=Nym Gateway ({{platform_release_version}})
Description=Nym Gateway <VERSION>
StartLimitInterval=350
StartLimitBurst=10
@@ -274,7 +274,7 @@ WantedBy=multi-user.target
```ini
[Unit]
Description=Nym Network Requester ({{platform_release_version}})
Description=Nym Network Requester <VERSION>
StartLimitInterval=350
StartLimitBurst=10
@@ -87,13 +87,13 @@ Mixnode configuration completed.
|_| |_|\__, |_| |_| |_|
|___/
(nym-mixnode - version {{platform_release_version}})
(nym-mixnode - version v1.1.29)
Identity Key: DhmUYedPZvhP9MMwXdNpPaqCxxTQgjAg78s2nqtTTiNF","version":"{{platform_release_version}}"},"cost_params
Identity Key: DhmUYedPZvhP9MMwXdNpPaqCxxTQgjAg78s2nqtTTiNF","version":"v1.1.29"},"cost_params
Sphinx Key: CfZSy1jRfrfiVi9JYexjFWPqWkKoY72t7NdpWaq37K8Z
Host: 62.240.134.189 (bind address: 62.240.134.189)
Version: {{platform_release_version}}
Version: v1.1.29
Mix Port: 1789, Verloc port: 1790, Http Port: 8000
```
~~~
@@ -134,7 +134,7 @@ It will look something like this:
|_| |_|\__, |_| |_| |_|
|___/
(nym-mixnode - version {{platform_release_version}})
(nym-mixnode - version v1.1.29)
>>> attempting to sign 22Z9wt4PyiBCbMiErxj5bBa4VCCFsjNawZ1KnLyMeV9pMUQGyksRVANbXHjWndMUaXNRnAuEVJW6UCxpRJwZe788hDt4sicsrv7iAXRajEq19cWPVybbUqgeo76wbXbCbRdg1FvVKgYZGZZp8D72p5zWhKSBRD44qgCrqzfV1SkiFEhsvcLUvZATdLRocAUL75KmWivyRiQjCE1XYEWyRH9yvRYn4TymWwrKVDtEB63zhHjATN4QEi2E5qSrSbBcmmqatXsKakbgSbQoLsYygcHx7tkwbQ2HDYzeiKP1t16Rhcjn6Ftc2FuXUNnTcibk2LQ1hiqu3FAq31bHUbzn2wiaPfm4RgqTwGM4eqnjBofwR3251wQSxbYwKUYwGsrkweRcoPuEaovApR9R19oJ7GVG5BrKmFwZWX3XFVuECe8vt1x9MY7DbQ3xhAapsHhThUmzN6JPPU4qbQ3PdMt3YVWy6oRhap97ma2dPMBaidebfgLJizpRU3Yu7mtb6E8vgi5Xnehrgtd35gitoJqJUY5sB1p6TDPd6vk3MVU1zqusrke7Lvrud4xKfCLqp672Bj9eGb2wPwow643CpHuMkhigfSWsv9jDq13d75EGTEiprC2UmWTzCJWHrDH7ka68DZJ5XXAW67DBewu7KUm1jrJkNs55vS83SWwm5RjzQLVhscdtCH1Bamec6uZoFBNVzjs21o7ax2WHDghJpGMxFi6dmdMCZpqn618t4
@@ -190,7 +190,7 @@ Stop the running process with `CTRL-C`, and create a service file for the reques
```ini
[Unit]
Description=Nym Network Requester ({{platform_release_version}})
Description=Nym Network Requester
StartLimitInterval=350
StartLimitBurst=10
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# this is a script called by the github CI and CD workflows to post process CSS/image/href links for serving
# several mdbooks from a subdirectory
cd scripts/post-process
npm install
node index.mjs
@@ -0,0 +1,88 @@
import { unified } from "unified";
import parse from "rehype-parse";
import inspectUrls from "@jsdevtools/rehype-url-inspector";
import stringify from "rehype-stringify";
import { read, write } from "to-vfile";
import path from "path";
import fs from "fs";
import { glob } from "glob";
async function main() {
const distDir = "../../../dist/docs";
const items = [];
const books = [
'developers',
'docs',
'operators',
];
for(const book of books)
{
// only process the root `index.html` files, because they have absolute paths instead of relative paths
const filenames = [ path.resolve(distDir, book, 'index.html') ];
// leaving this here for a future where other files need to be processed
// const filenames = await glob(path.resolve(distDir, book) + '/**/*.html');
for (const f of filenames) {
// Create a Rehype processor with the inspectUrls plugin
const processor = unified()
.use(parse)
.use(inspectUrls, {
inspectEach(args) {
const { url: rawUrl, propertyName } = args;
const { tagName } = args.node;
const filename = args.file.history[0];
const relativeFilename = path.relative(distDir, filename);
const relativeDirectory = path.dirname(relativeFilename);
// remove relative paths from URL
const bareUrl = rawUrl.split('/').filter(c => c !== '.' && c !== '..').join('/');
let url;
if(rawUrl.includes('.html#')) {
url = path.join(`/${relativeDirectory}`, bareUrl);
} else if(rawUrl.startsWith('#')) {
url = path.join(`/`, relativeFilename + bareUrl);
} else {
url = path.join(`/${book}`, bareUrl);
}
// const item = { filename, relativeDirectory, tagName, propertyName, rawUrl, url };
const item = { tagName, rawUrl, url };
// if(tagName === 'a') {
// console.log(args);
// }
if(!rawUrl.startsWith('http')) {
if (tagName === 'link' || tagName === 'script' || tagName === 'a') {
args.node.properties[propertyName] = url;
items.push(item);
}
}
}
})
.use(stringify);
// Read the example HTML file
const filename = path.resolve(distDir, f);
console.log(`${filename}...`);
let file = await read(filename);
// Crawl the HTML file and find all the URLs
const res = await processor.process(file);
fs.writeFileSync(filename, res.value);
}
}
// console.table(items);
// console.log();
}
main();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
{
"name": "nym-docs-post-process",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"process": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@jsdevtools/rehype-url-inspector": "^2.0.2",
"glob": "^10.3.4",
"rehype-parse": "^9.0.0",
"rehype-stringify": "^10.0.0",
"to-vfile": "^8.0.0",
"unified": "^11.0.2"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.27"
version = "1.1.28"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1
View File
@@ -1,3 +1,4 @@
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;
use reqwest::StatusCode;
+2 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.1.27"
version = "1.1.28"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
@@ -37,7 +37,7 @@ serde_json = { workspace = true }
sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "sqlite", "macros", "migrate", ] }
subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
thiserror = "1"
tokio = { version = "1.24.1", features = [ "rt-multi-thread", "net", "signal", "fs", ] }
tokio = { workspace = true, features = [ "rt-multi-thread", "net", "signal", "fs", "time" ] }
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tungstenite = "0.14"
tokio-util = { version = "0.7.4", features = ["codec"] }
@@ -1,13 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node::client_handling::websocket::message_receiver::MixMessageSender;
use dashmap::DashMap;
use nym_sphinx::DestinationAddressBytes;
use std::sync::Arc;
use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender};
#[derive(Clone)]
pub(crate) struct ActiveClientsStore(Arc<DashMap<DestinationAddressBytes, MixMessageSender>>);
pub(crate) struct ActiveClientsStore(Arc<DashMap<DestinationAddressBytes, ClientIncomingChannels>>);
#[derive(Clone)]
pub(crate) struct ClientIncomingChannels {
// Mix messages coming from the mixnet to the handler of a client.
pub mix_message_sender: MixMessageSender,
// Requests sent from the handler of one client to the handler of other clients.
pub is_active_request_sender: IsActiveRequestSender,
}
impl ActiveClientsStore {
/// Creates new instance of `ActiveClientsStore` to store in-memory handles to all currently connected clients.
@@ -21,13 +31,13 @@ impl ActiveClientsStore {
/// # Arguments
///
/// * `client`: address of the client for which to obtain the handle.
pub(crate) fn get(&self, client: DestinationAddressBytes) -> Option<MixMessageSender> {
pub(crate) fn get(&self, client: DestinationAddressBytes) -> Option<ClientIncomingChannels> {
let entry = self.0.get(&client)?;
let handle = entry.value();
// if the entry is stale, remove it from the map
// if handle.is_valid() {
if !handle.is_closed() {
if !handle.mix_message_sender.is_closed() {
Some(handle.clone())
} else {
// drop the reference to the map to prevent deadlocks
@@ -52,8 +62,19 @@ impl ActiveClientsStore {
///
/// * `client`: address of the client for which to insert the handle.
/// * `handle`: the sender channel for all mix packets to be pushed back onto the websocket
pub(crate) fn insert(&self, client: DestinationAddressBytes, handle: MixMessageSender) {
self.0.insert(client, handle);
pub(crate) fn insert(
&self,
client: DestinationAddressBytes,
handle: MixMessageSender,
is_active_request_sender: IsActiveRequestSender,
) {
self.0.insert(
client,
ClientIncomingChannels {
mix_message_sender: handle,
is_active_request_sender,
},
);
}
/// Get number of active clients in store
@@ -1,28 +1,39 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node::client_handling::websocket::connection_handler::{ClientDetails, FreshHandler};
use crate::node::client_handling::websocket::message_receiver::MixMessageReceiver;
use crate::node::storage::error::StorageError;
use crate::node::storage::Storage;
use futures::StreamExt;
use futures::{
future::{FusedFuture, OptionFuture},
FutureExt, StreamExt,
};
use log::*;
use nym_gateway_requests::iv::IVConversionError;
use nym_gateway_requests::types::{BinaryRequest, ServerResponse};
use nym_gateway_requests::{ClientControlRequest, GatewayRequestsError};
use nym_gateway_requests::{
iv::{IVConversionError, IV},
types::{BinaryRequest, ServerResponse},
ClientControlRequest, GatewayRequestsError,
};
use nym_sphinx::forwarding::packet::MixPacket;
use rand::{CryptoRng, Rng};
use std::convert::TryFrom;
use std::process;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::protocol::Message;
use crate::node::client_handling::bandwidth::Bandwidth;
use crate::node::client_handling::FREE_TESTNET_BANDWIDTH_VALUE;
use nym_gateway_requests::iv::IV;
use nym_task::TaskClient;
use nym_validator_client::coconut::CoconutApiError;
use rand::{CryptoRng, Rng};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
use std::{convert::TryFrom, process, time::Duration};
use crate::node::{
client_handling::{
bandwidth::Bandwidth,
websocket::{
connection_handler::{ClientDetails, FreshHandler},
message_receiver::{
IsActive, IsActiveRequestReceiver, IsActiveResultSender, MixMessageReceiver,
},
},
FREE_TESTNET_BANDWIDTH_VALUE,
},
storage::{error::StorageError, Storage},
};
#[derive(Debug, Error)]
pub(crate) enum RequestHandlingError {
@@ -97,6 +108,11 @@ pub(crate) struct AuthenticatedHandler<R, S, St> {
inner: FreshHandler<R, S, St>,
client: ClientDetails,
mix_receiver: MixMessageReceiver,
// Occasionally the handler is requested to ping the connected client for confirm that it's
// active, such as when a duplicate connection is detected. This hashmap stores the oneshot
// senders that are used to return the result of the ping to the handler requesting the ping.
is_active_request_receiver: IsActiveRequestReceiver,
is_active_ping_pending_reply: Option<(u64, IsActiveResultSender)>,
}
// explicitly remove handle from the global store upon being dropped
@@ -127,11 +143,14 @@ where
fresh: FreshHandler<R, S, St>,
client: ClientDetails,
mix_receiver: MixMessageReceiver,
is_active_request_receiver: IsActiveRequestReceiver,
) -> Self {
AuthenticatedHandler {
inner: fresh,
client,
mix_receiver,
is_active_request_receiver,
is_active_ping_pending_reply: None,
}
}
@@ -351,6 +370,29 @@ where
}
}
/// Handles pong message received from the client.
/// If the client is still active, the handler that requested the ping will receive a reply.
async fn handle_pong(&mut self, msg: Vec<u8>) {
if let Ok(msg) = msg.try_into() {
let msg = u64::from_be_bytes(msg);
trace!("Received pong from client: {}", msg);
if let Some((tag, _)) = &self.is_active_ping_pending_reply {
if tag == &msg {
debug!("Reporting back to the handler that the client is still active");
let tx = self.is_active_ping_pending_reply.take().unwrap().1;
if let Err(err) = tx.send(IsActive::Active) {
warn!("Failed to send pong reply back to the requesting handler: {err:?}");
}
} else {
warn!(
"Received pong reply from the client with unexpected tag: {}",
msg
);
}
}
}
}
/// Attempts to handle websocket message received from the connected client.
///
/// # Arguments
@@ -363,10 +405,64 @@ where
match raw_request {
Message::Binary(bin_msg) => Some(self.handle_binary(bin_msg).await),
Message::Text(text_msg) => Some(self.handle_text(text_msg).await),
Message::Pong(msg) => {
self.handle_pong(msg).await;
None
}
_ => None,
}
}
/// Send a ping to the connected client and return a tag identifying the ping.
async fn send_ping(&mut self) -> Result<u64, WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
let tag: u64 = rand::thread_rng().gen();
debug!("Got request to ping our connection: {}", tag);
self.inner
.send_websocket_message(Message::Ping(tag.to_be_bytes().to_vec()))
.await?;
Ok(tag)
}
/// Handles the ping timeout by responding back to the handler that requested the ping.
async fn handle_ping_timeout(&mut self) {
debug!("Ping timeout expired!");
if let Some((_tag, reply_tx)) = self.is_active_ping_pending_reply.take() {
if let Err(err) = reply_tx.send(IsActive::NotActive) {
warn!("Failed to respond back to the handler requesting the ping: {err:?}");
}
}
}
async fn handle_is_active_request(
&mut self,
reply_tx: IsActiveResultSender,
) -> Result<(), WsError>
where
S: AsyncRead + AsyncWrite + Unpin,
{
if self.is_active_ping_pending_reply.is_some() {
warn!("Received request to ping the client, but a ping is already in progress!");
if let Err(err) = reply_tx.send(IsActive::BusyPinging) {
warn!("Failed to respond back to the handler requesting the ping: {err:?}");
}
return Ok(());
}
match self.send_ping().await {
Ok(tag) => {
self.is_active_ping_pending_reply = Some((tag, reply_tx));
Ok(())
}
Err(err) => {
warn!("Failed to send ping to client: {err}. Assuming the connection is dead.");
Err(err)
}
}
}
/// Simultaneously listens for incoming client requests, which realistically should only be
/// binary requests to forward sphinx packets or increase bandwidth
/// and for sphinx packets received from the mix network that should be sent back to the client.
@@ -377,11 +473,32 @@ where
{
trace!("Started listening for ALL incoming requests...");
// Ping timeout future used to check if the client responded to our ping request
let mut ping_timeout: OptionFuture<_> = None.into();
while !shutdown.is_shutdown() {
tokio::select! {
_ = shutdown.recv() => {
log::trace!("client_handling::AuthenticatedHandler: received shutdown");
}
},
// Received a request to ping the client to check if it's still active
tx = self.is_active_request_receiver.next() => {
match tx {
None => break,
Some(reply_tx) => {
if self.handle_is_active_request(reply_tx).await.is_err() {
break;
}
// NOTE: fuse here due to .is_terminated() check below
ping_timeout = Some(Box::pin(tokio::time::sleep(Duration::from_millis(1000)).fuse())).into();
}
};
},
// The ping timeout expired, meaning the client didn't respond to our ping request
_ = &mut ping_timeout, if !ping_timeout.is_terminated() => {
ping_timeout = None.into();
self.handle_ping_timeout().await;
},
socket_msg = self.inner.read_websocket_message() => {
let socket_msg = match socket_msg {
None => break,
@@ -406,7 +523,13 @@ where
}
},
mix_messages = self.mix_receiver.next() => {
let mix_messages = mix_messages.expect("sender was unexpectedly closed! this shouldn't have ever happened!");
let mix_messages = match mix_messages {
None => {
warn!("mix receiver was closed! Assuming the connection is dead.");
break;
}
Some(mix_messages) => mix_messages,
};
if let Err(err) = self.inner.push_packets_to_client(&self.client.shared_keys, mix_messages).await {
warn!("failed to send the unwrapped sphinx packets back to the client - {err}, assuming the connection is dead");
break;
@@ -1,33 +1,43 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::node::client_handling::active_clients::ActiveClientsStore;
use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier;
use crate::node::client_handling::websocket::connection_handler::{
AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream,
use futures::{
channel::{mpsc, oneshot},
SinkExt, StreamExt,
};
use crate::node::storage::error::StorageError;
use crate::node::storage::Storage;
use futures::{channel::mpsc, SinkExt, StreamExt};
use log::*;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::authentication::encrypted_address::{
EncryptedAddressBytes, EncryptedAddressConversionError,
};
use nym_gateway_requests::iv::{IVConversionError, IV};
use nym_gateway_requests::registration::handshake::error::HandshakeError;
use nym_gateway_requests::registration::handshake::{gateway_handshake, SharedKeys};
use nym_gateway_requests::types::{ClientControlRequest, ServerResponse};
use nym_gateway_requests::{BinaryResponse, PROTOCOL_VERSION};
use nym_gateway_requests::{
iv::{IVConversionError, IV},
registration::handshake::{error::HandshakeError, gateway_handshake, SharedKeys},
types::{ClientControlRequest, ServerResponse},
BinaryResponse, PROTOCOL_VERSION,
};
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_sphinx::DestinationAddressBytes;
use rand::{CryptoRng, Rng};
use std::convert::TryFrom;
use std::sync::Arc;
use std::{convert::TryFrom, sync::Arc, time::Duration};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
use crate::node::{
client_handling::{
active_clients::ActiveClientsStore,
websocket::{
connection_handler::{
coconut::CoconutVerifier, AuthenticatedHandler, ClientDetails, InitialAuthResult,
SocketStream,
},
message_receiver::{IsActive, IsActiveRequestSender},
},
},
storage::{error::StorageError, Storage},
};
#[derive(Debug, Error)]
pub(crate) enum InitialAuthenticationError {
#[error("Internal gateway storage error")]
@@ -394,6 +404,59 @@ where
}
}
async fn handle_duplicate_client(
&mut self,
address: DestinationAddressBytes,
mut is_active_request_tx: IsActiveRequestSender,
) -> Result<(), InitialAuthenticationError> {
// Ask the other connection to ping if they are still active.
// Use a oneshot channel to return the result to us
let (ping_result_sender, ping_result_receiver) = oneshot::channel();
log::debug!("Asking other connection handler to ping the connected client to see if it is still active");
if let Err(err) = is_active_request_tx.send(ping_result_sender).await {
warn!("Failed to send ping request to other handler: {err}");
}
// Wait for the reply
match tokio::time::timeout(Duration::from_millis(2000), ping_result_receiver).await {
Ok(Ok(res)) => {
match res {
IsActive::NotActive => {
// The other handler reported that the client is not active, so we can
// disconnect the other client and continue with this connection.
log::debug!("Other handler reports it is not active");
self.active_clients_store.disconnect(address);
}
IsActive::Active => {
// The other handled reported a positive reply, so we have to assume it's
// still active and disconnect this connection.
log::info!("Other handler reports it is active");
return Err(InitialAuthenticationError::DuplicateConnection);
}
IsActive::BusyPinging => {
// The other handler is already busy pinging the client, so we have to
// assume it's still active and disconnect this connection.
log::debug!("Other handler reports it is already busy pinging the client");
return Err(InitialAuthenticationError::DuplicateConnection);
}
}
}
Ok(Err(_)) => {
// Other channel failed to reply (the channel sender probably dropped)
log::info!("Other connection failed to reply, disconnecting it in favour of this new connection");
self.active_clients_store.disconnect(address);
}
Err(_) => {
// Timeout waiting for reply
log::warn!(
"Other connection timed out, disconnecting it in favour of this new connection"
);
self.active_clients_store.disconnect(address);
}
}
Ok(())
}
/// Tries to handle the received authentication request by checking correctness of the received data.
///
/// # Arguments
@@ -418,8 +481,11 @@ where
let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?;
let iv = IV::try_from_base58_string(iv)?;
if self.active_clients_store.get(address).is_some() {
return Err(InitialAuthenticationError::DuplicateConnection);
// Check for duplicate clients
if let Some(client_tx) = self.active_clients_store.get(address) {
log::warn!("Detected duplicate connection for client: {}", address);
self.handle_duplicate_client(address, client_tx.is_active_request_sender)
.await?;
}
let shared_keys = self
@@ -606,12 +672,19 @@ where
}
return if let Some(client_details) = auth_result.client_details {
self.active_clients_store
.insert(client_details.address, mix_sender);
// Channel for handlers to ask other handlers if they are still active.
let (is_active_request_sender, is_active_request_receiver) =
mpsc::unbounded();
self.active_clients_store.insert(
client_details.address,
mix_sender,
is_active_request_sender,
);
Some(AuthenticatedHandler::upgrade(
self,
client_details,
mix_receiver,
is_active_request_receiver,
))
} else {
None
@@ -100,7 +100,7 @@ pub(crate) async fn handle_connection<R, S, St>(
.await
{
None => {
trace!("received shutdown signal while performing initial authetnication");
trace!("received shutdown signal while performing initial authentication");
return;
}
Some(None) => {
@@ -1,7 +1,20 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc;
use futures::channel::{mpsc, oneshot};
pub(crate) type MixMessageSender = mpsc::UnboundedSender<Vec<Vec<u8>>>;
pub(crate) type MixMessageReceiver = mpsc::UnboundedReceiver<Vec<Vec<u8>>>;
// Channels used for one handler to requester another handler to check that the client is still
// active. The result is then passed back to the requesting handler in the oneshot channel.
pub(crate) type IsActiveRequestSender = mpsc::UnboundedSender<IsActiveResultSender>;
pub(crate) type IsActiveRequestReceiver = mpsc::UnboundedReceiver<IsActiveResultSender>;
#[derive(Debug)]
pub(crate) enum IsActive {
Active,
NotActive,
BusyPinging,
}
pub(crate) type IsActiveResultSender = oneshot::Sender<IsActive>;
@@ -70,9 +70,9 @@ impl<St: Storage> ConnectionHandler<St> {
}
fn update_clients_store_cache_entry(&mut self, client_address: DestinationAddressBytes) {
if let Some(client_sender) = self.active_clients_store.get(client_address) {
if let Some(client_senders) = self.active_clients_store.get(client_address) {
self.clients_store_cache
.insert(client_address, client_sender);
.insert(client_address, client_senders.mix_message_sender);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.1.28"
version = "1.1.29"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.28"
version = "1.1.29"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+8
View File
@@ -2,6 +2,14 @@
## [Unreleased]
## [v1.1.20-twix] (2023-09-05)
- nym-connect directory error handling ([#3830])
- NC - it should not be possible to toggle speedy mode while the connection is active ([#3816])
[#3830]: https://github.com/nymtech/nym/pull/3830
[#3816]: https://github.com/nymtech/nym/issues/3816
## [v1.1.19-snickers] (2023-08-29)
- NymConnect sometimes fails to connect because the gateway it fetches from the validator-api to use is running an old version (of the gateway binary) ([#3788])
+1 -1
View File
@@ -3992,7 +3992,7 @@ dependencies = [
[[package]]
name = "nym-connect"
version = "1.1.19"
version = "1.1.20"
dependencies = [
"anyhow",
"bip39",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@nym/nym-connect",
"version": "1.1.19",
"version": "1.1.20",
"main": "index.js",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-connect"
version = "1.1.19"
version = "1.1.20"
description = "nym-connect"
authors = ["Nym Technologies SA"]
license = ""
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-connect",
"version": "1.1.19"
"version": "1.1.20"
},
"build": {
"distDir": "../dist",
+71
View File
@@ -0,0 +1,71 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
release/
build/
# Gradle files
.gradle/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/gradle.xml
# .idea/assetWizardSettings.xml
# .idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# MacOS
.DS_Store
# App Specific cases
app/release/output.json
.idea/codeStyles/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 NymConnect
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
## NymConnect for Android
### Prerequisites
_TODO_
### Getting started
[Install](https://developer.android.com/studio/install) Android Studio and open
the project.\
Setup an android emulator using AVD.\
[Run](https://developer.android.com/studio/run/emulator) the project.
**⚠ NOTE**: be sure
to [set](https://developer.android.com/studio/run#changing-variant)
the build variant to `x86_64Debug` when running on emulator
### Features
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application with search
* Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
* Service will stay running in background after app has been closed
### Building
_TODO_
### Credits
This project is based on the "WG Tunnel" project made by Zane Schepke
https://github.com/zaneschepke/wgtunnel
### License
MIT
+2
View File
@@ -0,0 +1,2 @@
/build
/release
+159
View File
@@ -0,0 +1,159 @@
val rExtra = rootProject.extra
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
}
android {
namespace = "net.nymtech.nymconnect"
compileSdk = 34
val versionMajor = 1
val versionMinor = 0
val versionPatch = 0
val versionBuild = 0
defaultConfig {
applicationId = "net.nymtech.nymconnect"
minSdk = 28
targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isDebuggable = false
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
/* flavorDimensions += "abi"
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
create("arch64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += "arm64-v8a"
}
}
create("arm") {
dimension = "abi"
ndk {
abiFilters += "armeabi-v7a"
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += "x86_64"
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += "x86"
}
}
} */
}
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230706")
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
//accompanist
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
//db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
//icons
implementation("androidx.compose.material:material-icons-extended:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
}
kapt {
correctErrorTypes = true
}
@@ -0,0 +1,99 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "9:4468844863383145378",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
@@ -0,0 +1,94 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package net.nymtech.nymconnect
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("net.nymtech.nymconnect", appContext.packageName)
}
}
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!--android tv support-->
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:allowBackup="true"
android:name=".WireGuardAutoTunnel"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay"
android:name=".service.shortcut.ShortcutsActivity"/>
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:exported="true"
android:name=".service.tile.TunnelControlTile"
android:icon="@drawable/shield"
android:label="NymConnect"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true"
android:persistent="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true"/>
</service>
<service
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,6 @@
package net.nymtech.nymconnect
object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
}
@@ -0,0 +1,27 @@
package net.nymtech.nymconnect
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onCreate() {
super.onCreate()
settingsRepo.init()
}
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
}
}
@@ -0,0 +1,40 @@
package net.nymtech.nymconnect.module
import android.content.Context
import net.nymtech.nymconnect.service.tunnel.model.MyObjectBox
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.objectbox.Box
import io.objectbox.BoxStore
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class BoxModule {
@Provides
@Singleton
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
return MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
@Provides
@Singleton
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
return store.boxFor(Settings::class.java)
}
@Provides
@Singleton
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
return store.boxFor(TunnelConfig::class.java)
}
}
@@ -0,0 +1,25 @@
package net.nymtech.nymconnect.module
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.repository.SettingsBox
import net.nymtech.nymconnect.repository.TunnelBox
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
@Binds
@Singleton
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
}
@@ -0,0 +1,29 @@
package net.nymtech.nymconnect.module
import net.nymtech.nymconnect.service.network.MobileDataService
import net.nymtech.nymconnect.service.network.NetworkService
import net.nymtech.nymconnect.service.network.WifiService
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.notification.WireGuardNotification
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
}
@@ -0,0 +1,31 @@
package net.nymtech.nymconnect.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.WireGuardTunnel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideBackend(@ApplicationContext context : Context) : Backend {
return GoBackend(context)
}
@Provides
@Singleton
fun provideVpnService(backend: Backend) : VpnService {
return WireGuardTunnel(backend)
}
}
@@ -0,0 +1,39 @@
package net.nymtech.nymconnect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
}
}
} finally {
cancel()
}
}
}
}
}
@@ -0,0 +1,39 @@
package net.nymtech.nymconnect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceManager.stopVpnService(context)
delay(1000)
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
}
}
} finally {
cancel()
}
}
}
}
@@ -0,0 +1,16 @@
package net.nymtech.nymconnect.repository
import kotlinx.coroutines.flow.Flow
interface Repository<T> {
suspend fun save(t : T)
suspend fun saveAll(t : List<T>)
suspend fun getById(id : Long) : T?
suspend fun getAll() : List<T>?
suspend fun delete(t : T) : Boolean?
suspend fun count() : Long?
val itemFlow : Flow<MutableList<T>>
fun init()
}
@@ -0,0 +1,63 @@
package net.nymtech.nymconnect.repository
import net.nymtech.nymconnect.service.tunnel.model.Settings
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
CoroutineScope(Dispatchers.IO).launch {
if(getAll().isNullOrEmpty()) {
save(Settings())
}
}
}
override suspend fun save(t : Settings) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<Settings>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): Settings? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<Settings>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : Settings): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,57 @@
package net.nymtech.nymconnect.repository
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import javax.inject.Inject
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
}
override suspend fun save(t : TunnelConfig) {
Timber.d("Saving tunnel config")
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<TunnelConfig>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): TunnelConfig? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<TunnelConfig>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : TunnelConfig): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,6 @@
package net.nymtech.nymconnect.service.foreground
enum class Action {
START,
STOP
}
@@ -0,0 +1,64 @@
package net.nymtech.nymconnect.service.foreground
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import timber.log.Timber
open class ForegroundService : Service() {
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent")
}
} else {
Timber.d(
"with a null intent. It has been probably restarted by the system."
)
}
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras : Bundle?) {
if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true
}
protected open fun stopService(extras : Bundle?) {
Timber.d("Stopping ${this.javaClass.simpleName}")
try {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
}
@@ -0,0 +1,90 @@
package net.nymtech.nymconnect.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import net.nymtech.nymconnect.R
import timber.log.Timber
object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
when(action) {
Action.START -> {
try {
context.startForegroundService(intent)
} catch (e : Exception) {
Timber.e("Unable to start service foreground ${e.message}")
context.startService(intent)
}
}
Action.STOP -> context.startService(intent)
}
} catch (e : Exception) {
Timber.tag("ServiceManager").e(e)
}
}
fun startVpnService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
fun stopVpnService(context : Context) {
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java
)
}
fun startWatcherService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun stopWatcherService(context : Context) {
actionOnService(
Action.STOP, context,
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState(
context,
WireGuardConnectivityWatcherService::class.java,
)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
}
@@ -0,0 +1,6 @@
package net.nymtech.nymconnect.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -0,0 +1,210 @@
package net.nymtech.nymconnect.service.foreground
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import com.wireguard.android.backend.Tunnel
import net.nymtech.nymconnect.Constants
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.network.MobileDataService
import net.nymtech.nymconnect.service.network.NetworkService
import net.nymtech.nymconnect.service.network.NetworkStatus
import net.nymtech.nymconnect.service.network.WifiService
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122;
@Inject
lateinit var wifiService : NetworkService<WifiService>
@Inject
lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService : NotificationService
@Inject
lateinit var vpnService : VpnService
private var isWifiConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name;
override fun startService(extras: Bundle?) {
super.startService(extras)
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelConfig = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelConfig.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
}
}
override fun stopService(extras: Bundle?) {
super.stopService(extras)
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
cancelWatcherJob()
stopSelf()
}
private fun launchWatcherNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text))
super.startForeground(foregroundId, notification)
}
//try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
acquire()
}
}
}
private fun cancelWatcherJob() {
if(this::watcherJob.isInitialized) {
watcherJob.cancel()
}
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
}
launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
launch {
watchForMobileDataConnectivityChanges()
}
}
launch {
manageVpn()
}
}
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when(it) {
is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection")
isMobileDataConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
Timber.d("Lost mobile data connection")
}
else -> {}
}
}
}
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
}
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
}
else -> {}
}
}
}
private suspend fun manageVpn() {
while(watcherJob.isActive) {
if(setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this)
} else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
ServiceManager.stopVpnService(this)
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
}
}
}
@@ -0,0 +1,154 @@
package net.nymtech.nymconnect.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.receiver.NotificationActionReceiver
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.tunnel.HandshakeStatus
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123;
@Inject
lateinit var vpnService : VpnService
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService : NotificationService
private lateinit var job : Job
private var tunnelName : String = ""
override fun startService(extras : Bundle?) {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(Dispatchers.IO).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
launchVpnStartingNotification()
} catch (e : Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
} else {
Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
launchVpnStartingNotification()
}
}
}
}
CoroutineScope(job).launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
}
}
HandshakeStatus.UNHEALTHY -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
}
}
}
}
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
vpnService.stopTunnel()
}
cancelJob()
stopSelf()
}
private fun launchVpnConnectedNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.tunnel_start_title),
onGoing = false,
showTimestamp = true,
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnStartingNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.vpn_starting),
onGoing = false,
showTimestamp = true,
description = getString(R.string.attempt_connection)
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
showTimestamp = true,
description = message
)
super.startForeground(foregroundId, notification)
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel()
}
}
}
@@ -0,0 +1,117 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus = callbackFlow {
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose {
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
}
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
return info.ssid
}
}
return null
}
}
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network : Network) -> Result,
crossinline onAvailable: suspend (network : Network) -> Result,
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network)
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
}
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
val networkStatus : Flow<NetworkStatus>
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.net.Network
import android.net.NetworkCapabilities
sealed class NetworkStatus {
class Available(val network : Network) : NetworkStatus()
class Unavailable(val network : Network) : NetworkStatus()
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WifiService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
}
@@ -0,0 +1,21 @@
package net.nymtech.nymconnect.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
interface NotificationService {
fun createNotification(
channelId: String,
channelName: String,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp : Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = true,
onGoing: Boolean = true,
lights: Boolean = true
): Notification
}
@@ -0,0 +1,77 @@
package net.nymtech.nymconnect.service.notification
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
override fun createNotification(
channelId: String,
channelName: String,
title: String,
action: PendingIntent?,
actionText: String?,
description: String,
showTimestamp: Boolean,
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean
): Notification {
val channel = NotificationChannel(
channelId,
channelName,
importance
).let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
}
val builder: Notification.Builder =
Notification.Builder(
context,
channelId
)
return builder.let {
if(action != null && actionText != null) {
//TODO find a not deprecated way to do this
it.addAction(
Notification.Action.Builder(0, actionText, action)
.build())
it.setAutoCancel(true)
}
it.setContentTitle(title)
.setContentText(description)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setShowWhen(showTimestamp)
.setSmallIcon(R.mipmap.ic_launcher_foreground)
.build()
}
}
}
@@ -0,0 +1,28 @@
package net.nymtech.nymconnect.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.service.foreground.Action
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.name)) {
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
ServiceManager.toggleWatcherService(this, it)
}
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this)
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
?.let { ServiceManager.startVpnService(this, it) }
}
}
finish()
}
}
@@ -0,0 +1,73 @@
package net.nymtech.nymconnect.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.service.foreground.Action
import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
object ShortcutsManager {
private const val SHORT_LABEL_MAX_SIZE = 10;
private const val LONG_LABEL_MAX_SIZE = 25;
private const val APPEND_ON = " On";
private const val APPEND_OFF = " Off"
const val CLASS_NAME_EXTRA_KEY = "className"
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
longLabel : String, drawable : Int ) {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(IconCompat.createWithResource(context, drawable))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
}
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
createAndPushShortcut(context,
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
R.drawable.vpn_on
)
createAndPushShortcut(context,
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_OFF,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
R.drawable.vpn_off
)
}
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.START.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.STOP.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
}
@@ -0,0 +1,142 @@
package net.nymtech.nymconnect.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var settingsRepo : Repository<Settings>
@Inject
lateinit var configRepo : Repository<TunnelConfig>
@Inject
lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main);
private lateinit var job : Job
override fun onStartListening() {
job = scope.launch {
updateTileState()
}
super.onStartListening()
}
override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch {
updateTileState();
}
}
override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
val tunnel = determineTileTunnel();
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnService(this@TunnelControlTile, tunnel.toString())
}
}
} catch (e : Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null;
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
} else {
val config = configRepo.getAll()?.first();
config;
}
}
return tunnelConfig;
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherService(this@TunnelControlTile, tunnelConfig)
}
}
}
}
private suspend fun updateTileState() {
vpnService.state.collect {
when(it) {
Tunnel.State.UP -> {
qsTile.state = Tile.STATE_ACTIVE
}
Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE;
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
val config = determineTileTunnel();
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile()
}
}
private fun setTileDescription(description : String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description;
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
}
}
}
@@ -0,0 +1,14 @@
package net.nymtech.nymconnect.service.tunnel
enum class HandshakeStatus {
HEALTHY,
UNHEALTHY,
NEVER_CONNECTED,
NOT_STARTED;
companion object {
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
}
}
@@ -0,0 +1,18 @@
package net.nymtech.nymconnect.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import kotlinx.coroutines.flow.SharedFlow
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
suspend fun stopTunnel()
val state : SharedFlow<Tunnel.State>
val tunnelName : SharedFlow<String>
val statistics : SharedFlow<Statistics>
val lastHandshake : SharedFlow<Map<Key,Long>>
val handshakeStatus : SharedFlow<HandshakeStatus>
fun getState() : Tunnel.State
}

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