mirror of
https://github.com/TrustTunnel/TrustTunnel.git
synced 2026-04-26 04:26:26 +00:00
Pull request #60: Introduce a setup wizard tool
Merge in ADGUARD-CORE-LIBS/vpn-libs-endpoint from feature/AG-22596 to master Squashed commit of the following: commit 8927b3155db76dcc2e3cb45677c30774a4173b02 Merge: ac3b80768a3ae5Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Tue Jul 4 16:31:27 2023 +0300 Merge remote-tracking branch 'origin/master' into feature/AG-22596 # Conflicts: # Cargo.toml commit ac3b80744f8fa70c13ef1b58298982bb4d0cebc9 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jul 3 16:44:50 2023 +0300 wizard: allow specifying multiple client through dialogue commit fc718a24d824857287a80e099e22142a9f7e36b6 Merge: 732c1b33b5b0e7Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jul 3 13:49:12 2023 +0300 Merge remote-tracking branch 'origin/master' into feature/AG-22596 # Conflicts: # Cargo.toml # examples/my_vpn/auth_info.txt # examples/my_vpn/vpn.toml # lib/src/authentication/file_based.rs # lib/src/settings.rs commit 732c1b3ead367bb2b0740d86ba255d8c3334446e Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Tue Jun 27 14:25:21 2023 +0300 wizard: minor commit 284182a2d3d75ebefb968b0a44316b889e30036d Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 19:39:35 2023 +0300 macros: fix doc commit 250d7d8f5759c2618281147d0aa159c13eda0238 Merge: d944c6f3df93e3Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 19:35:30 2023 +0300 Merge remote-tracking branch 'origin/master' into feature/AG-22596 # Conflicts: # Cargo.toml # lib/Cargo.toml # lib/src/settings.rs commit d944c6f21675841a0511da4c6c158d47fff0b30e Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 16:16:37 2023 +0300 Revert "Revert "remove accidental changes"" This reverts commit 00b8f98dbd7bb98baf91403fa98a6b604c63d50c. commit 7fd663c2deff4ad568e1047ba31346b698c7baf1 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 16:15:02 2023 +0300 :security: commit 4bab5e857dbfdfc8af2a25cb220d870160a36973 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 11:18:37 2023 +0300 wizard: minor commit fb31f912b0ddf1235ec6451bd8c849d38619ff28 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 11:16:10 2023 +0300 wizard: fix non-interactive mode commit 10d106a440a21e5fd99711aed5f9aa28d0e02b1e Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Mon Jun 26 11:13:21 2023 +0300 wizard: add an option to specify certificate path commit 960f6457ad099875c29b204f0b7758a4f64736dc Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Fri Jun 23 14:37:21 2023 +0300 Deduplicate docs + print descriptions and disabled features into output file commit cf55c8ee8a0a69410c1b67643d7646d81dfb5123 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Thu Jun 22 14:27:52 2023 +0300 wizard: get rid of excessive modes and be less picky on user commit 7d25c5b3297c0b4fd99c712cab10050547dea504 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Fri Jun 9 17:24:40 2023 +0300 fix common name commit 2715f246c3feaf24f4fb0f14e3670d71a168bcf2 Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Fri Jun 9 13:17:03 2023 +0300 wizard: add common name in alt names as well commit f5003a5008fbc54468ce59608c3038f9bcbf154a Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Thu Jun 8 18:04:41 2023 +0300 wizard: don't accept empty string without the default value commit 64b3b7f432169c3332304ac667bf7daa4d2938fc Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Wed Jun 7 19:48:01 2023 +0300 build binaries along with running unit tests commit 2356b188493f446a683b61d65f0e58cf4129727f Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Wed Jun 7 19:16:55 2023 +0300 Fix readme commit 07aa8fae5a4b94c078324803a107e23c65ab764a Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Wed Jun 7 19:03:29 2023 +0300 bench: use the wizard for configuration commit 00b8f98dbd7bb98baf91403fa98a6b604c63d50c Author: Sergei Gunchenko <s.gunchenko@adguard.com> Date: Wed Jun 7 17:59:54 2023 +0300 Revert "remove accidental changes" This reverts commit d52bac61d50f97ffea3bdb30a9c6fa82a5c2b52d. ... and 15 more commits
This commit is contained in:
31
Cargo.toml
31
Cargo.toml
@@ -1,25 +1,10 @@
|
||||
[package]
|
||||
name = "vpn_endpoint"
|
||||
version = "0.9.49"
|
||||
authors = ["Sergei Gunchenko <s.gunchenko@adguard.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "vpn_endpoint"
|
||||
doctest = false
|
||||
|
||||
[workspace]
|
||||
# The empty workspace prevents redundant builds of each component
|
||||
members = [
|
||||
"endpoint",
|
||||
"lib",
|
||||
"macros",
|
||||
"tools",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "=4.3.8"
|
||||
console-subscriber = { version = "=0.1.9", optional = true }
|
||||
log = "=0.4.19"
|
||||
sentry = { version = "=0.31.5", default-features = false, features = ["backtrace", "panic", "reqwest", "rustls", "contexts"] }
|
||||
tokio = { version = "=1.28.2", features = ["rt-multi-thread", "signal"] }
|
||||
toml = { version = "=0.7.4", default-features = false, features = ["parse"] }
|
||||
vpn_libs_endpoint = { version = "0.1", path = "lib" }
|
||||
|
||||
[features]
|
||||
# RUSTFLAGS="--cfg tokio_unstable" must also be set
|
||||
tracing = ["vpn_libs_endpoint/tracing", "tokio/tracing", "dep:console-subscriber"]
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
75
README.md
75
README.md
@@ -3,45 +3,43 @@
|
||||
## Building
|
||||
|
||||
Execute the following commands in the Terminal:
|
||||
|
||||
```shell
|
||||
cargo build
|
||||
```
|
||||
|
||||
to build the debug version, or
|
||||
|
||||
```shell
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
to build the release version.
|
||||
|
||||
## Issuing self-signed cert and keys (RSA)
|
||||
|
||||
Execute the following commands in the Terminal:
|
||||
```shell
|
||||
openssl req -config <openssl.conf> -new -x509 -sha256 -newkey rsa:2048 -nodes -days 1000 -keyout key.pem -out cert.pem
|
||||
```
|
||||
where
|
||||
* `<openssl.conf>` is an optional OpenSSL request template file
|
||||
|
||||
## Endpoint configuration
|
||||
|
||||
### Library configuration
|
||||
|
||||
An endpoint can be configured using a couple of TOML files.
|
||||
An endpoint can be configured using a couple of TOML files:
|
||||
|
||||
#### Settings
|
||||
1) The main library settings reflect (`struct Settings` in [settings.rs](./lib/src/settings.rs)).
|
||||
2) The TLS hosts library settings reflect (`struct TlsHostsSettings` in [settings.rs](./lib/src/settings.rs)).
|
||||
These settings may be reloaded dynamically (see [here](#dynamic-reloading-of-tls-hosts-settings) for details).
|
||||
|
||||
The example file with the full set of available options and their descriptions
|
||||
can be found [here](./examples/my_vpn/vpn.toml).
|
||||
The file structure reflects the library settings (`struct Settings` in [settings.rs](./lib/src/settings.rs)).
|
||||
All of them may be generated using the [setup wizard](./tools/setup_wizard) tool.
|
||||
To configure the most basic options, execute the following command in the Terminal:
|
||||
|
||||
#### TlsHostsSettings
|
||||
```shell
|
||||
cargo run --bin setup_wizard
|
||||
```
|
||||
|
||||
The example file with the full set of available options and their descriptions
|
||||
can be found [here](./examples/my_vpn/tls_hosts.toml).
|
||||
The file structure reflects the TLS hosts library settings
|
||||
(`struct TlsHostsSettings` in [settings.rs](./lib/src/settings.rs)).
|
||||
These settings may be reloaded dynamically (see [here](#dynamic-reloading-of-tls-hosts-settings) for details).
|
||||
To see the full set of available options, execute the following command in the Terminal:
|
||||
|
||||
### Executable features
|
||||
```shell
|
||||
cargo run --bin setup_wizard -- -h
|
||||
```
|
||||
|
||||
### Endpoint executable features
|
||||
|
||||
#### Configuration
|
||||
|
||||
@@ -54,9 +52,10 @@ line arguments. For example:
|
||||
* Logging file is configured by `--log_file <path>`. If not specified, the instance logs
|
||||
to `stdout`.
|
||||
|
||||
To see the full set of available options, execute the following commands in the Terminal:
|
||||
To see the full set of available options, execute the following command in the Terminal:
|
||||
|
||||
```shell
|
||||
<path/to/target>/vpn_endpoint -h
|
||||
cargo run --bin vpn_endpoint -- -h
|
||||
```
|
||||
|
||||
#### Dynamic reloading of TLS hosts settings
|
||||
@@ -71,50 +70,42 @@ the next reloading.
|
||||
## Running
|
||||
|
||||
To run the binary through `cargo`, execute the following commands in the Terminal:
|
||||
|
||||
```shell
|
||||
cargo run --bin vpn_endpoint -- <path/to/vpn.config> <path/to/tls_hosts.config>
|
||||
```
|
||||
|
||||
To run the binary directly, execute the following commands in the Terminal:
|
||||
|
||||
```shell
|
||||
<path/to/target>/vpn_endpoint <path/to/vpn.config> <path/to/tls_hosts.config>
|
||||
```
|
||||
|
||||
where `<path/to/target>` is determined by the build command (by default it is `./target/debug` or
|
||||
`./target/release` depending on the build type).
|
||||
|
||||
## Example endpoint
|
||||
|
||||
For a quic setup, you can run the example endpoint (see [here](./examples/my_vpn)).
|
||||
It shows the essential things needed to run an instance.
|
||||
To start one, run the following commands in the Terminal:
|
||||
```shell
|
||||
cd ./examples/my_vpn && ./run.sh
|
||||
```
|
||||
It may ask you to enter some information for generating your certificate.
|
||||
Skip it clicking `enter` if it does not matter.
|
||||
|
||||
## Testing with Google Chrome
|
||||
|
||||
1) 2 options:
|
||||
* Add the generated certificate to the trusted store and run the Google Chrome
|
||||
* Run the Google Chrome from Terminal like this:
|
||||
* Add the generated certificate to the trusted store and run the Google Chrome
|
||||
* Run the Google Chrome from Terminal like this:
|
||||
```shell
|
||||
google-chrome --ignore-certificate-errors
|
||||
```
|
||||
**IMPORTANT:** the second option should be used just for testing, it removes the first line
|
||||
of defence against malicious resources
|
||||
2) Set up the endpoint as an HTTPS proxy server in the browser (either via browser settings or
|
||||
using an extension like `Proxy SwitchyOmega`)
|
||||
of defence against malicious resources
|
||||
2) Set up the endpoint as an HTTPS proxy server in the browser (either via browser settings or
|
||||
using an extension like `Proxy SwitchyOmega`)
|
||||
|
||||
## Collecting metrics
|
||||
|
||||
Common ways:
|
||||
|
||||
* As plain text: send a GET request to `<ip>:<port>/metrics`, for example, using CURL
|
||||
or a web browser
|
||||
or a web browser
|
||||
* Set up Prometheus:
|
||||
1) Configure the instance to monitor the endpoint metrics (see [here](https://prometheus.io/docs/prometheus/latest/getting_started/#configure-prometheus-to-monitor-the-sample-targets))
|
||||
2) Use [the graph interface](https://prometheus.io/docs/prometheus/latest/getting_started/#using-the-graphing-interface)
|
||||
1) Configure the instance to monitor the endpoint metrics (see [here](https://prometheus.io/docs/prometheus/latest/getting_started/#configure-prometheus-to-monitor-the-sample-targets))
|
||||
2) Use [the graph interface](https://prometheus.io/docs/prometheus/latest/getting_started/#using-the-graphing-interface)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ Increment version:
|
||||
|
||||
git remote set-url origin ${bamboo_planRepository_repositoryUrl}
|
||||
git pull
|
||||
git reset
|
||||
git reset
|
||||
|
||||
./scripts/increment_version.sh ${bamboo_custom_version}
|
||||
git add Cargo.toml CHANGELOG.md
|
||||
git add ./endpoint/Cargo.toml CHANGELOG.md
|
||||
|
||||
git commit -m "skipci: Automatic version increment by Bamboo"
|
||||
git push
|
||||
|
||||
@@ -29,10 +29,18 @@ Test on Linux:
|
||||
force-clean-build: 'true'
|
||||
- script:
|
||||
interpreter: SHELL
|
||||
description: Run tests
|
||||
scripts:
|
||||
- |-
|
||||
set -x -e
|
||||
cargo test --workspace
|
||||
- script:
|
||||
interpreter: SHELL
|
||||
description: Build binaries
|
||||
scripts:
|
||||
- |-
|
||||
set -x -e
|
||||
cargo build --bins
|
||||
requirements:
|
||||
- adg-privileged-docker
|
||||
artifact-subscriptions: [ ]
|
||||
@@ -45,10 +53,18 @@ Test on macOS:
|
||||
force-clean-build: 'true'
|
||||
- script:
|
||||
interpreter: SHELL
|
||||
description: Run tests
|
||||
scripts:
|
||||
- |-
|
||||
set -x -e
|
||||
cargo test --workspace --target x86_64-apple-darwin
|
||||
- script:
|
||||
interpreter: SHELL
|
||||
description: Build binaries
|
||||
scripts:
|
||||
- |-
|
||||
set -x -e
|
||||
cargo build --bins --target x86_64-apple-darwin
|
||||
requirements:
|
||||
- macOS 11.0 or later
|
||||
artifact-subscriptions: [ ]
|
||||
|
||||
@@ -16,28 +16,16 @@ COPY $ENDPOINT_DIR /bench/$ENDPOINT_DIR
|
||||
|
||||
WORKDIR /bench/
|
||||
RUN cd "$ENDPOINT_DIR" && \
|
||||
cargo build --release --bin setup_wizard && \
|
||||
cargo build --release --bin vpn_endpoint && \
|
||||
mv ./target/release/vpn_endpoint /bench/ && \
|
||||
cd examples/my_vpn && \
|
||||
openssl req -config openssl.conf -new -x509 -sha256 -newkey rsa:2048 -nodes -days 1000 \
|
||||
-keyout /bench/key.pem -out /bench/cert.pem \
|
||||
-subj "/C=de/CN=$ENDPOINT_HOSTNAME" \
|
||||
-addext "subjectAltName = DNS:*.$ENDPOINT_HOSTNAME"
|
||||
mv ./target/release/setup_wizard ./target/release/vpn_endpoint /bench/
|
||||
|
||||
RUN echo "\
|
||||
listen_address = \"[::]:4433\"\n\
|
||||
allow_private_network_connections = true\n\
|
||||
[listen_protocols.http1]\n\
|
||||
[listen_protocols.http2]\n\
|
||||
[listen_protocols.quic]\n\
|
||||
" >>$CONFIG_FILE
|
||||
|
||||
RUN echo "\
|
||||
[[main_hosts]]\n\
|
||||
hostname = \"$ENDPOINT_HOSTNAME\"\n\
|
||||
cert_chain_path = \"cert.pem\"\n\
|
||||
private_key_path = \"key.pem\"\n\
|
||||
" >>$TLS_HOSTS_SETTINGS_FILE
|
||||
RUN ./setup_wizard --mode non-interactive \
|
||||
--address [::]:4433 \
|
||||
--creds premium:premium \
|
||||
--hostname "$ENDPOINT_HOSTNAME" \
|
||||
--lib-settings "$CONFIG_FILE" \
|
||||
--hosts-settings "$TLS_HOSTS_SETTINGS_FILE"
|
||||
|
||||
ENV LOG_LEVEL=$LOG_LEVEL \
|
||||
CONFIG_FILE=$CONFIG_FILE \
|
||||
|
||||
22
endpoint/Cargo.toml
Normal file
22
endpoint/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "vpn_endpoint"
|
||||
version = "0.9.49"
|
||||
authors = ["Sergei Gunchenko <s.gunchenko@adguard.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "vpn_endpoint"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
clap = "=4.3.8"
|
||||
console-subscriber = { version = "=0.1.9", optional = true }
|
||||
log = "=0.4.19"
|
||||
sentry = { version = "=0.31.5", default-features = false, features = ["backtrace", "panic", "reqwest", "rustls", "contexts"] }
|
||||
tokio = { version = "=1.28.2", features = ["rt-multi-thread", "signal"] }
|
||||
toml = { version = "=0.7.4", default-features = false, features = ["parse"] }
|
||||
vpn_libs_endpoint = { version = "0.1", path = "../lib" }
|
||||
|
||||
[features]
|
||||
# RUSTFLAGS="--cfg tokio_unstable" must also be set
|
||||
tracing = ["vpn_libs_endpoint/tracing", "tokio/tracing", "dep:console-subscriber"]
|
||||
1
examples/my_vpn/.gitignore
vendored
1
examples/my_vpn/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
*.pem
|
||||
@@ -1,7 +0,0 @@
|
||||
[[client]]
|
||||
username = "premium"
|
||||
password = "premium"
|
||||
|
||||
[[client]]
|
||||
username = "free"
|
||||
password = "free"
|
||||
@@ -1,81 +0,0 @@
|
||||
[req]
|
||||
distinguished_name = subject
|
||||
req_extensions = req_ext
|
||||
x509_extensions = x509_ext
|
||||
string_mask = utf8only
|
||||
|
||||
# The Subject DN can be formed using X501 or RFC 4514 (see RFC 4519 for a description).
|
||||
# Its sort of a mashup. For example, RFC 4514 does not provide emailAddress.
|
||||
[ subject ]
|
||||
countryName = Country Name (2 letter code)
|
||||
countryName_default = MC
|
||||
|
||||
stateOrProvinceName = State or Province Name (full name)
|
||||
stateOrProvinceName_default = My State
|
||||
|
||||
localityName = Locality Name (eg, city)
|
||||
localityName_default = My Locality
|
||||
|
||||
organizationName = Organization Name (eg, company)
|
||||
organizationName_default = My Organization Limited
|
||||
|
||||
# Use a friendly name here because its presented to the user. The server's DNS
|
||||
# names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
|
||||
# by both IETF and CA/Browser Forums. If you place a DNS name here, then you
|
||||
# must include the DNS name in the SAN too (otherwise, Chrome and others that
|
||||
# strictly follow the CA/Browser Baseline Requirements will fail).
|
||||
commonName = Common Name (e.g. server FQDN or YOUR name)
|
||||
commonName_default = localhost
|
||||
|
||||
emailAddress = Email Address
|
||||
emailAddress_default = support@email.com
|
||||
|
||||
# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ...
|
||||
[ x509_ext ]
|
||||
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid,issuer
|
||||
|
||||
# You only need digitalSignature below. *If* you don't allow
|
||||
# RSA Key transport (i.e., you use ephemeral cipher suites), then
|
||||
# omit keyEncipherment because that's key transport.
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
subjectAltName = @alternate_names
|
||||
nsComment = "OpenSSL Generated Certificate"
|
||||
|
||||
# RFC 5280, Section 4.2.1.12 makes EKU optional
|
||||
# CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused
|
||||
# In either case, you probably only need serverAuth.
|
||||
# extendedKeyUsage = serverAuth, clientAuth
|
||||
|
||||
# Section req_ext is used when generating a certificate signing request. I.e., openssl req ...
|
||||
[ req_ext ]
|
||||
|
||||
subjectKeyIdentifier = hash
|
||||
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
subjectAltName = @alternate_names
|
||||
nsComment = "OpenSSL Generated Certificate"
|
||||
|
||||
# RFC 5280, Section 4.2.1.12 makes EKU optional
|
||||
# CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused
|
||||
# In either case, you probably only need serverAuth.
|
||||
# extendedKeyUsage = serverAuth, clientAuth
|
||||
|
||||
[ alternate_names ]
|
||||
|
||||
DNS.1 = localhost
|
||||
DNS.2 = localhost.localdomain
|
||||
DNS.3 = 127.0.0.1
|
||||
|
||||
# IPv6 localhost
|
||||
# DNS.4 = ::1
|
||||
|
||||
[ root_ca ]
|
||||
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
basicConstraints = critical, CA:TRUE
|
||||
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
OPENSSL_CONF_FILE="openssl.conf"
|
||||
KEY_FILE_NAME="key.pem"
|
||||
CERT_FILE_NAME="cert.pem"
|
||||
VPN_CONF_FILE="vpn.conf"
|
||||
TLS_HOSTS_FILE="tls_hosts.conf"
|
||||
|
||||
if [ ! -f "$KEY_FILE_NAME" ] || [ ! -f "$CERT_FILE_NAME" ]; then
|
||||
openssl req -config "$OPENSSL_CONF_FILE" -new -x509 -sha256 -newkey rsa:2048 -nodes -days 1000 \
|
||||
-keyout "$KEY_FILE_NAME" -out "$CERT_FILE_NAME"
|
||||
fi
|
||||
|
||||
cargo run --package vpn_endpoint --bin vpn_endpoint "$VPN_CONF_FILE" "$TLS_HOSTS_FILE"
|
||||
@@ -1,55 +0,0 @@
|
||||
# The TLS hosts for traffic tunneling and service requests handling.
|
||||
[[main_hosts]]
|
||||
# Used as a key for selecting a certificate chain in TLS handshake.
|
||||
# MUST be unique.
|
||||
hostname = "localhost"
|
||||
# Path to a file containing the certificate chain.
|
||||
# MUST remain valid until the endpoint is running or the next TLS hosts settings reload.
|
||||
cert_chain_path = "cert.pem"
|
||||
# Path to a file containing the private key.
|
||||
# May be equal to `cert_chain_path` if it contains both of them.
|
||||
# MUST remain valid until the endpoint is running or the next TLS hosts settings reload.
|
||||
private_key_path = "key.pem"
|
||||
|
||||
[[main_hosts]]
|
||||
hostname = "tunnel.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
|
||||
# The TLS hosts for HTTPS pinging.
|
||||
# With this one set up the endpoint responds with `200 OK` to HTTPS `GET` requests
|
||||
# to the specified domains.
|
||||
[[ping_hosts]]
|
||||
hostname = "ping.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
|
||||
[[ping_hosts]]
|
||||
hostname = "ping2.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
|
||||
# The TLS hosts for speed testing.
|
||||
# With this one set up the endpoint accepts connections to the specified hosts and
|
||||
# handles HTTP requests in the following way:
|
||||
# * `GET` requests with `/Nmb.bin` path (where `N` is 1 to 100, e.g. `/100mb.bin`)
|
||||
# are considered as download speedtest transferring `N` megabytes to a client
|
||||
# * `POST` requests with `/upload.html` path and `Content-Length: N`
|
||||
# are considered as upload speedtest receiving `N` bytes from a client,
|
||||
# where `N` is up to 120 * 1024 * 1024 bytes
|
||||
[[speedtest_hosts]]
|
||||
hostname = "speed.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
|
||||
[[speedtest_hosts]]
|
||||
hostname = "speed2.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
|
||||
# The TLS hosts for the connections must be forwarded to the reverse proxy.
|
||||
# Only makes sense if the reverse proxy is set up, otherwise it is ignored.
|
||||
[[reverse_proxy_hosts]]
|
||||
hostname = "hello.localhost"
|
||||
cert_chain_path = "cert.pem"
|
||||
private_key_path = "key.pem"
|
||||
@@ -1,190 +0,0 @@
|
||||
# (Required) The address to listen on.
|
||||
listen_address = "[::]:4433"
|
||||
|
||||
# Whether IPv6 connections can be routed or rejected with unreachable status.
|
||||
# Default is true.
|
||||
ipv6_available = true
|
||||
|
||||
# Whether connections to private network of the endpoint are allowed.
|
||||
# Default is false.
|
||||
allow_private_network_connections = false
|
||||
|
||||
# Timeout of a TLS handshake in seconds.
|
||||
# Default is 10s.
|
||||
tls_handshake_timeout_secs = 10
|
||||
|
||||
# Timeout of a client listener in seconds.
|
||||
# Default is 600s.
|
||||
client_listener_timeout_secs = 600
|
||||
|
||||
# Timeout of connection establishment in seconds. For example, it is related to
|
||||
# client's connection requests.
|
||||
# Default is 30s.
|
||||
connection_establishment_timeout_secs = 30
|
||||
|
||||
# Idle timeout of tunneled TCP connections in seconds.
|
||||
# Default is 604800s (1 week).
|
||||
tcp_connections_timeout_secs = 604800
|
||||
|
||||
# Timeout of tunneled UDP "connections" in seconds.
|
||||
# Default is 300s.
|
||||
udp_connections_timeout_secs = 300
|
||||
|
||||
# (Optional) The registry of client credentials.
|
||||
# Must be TOML-formatted and contain an array of clients:
|
||||
# [[client]]
|
||||
# username = "a name"
|
||||
# password = "a password"
|
||||
# [[client]]
|
||||
# ...
|
||||
#
|
||||
# If this one is omitted:
|
||||
# * if `forward_protocol` is set to `socks5`, the endpoint will try to authenticate
|
||||
# requests using the SOCKS5 authentication protocol,
|
||||
# * otherwise, any client is welcome.
|
||||
credentials_file = "auth_info.txt"
|
||||
|
||||
# (Required) The set of connection forwarder settings.
|
||||
# Possible values:
|
||||
# * direct: a direct forwarder routes a connection directly to its target host,
|
||||
# * socks5: a SOCKS5 forwarder routes a connection though a SOCKS5 proxy.
|
||||
# Default is direct
|
||||
#[forward_protocol]
|
||||
|
||||
# The set of direct forwarder settings.
|
||||
[forward_protocol.direct]
|
||||
|
||||
# The set of SOCKS5 forwarder settings.
|
||||
#[forward_protocol.socks5]
|
||||
## (Required) The address of the SOCKS5 server.
|
||||
#address = "127.0.0.1:1080"
|
||||
## Enable/disable extended authentication.
|
||||
## See lib/README.md#extended-authentication for details.
|
||||
## Disabled by default.
|
||||
#extended_auth = false
|
||||
|
||||
# The list of enabled client listener codecs.
|
||||
# Possible values:
|
||||
# * http1: enables HTTP1 codec,
|
||||
# * http2: enables HTTP2 codec,
|
||||
# * quic: enables QUIC/HTTP3 codec.
|
||||
# At least one listener codec MUST be specified.
|
||||
[listen_protocols]
|
||||
# The set of HTTP1 listener codec settings.
|
||||
[listen_protocols.http1]
|
||||
# Buffer size for outgoing traffic.
|
||||
# Default is 32K.
|
||||
upload_buffer_size = 32768
|
||||
|
||||
# The set of HTTP2 listener codec settings.
|
||||
[listen_protocols.http2]
|
||||
# The initial window size (in octets) for connection-level flow control for received data.
|
||||
# Default is 8M.
|
||||
initial_connection_window_size = 8_388_608
|
||||
# The initial window size (in octets) for stream-level flow control for received data.
|
||||
# Default is 128K.
|
||||
initial_stream_window_size = 131072
|
||||
# The number of streams that the sender permits the receiver to create.
|
||||
# Default is 1000.
|
||||
max_concurrent_streams = 1000
|
||||
# The size (in octets) of the largest HTTP/2 frame payload that we are able to accept.
|
||||
# Default is 16K.
|
||||
max_frame_size = 16384
|
||||
# The max size of received header frames.
|
||||
# Default is 64K.
|
||||
header_table_size = 65536
|
||||
|
||||
# The set of QUIC listener codec settings.
|
||||
[listen_protocols.quic]
|
||||
# The size of UDP payloads that the endpoint is willing to receive. UDP datagrams with
|
||||
# payloads larger than this limit are not likely to be processed.
|
||||
# Default is 1350.
|
||||
recv_udp_payload_size = 1350
|
||||
# The size of UDP payloads that the endpoint is willing to send.
|
||||
# Default is 1350.
|
||||
send_udp_payload_size = 1350
|
||||
# The initial value for the maximum amount of data that can be sent on the connection.
|
||||
# Default is 100M.
|
||||
initial_max_data = 104_857_600
|
||||
# The initial flow control limit for locally initiated bidirectional streams.
|
||||
# Default is 1M.
|
||||
max_stream_data_bidi_local = 1_048_576
|
||||
# The initial flow control limit for peer-initiated bidirectional streams.
|
||||
# Default is 1M.
|
||||
max_stream_data_bidi_remote = 1_048_576
|
||||
# The initial flow control limit for unidirectional streams.
|
||||
# Default is 1M.
|
||||
max_stream_data_uni = 1_048_576
|
||||
# The initial maximum number of bidirectional streams the endpoint that receives this
|
||||
# transport parameter is permitted to initiate.
|
||||
# Default is 4K.
|
||||
max_streams_bidi = 4096
|
||||
# The initial maximum number of unidirectional streams the endpoint that receives this
|
||||
# transport parameter is permitted to initiate.
|
||||
# Default is 4K.
|
||||
max_streams_uni = 4096
|
||||
# The maximum size of the connection window.
|
||||
# Default is 24M.
|
||||
max_connection_window = 25_165_824
|
||||
# The maximum size of the stream window.
|
||||
# Default is 16M.
|
||||
max_stream_window = 16_777_216
|
||||
# Whether the active connection migration is enabled on the address being used
|
||||
# during the handshake.
|
||||
# Disabled by default.
|
||||
disable_active_migration = true
|
||||
# Whether sending or receiving early data is enabled.
|
||||
# Enabled by default.
|
||||
enable_early_data = true
|
||||
# The capacity of the QUIC multiplexer message queue.
|
||||
# Decreasing it may cause packet dropping in case the multiplexer cannot keep up the pace.
|
||||
# Increasing it may lead to high memory consumption.
|
||||
# Default is 4K.
|
||||
message_queue_capacity = 4096
|
||||
|
||||
# (Optional) The reverse proxy settings.
|
||||
# With this one set up the endpoint does TLS termination on matching connections and
|
||||
# translates HTTP/x traffic into HTTP/1.1 protocol towards the server and back
|
||||
# into original HTTP/x towards the client. Like this:
|
||||
#
|
||||
# ```(client) TLS(HTTP/x) <--(endpoint)--> (server) HTTP/1.1```
|
||||
#
|
||||
# The translated HTTP/1.1 requests have the custom header `X-Original-Protocol`
|
||||
# appended. For now, its value can be either `HTTP1`, or `HTTP3`.
|
||||
# TLS hosts for the reverse proxy channel are configured through the TLS hosts settings.
|
||||
[reverse_proxy]
|
||||
# (Required) The origin server address.
|
||||
server_address = "127.0.0.1:1111"
|
||||
# Connections to the main hosts with paths starting with this mask are routed
|
||||
# to the reverse proxy server.
|
||||
# MUST not be empty and start with slash.
|
||||
path_mask = "/proxy"
|
||||
# With this one set to `true` the endpoint overrides the HTTP method while
|
||||
# translating an HTTP3 request to HTTP1 in case the request has the `GET` method
|
||||
# and its path is `/`
|
||||
# Disabled by default.
|
||||
h3_backward_compatibility = false
|
||||
|
||||
# (Optional) The ICMP forwarding settings.
|
||||
# Setting up this feature requires superuser rights on some systems.
|
||||
[icmp]
|
||||
# (Required) The name of a network interface to bind the outbound ICMP socket to.
|
||||
interface_name = "eth0"
|
||||
# Timeout of tunneled ICMP requests in seconds.
|
||||
# Default is 3s.
|
||||
request_timeout_secs = 3
|
||||
# The capacity of the ICMP multiplexer received messages queue.
|
||||
# Decreasing it may cause packet dropping in case the multiplexer cannot keep up the pace.
|
||||
# Increasing it may lead to high memory consumption.
|
||||
# Each client has its own queue.
|
||||
# Default is 256.
|
||||
recv_message_queue_capacity = 256
|
||||
|
||||
# (Optional) The metrics gathering request handler settings.
|
||||
[metrics]
|
||||
# The address to listen on for settings export requests.
|
||||
# Default is 0.0.0.0:1987.
|
||||
address = "0.0.0.0:1987"
|
||||
# Timeout of a metrics request in seconds.
|
||||
# Default is 3s.
|
||||
request_timeout_secs = 3
|
||||
@@ -25,6 +25,7 @@ httparse = "=1.8.0"
|
||||
lazy_static = "=1.4.0"
|
||||
libc = "=0.2.147"
|
||||
log = "=0.4.19"
|
||||
macros = { version = "0.1.0", path = "../macros", optional = true }
|
||||
once_cell = "=1.18.0"
|
||||
prometheus = { version = "=0.13.3", features = ["process"] }
|
||||
quiche = { version = "=0.17.2", features = ["qlog"] }
|
||||
@@ -43,4 +44,5 @@ hyper = { version = "=0.14.26", features = ["http1", "http2", "client", "server"
|
||||
rustls = { version = "=0.21.2", features = ["logging", "dangerous_configuration"] }
|
||||
|
||||
[features]
|
||||
rt_doc = ["dep:macros"]
|
||||
tracing = ["tokio/tracing"]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "rt_doc")]
|
||||
extern crate macros;
|
||||
|
||||
pub mod authentication;
|
||||
pub mod core;
|
||||
|
||||
@@ -6,11 +6,14 @@ use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "rt_doc")]
|
||||
use macros::{Getter, RuntimeDoc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::{Document, Item};
|
||||
use authentication::registry_based::RegistryBasedAuthenticator;
|
||||
use crate::authentication::Authenticator;
|
||||
use crate::{authentication, utils};
|
||||
use serde::Deserialize;
|
||||
use toml_edit::{Document, Item};
|
||||
|
||||
pub type Socks5BuilderResult<T> = Result<T, Socks5Error>;
|
||||
|
||||
@@ -55,49 +58,48 @@ impl Debug for Socks5Error {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct Settings {
|
||||
/// The address to listen on
|
||||
#[serde(default = "Settings::default_listen_address")]
|
||||
pub(crate) listen_address: SocketAddr,
|
||||
/// See [`SettingsBuilder::reverse_proxy`]
|
||||
pub(crate) reverse_proxy: Option<ReverseProxySettings>,
|
||||
/// IPv6 availability
|
||||
/// Whether IPv6 connections can be routed or rejected with unreachable status
|
||||
#[serde(default = "Settings::default_ipv6_available")]
|
||||
pub(crate) ipv6_available: bool,
|
||||
/// Whether connections to private network of the endpoint are allowed
|
||||
#[serde(default = "Settings::default_allow_private_network_connections")]
|
||||
pub(crate) allow_private_network_connections: bool,
|
||||
/// Timeout of a TLS handshake
|
||||
/// Timeout of an incoming TLS handshake
|
||||
#[serde(default = "Settings::default_tls_handshake_timeout")]
|
||||
#[serde(rename(deserialize = "tls_handshake_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "tls_handshake_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) tls_handshake_timeout: Duration,
|
||||
/// Timeout of a client listener
|
||||
#[serde(default = "Settings::default_client_listener_timeout")]
|
||||
#[serde(rename(deserialize = "client_listener_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "client_listener_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) client_listener_timeout: Duration,
|
||||
/// Timeout of connection establishment. For example, it is related to
|
||||
/// client's connection requests.
|
||||
/// Timeout of outgoing connection establishment.
|
||||
/// For example, it is related to client's connection requests.
|
||||
#[serde(default = "Settings::default_connection_establishment_timeout")]
|
||||
#[serde(rename(deserialize = "connection_establishment_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "connection_establishment_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) connection_establishment_timeout: Duration,
|
||||
/// Timeout of tunneled TCP connections
|
||||
/// Idle timeout of tunneled TCP connections
|
||||
#[serde(default = "Settings::default_tcp_connections_timeout")]
|
||||
#[serde(rename(deserialize = "tcp_connections_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "tcp_connections_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) tcp_connections_timeout: Duration,
|
||||
/// Timeout of tunneled UDP "connections"
|
||||
#[serde(default = "Settings::default_udp_connections_timeout")]
|
||||
#[serde(rename(deserialize = "udp_connections_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "udp_connections_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) udp_connections_timeout: Duration,
|
||||
/// The forwarder codec settings
|
||||
/// The set of connection forwarder settings
|
||||
#[serde(default)]
|
||||
pub(crate) forward_protocol: ForwardProtocolSettings,
|
||||
/// The listener codec settings
|
||||
/// The set of enabled client listener codecs
|
||||
pub(crate) listen_protocols: ListenProtocolSettings,
|
||||
/// The client authenticator.
|
||||
///
|
||||
@@ -121,13 +123,25 @@ pub struct Settings {
|
||||
/// ...
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing)]
|
||||
#[serde(rename(deserialize = "credentials_file"))]
|
||||
#[serde(deserialize_with = "deserialize_authenticator")]
|
||||
pub(crate) authenticator: Option<Arc<dyn Authenticator>>,
|
||||
/// The reverse proxy settings.
|
||||
/// With this one set up the endpoint does TLS termination on such connections and
|
||||
/// translates HTTP/x traffic into HTTP/1.1 protocol towards the server and back
|
||||
/// into original HTTP/x towards the client. Like this:
|
||||
///
|
||||
/// ```(client) TLS(HTTP/x) <--(endpoint)--> (server) HTTP/1.1```
|
||||
///
|
||||
/// The translated HTTP/1.1 requests have the custom header `X-Original-Protocol`
|
||||
/// appended. For now, its value can be either `HTTP1`, or `HTTP3`.
|
||||
/// TLS hosts for the reverse proxy channel are configured through [`TlsHostsSettings`].
|
||||
pub(crate) reverse_proxy: Option<ReverseProxySettings>,
|
||||
/// The ICMP forwarding settings.
|
||||
/// Setting up this feature requires superuser rights on some systems.
|
||||
pub(crate) icmp: Option<IcmpSettings>,
|
||||
/// The metrics handling settings
|
||||
/// The metrics gathering request handler settings
|
||||
pub(crate) metrics: Option<MetricsSettings>,
|
||||
|
||||
/// Whether an instance was built through a [`SettingsBuilder`].
|
||||
@@ -138,7 +152,8 @@ pub struct Settings {
|
||||
built: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(RuntimeDoc))]
|
||||
pub struct TlsHostInfo {
|
||||
/// Used as a key for selecting a certificate chain in TLS handshake.
|
||||
/// MUST be unique.
|
||||
@@ -158,18 +173,31 @@ pub struct TlsHostInfo {
|
||||
pub private_key_path: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[cfg_attr(feature = "rt_doc", derive(RuntimeDoc))]
|
||||
pub struct TlsHostsSettings {
|
||||
/// See [`TlsSettingsBuilder::main_hosts`]
|
||||
/// Еhe main TLS hosts.
|
||||
/// Used for traffic tunneling and service requests handling.
|
||||
pub(crate) main_hosts: Vec<TlsHostInfo>,
|
||||
/// See [`TlsSettingsBuilder::ping_hosts`]
|
||||
/// The TLS hosts for HTTPS pinging.
|
||||
/// With this one set up the endpoint responds with `200 OK` to HTTPS `GET` requests
|
||||
/// to the specified domains.
|
||||
#[serde(default)]
|
||||
pub(crate) ping_hosts: Vec<TlsHostInfo>,
|
||||
/// See [`TlsSettingsBuilder::speedtest_hosts`]
|
||||
/// The TLS hosts for speed testing.
|
||||
/// With this one set up the endpoint accepts connections to the specified hosts and
|
||||
/// handles HTTP requests in the following way:
|
||||
/// * `GET` requests with `/Nmb.bin` path (where `N` is 1 to 100, e.g. `/100mb.bin`)
|
||||
/// are considered as download speedtest transferring `N` megabytes to a client
|
||||
/// * `POST` requests with `/upload.html` path and `Content-Length: N`
|
||||
/// are considered as upload speedtest receiving `N` bytes from a client,
|
||||
/// where `N` is up to 120 * 1024 * 1024 bytes
|
||||
#[serde(default)]
|
||||
pub(crate) speedtest_hosts: Vec<TlsHostInfo>,
|
||||
/// See [`TlsSettingsBuilder::reverse_proxy_hosts`]
|
||||
/// The TLS hosts for the connections must be forwarded to the reverse proxy
|
||||
/// (see [`Settings::reverse_proxy`]).
|
||||
/// Only makes sense if the reverse proxy is set up, otherwise it is ignored.
|
||||
#[serde(default)]
|
||||
pub(crate) reverse_proxy_hosts: Vec<TlsHostInfo>,
|
||||
|
||||
@@ -181,19 +209,26 @@ pub struct TlsHostsSettings {
|
||||
built: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(RuntimeDoc))]
|
||||
pub struct ReverseProxySettings {
|
||||
/// See [`ReverseProxySettingsBuilder::server_address`]
|
||||
/// The origin server address
|
||||
pub(crate) server_address: SocketAddr,
|
||||
/// See [`ReverseProxySettingsBuilder::path_mask`]
|
||||
/// Connections to [the main hosts](TlsHostsSettings.main_hosts) with
|
||||
/// paths starting with this mask are routed to the reverse proxy server.
|
||||
/// MUST start with slash.
|
||||
pub(crate) path_mask: String,
|
||||
/// See [`ReverseProxySettingsBuilder::h3_backward_compatibility`]
|
||||
/// With this one set to `true` the endpoint overrides the HTTP method while
|
||||
/// translating an HTTP3 request to HTTP1 in case the request has the `GET` method
|
||||
/// and its path is `/` or matches [`ReverseProxySettings.path_mask`]
|
||||
#[serde(default)]
|
||||
pub(crate) h3_backward_compatibility: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
/// The set of connection forwarder settings
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "rt_doc", derive(RuntimeDoc))]
|
||||
pub enum ForwardProtocolSettings {
|
||||
/// A direct forwarder routes a connection directly to its target host
|
||||
Direct(DirectForwarderSettings),
|
||||
@@ -201,15 +236,15 @@ pub enum ForwardProtocolSettings {
|
||||
Socks5(Socks5ForwarderSettings),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct DirectForwarderSettings {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct Socks5ForwarderSettings {
|
||||
/// The address of a proxy
|
||||
pub(crate) address: SocketAddr,
|
||||
/// The extended authentication flag.
|
||||
/// See [`Socks5ForwarderSettingsBuilder::extended_auth`] for details.
|
||||
/// Whether the extended authentication is enabled
|
||||
#[serde(default)]
|
||||
pub(crate) extended_auth: bool,
|
||||
}
|
||||
@@ -218,7 +253,9 @@ pub struct Socks5ForwarderSettingsBuilder {
|
||||
settings: Socks5ForwarderSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
/// The set of enabled client listener codecs
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(RuntimeDoc))]
|
||||
pub struct ListenProtocolSettings {
|
||||
/// HTTP/1.1 listener settings
|
||||
#[serde(default)]
|
||||
@@ -231,14 +268,18 @@ pub struct ListenProtocolSettings {
|
||||
pub quic: Option<QuicSettings>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
/// The ICMP forwarding settings.
|
||||
/// Setting up this feature requires superuser rights on some systems.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct IcmpSettings {
|
||||
/// The name of an interface to bind the ICMP socket to
|
||||
/// The name of a network interface to bind the outbound ICMP socket to
|
||||
#[serde(default = "IcmpSettings::default_interface_name")]
|
||||
pub(crate) interface_name: String,
|
||||
/// Timeout of tunneled ICMP requests
|
||||
#[serde(default = "IcmpSettings::default_request_timeout")]
|
||||
#[serde(rename(deserialize = "request_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "request_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) request_timeout: Duration,
|
||||
/// The capacity of the ICMP multiplexer received messages queue.
|
||||
/// Decreasing it may cause packet dropping in case the multiplexer cannot keep up the pace.
|
||||
@@ -248,26 +289,32 @@ pub struct IcmpSettings {
|
||||
pub(crate) recv_message_queue_capacity: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
/// The metrics gathering request handler settings
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct MetricsSettings {
|
||||
/// The address to listen on for settings export requests
|
||||
#[serde(default = "MetricsSettings::default_listen_address")]
|
||||
pub(crate) address: SocketAddr,
|
||||
/// Timeout of a metrics request
|
||||
#[serde(default = "MetricsSettings::default_request_timeout")]
|
||||
#[serde(rename(deserialize = "request_timeout_secs"))]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs")]
|
||||
#[serde(rename = "request_timeout_secs")]
|
||||
#[serde(deserialize_with = "deserialize_duration_secs", serialize_with = "serialize_duration_secs")]
|
||||
pub(crate) request_timeout: Duration,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
/// The set of HTTP/1.1 listener codec settings
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct Http1Settings {
|
||||
/// Buffer size for outgoing traffic
|
||||
#[serde(default = "Http1Settings::default_upload_buffer_size")]
|
||||
pub(crate) upload_buffer_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
/// The set of HTTP/2 listener codec settings
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct Http2Settings {
|
||||
/// The initial window size (in octets) for connection-level flow control for received data
|
||||
#[serde(default = "Http2Settings::default_initial_connection_window_size")]
|
||||
@@ -286,7 +333,9 @@ pub struct Http2Settings {
|
||||
pub(crate) header_table_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
/// The set of QUIC listener codec settings
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "rt_doc", derive(Getter, RuntimeDoc))]
|
||||
pub struct QuicSettings {
|
||||
/// The size of UDP payloads that the endpoint is willing to receive. UDP datagrams with
|
||||
/// payloads larger than this limit are not likely to be processed.
|
||||
@@ -393,35 +442,35 @@ impl Settings {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_listen_address() -> SocketAddr {
|
||||
pub fn default_listen_address() -> SocketAddr {
|
||||
SocketAddr::from((Ipv4Addr::UNSPECIFIED, 443))
|
||||
}
|
||||
|
||||
fn default_ipv6_available() -> bool {
|
||||
pub fn default_ipv6_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_allow_private_network_connections() -> bool {
|
||||
pub fn default_allow_private_network_connections() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn default_tls_handshake_timeout() -> Duration {
|
||||
pub fn default_tls_handshake_timeout() -> Duration {
|
||||
Duration::from_secs(10)
|
||||
}
|
||||
|
||||
fn default_client_listener_timeout() -> Duration {
|
||||
pub fn default_client_listener_timeout() -> Duration {
|
||||
Duration::from_secs(10 * 60)
|
||||
}
|
||||
|
||||
fn default_connection_establishment_timeout() -> Duration {
|
||||
pub fn default_connection_establishment_timeout() -> Duration {
|
||||
Duration::from_secs(30)
|
||||
}
|
||||
|
||||
fn default_tcp_connections_timeout() -> Duration {
|
||||
pub fn default_tcp_connections_timeout() -> Duration {
|
||||
Duration::from_secs(604800) // 1 week (match client tcpip module)
|
||||
}
|
||||
|
||||
fn default_udp_connections_timeout() -> Duration {
|
||||
pub fn default_udp_connections_timeout() -> Duration {
|
||||
Duration::from_secs(300) // 5 minutes (match client tcpip module)
|
||||
}
|
||||
}
|
||||
@@ -431,7 +480,6 @@ impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen_address: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)),
|
||||
reverse_proxy: None,
|
||||
ipv6_available: false,
|
||||
allow_private_network_connections: true,
|
||||
tls_handshake_timeout: Settings::default_tls_handshake_timeout(),
|
||||
@@ -446,6 +494,7 @@ impl Default for Settings {
|
||||
quic: Some(QuicSettings::builder().build()),
|
||||
},
|
||||
authenticator: None,
|
||||
reverse_proxy: None,
|
||||
icmp: None,
|
||||
metrics: Default::default(),
|
||||
built: false,
|
||||
@@ -514,7 +563,7 @@ impl Http1Settings {
|
||||
Http1SettingsBuilder::new()
|
||||
}
|
||||
|
||||
fn default_upload_buffer_size() -> usize {
|
||||
pub fn default_upload_buffer_size() -> usize {
|
||||
32 * 1024
|
||||
}
|
||||
}
|
||||
@@ -524,23 +573,23 @@ impl Http2Settings {
|
||||
Http2SettingsBuilder::new()
|
||||
}
|
||||
|
||||
fn default_initial_connection_window_size() -> u32 {
|
||||
pub fn default_initial_connection_window_size() -> u32 {
|
||||
8 * 1024 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_stream_window_size() -> u32 {
|
||||
pub fn default_initial_stream_window_size() -> u32 {
|
||||
128 * 1024 // Chrome constant
|
||||
}
|
||||
|
||||
fn default_max_concurrent_streams() -> u32 {
|
||||
pub fn default_max_concurrent_streams() -> u32 {
|
||||
1000 // Chrome constant
|
||||
}
|
||||
|
||||
fn default_max_frame_size() -> u32 {
|
||||
pub fn default_max_frame_size() -> u32 {
|
||||
1 << 14 // Firefox constant
|
||||
}
|
||||
|
||||
fn default_header_table_size() -> u32 {
|
||||
pub fn default_header_table_size() -> u32 {
|
||||
65536
|
||||
}
|
||||
}
|
||||
@@ -550,55 +599,55 @@ impl QuicSettings {
|
||||
QuicSettingsBuilder::new()
|
||||
}
|
||||
|
||||
fn default_recv_udp_payload_size() -> usize {
|
||||
pub fn default_recv_udp_payload_size() -> usize {
|
||||
1350
|
||||
}
|
||||
|
||||
fn default_send_udp_payload_size() -> usize {
|
||||
pub fn default_send_udp_payload_size() -> usize {
|
||||
1350
|
||||
}
|
||||
|
||||
fn default_initial_max_data() -> u64 {
|
||||
pub fn default_initial_max_data() -> u64 {
|
||||
100 * 1024 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_max_stream_data_bidi_local() -> u64 {
|
||||
pub fn default_initial_max_stream_data_bidi_local() -> u64 {
|
||||
1024 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_max_stream_data_bidi_remote() -> u64 {
|
||||
pub fn default_initial_max_stream_data_bidi_remote() -> u64 {
|
||||
1024 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_max_stream_data_uni() -> u64 {
|
||||
pub fn default_initial_max_stream_data_uni() -> u64 {
|
||||
1024 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_max_streams_bidi() -> u64 {
|
||||
pub fn default_initial_max_streams_bidi() -> u64 {
|
||||
4 * 1024
|
||||
}
|
||||
|
||||
fn default_initial_max_streams_uni() -> u64 {
|
||||
pub fn default_initial_max_streams_uni() -> u64 {
|
||||
4 * 1024
|
||||
}
|
||||
|
||||
fn default_max_connection_window() -> u64 {
|
||||
pub fn default_max_connection_window() -> u64 {
|
||||
24 * 1024 * 1024
|
||||
}
|
||||
|
||||
fn default_max_stream_window() -> u64 {
|
||||
pub fn default_max_stream_window() -> u64 {
|
||||
16 * 1024 * 1024
|
||||
}
|
||||
|
||||
fn default_disable_active_migration() -> bool {
|
||||
pub fn default_disable_active_migration() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_enable_early_data() -> bool {
|
||||
pub fn default_enable_early_data() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_message_queue_capacity() -> usize {
|
||||
pub fn default_message_queue_capacity() -> usize {
|
||||
4 * 1024
|
||||
}
|
||||
}
|
||||
@@ -626,11 +675,19 @@ impl IcmpSettings {
|
||||
IcmpSettingsBuilder::new()
|
||||
}
|
||||
|
||||
fn default_request_timeout() -> Duration {
|
||||
pub fn default_interface_name() -> String {
|
||||
if cfg!(target_os = "linux") {
|
||||
"eth0"
|
||||
} else {
|
||||
"en0"
|
||||
}.into()
|
||||
}
|
||||
|
||||
pub fn default_request_timeout() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
|
||||
fn default_message_queue_capacity() -> usize {
|
||||
pub fn default_message_queue_capacity() -> usize {
|
||||
256
|
||||
}
|
||||
}
|
||||
@@ -640,11 +697,11 @@ impl MetricsSettings {
|
||||
MetricsSettingsBuilder::new()
|
||||
}
|
||||
|
||||
fn default_listen_address() -> SocketAddr {
|
||||
pub fn default_listen_address() -> SocketAddr {
|
||||
(Ipv4Addr::UNSPECIFIED, 1987).into()
|
||||
}
|
||||
|
||||
fn default_request_timeout() -> Duration {
|
||||
pub fn default_request_timeout() -> Duration {
|
||||
Duration::from_secs(3)
|
||||
}
|
||||
}
|
||||
@@ -663,7 +720,6 @@ impl SettingsBuilder {
|
||||
Self {
|
||||
settings: Settings {
|
||||
listen_address: Settings::default_listen_address(),
|
||||
reverse_proxy: None,
|
||||
ipv6_available: Settings::default_ipv6_available(),
|
||||
allow_private_network_connections: Settings::default_allow_private_network_connections(),
|
||||
tls_handshake_timeout: Settings::default_tls_handshake_timeout(),
|
||||
@@ -674,6 +730,7 @@ impl SettingsBuilder {
|
||||
forward_protocol: Default::default(),
|
||||
listen_protocols: Default::default(),
|
||||
authenticator: None,
|
||||
reverse_proxy: None,
|
||||
icmp: None,
|
||||
metrics: Default::default(),
|
||||
built: true,
|
||||
@@ -735,6 +792,13 @@ impl SettingsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set timeout of outgoing connection establishment.
|
||||
/// For example, it is related to client's connection requests.
|
||||
pub fn connection_establishment_timeout(mut self, v: Duration) -> Self {
|
||||
self.settings.connection_establishment_timeout = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set timeout of tunneled TCP connections
|
||||
pub fn tcp_connections_timeout(mut self, v: Duration) -> Self {
|
||||
self.settings.tcp_connections_timeout = v;
|
||||
@@ -770,6 +834,12 @@ impl SettingsBuilder {
|
||||
self.settings.icmp = Some(x);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the metrics request listener settings
|
||||
pub fn metrics(mut self, x: MetricsSettings) -> Self {
|
||||
self.settings.metrics = Some(x);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TlsSettingsBuilder {
|
||||
@@ -791,7 +861,8 @@ impl TlsSettingsBuilder {
|
||||
Ok(self.settings)
|
||||
}
|
||||
|
||||
/// Set the main TLS hosts
|
||||
/// Set the main TLS hosts.
|
||||
/// Used for traffic tunneling and service requests handling.
|
||||
pub fn main_hosts(mut self, hosts: Vec<TlsHostInfo>) -> Self {
|
||||
self.settings.main_hosts = hosts;
|
||||
self
|
||||
@@ -1175,6 +1246,13 @@ fn deserialize_duration_secs<'de, D>(deserializer: D) -> Result<Duration, D::Err
|
||||
Ok(Duration::from_secs(x))
|
||||
}
|
||||
|
||||
fn serialize_duration_secs<S>(x: &Duration, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
serializer.serialize_u64(x.as_secs())
|
||||
}
|
||||
|
||||
fn deserialize_file_path<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
|
||||
@@ -118,6 +118,7 @@ impl TlsDemux {
|
||||
cert_chain: if cfg!(test) {
|
||||
Default::default()
|
||||
} else {
|
||||
// @todo: check if cert is expired?
|
||||
utils::load_certs(&x.cert_chain_path)?
|
||||
},
|
||||
key: if cfg!(test) {
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn hex_dump_uppercase(buf: &[u8]) -> String {
|
||||
}
|
||||
|
||||
/// Can hold either of the options
|
||||
pub(crate) enum Either<L, R> {
|
||||
pub enum Either<L, R> {
|
||||
Left(L),
|
||||
Right(R),
|
||||
}
|
||||
@@ -71,16 +71,55 @@ impl<L, R> Either<L, R> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_certs(filename: &str) -> io::Result<Vec<Certificate>> {
|
||||
pub fn load_certs(filename: &str) -> io::Result<Vec<Certificate>> {
|
||||
certs(&mut BufReader::new(File::open(filename)?))
|
||||
.map_err(|e| io::Error::new(
|
||||
ErrorKind::InvalidInput, format!("Invalid cert: {}", e)))
|
||||
.map(|mut certs| certs.drain(..).map(Certificate).collect())
|
||||
}
|
||||
|
||||
pub(crate) fn load_private_key(filename: &str) -> io::Result<PrivateKey> {
|
||||
pub fn load_private_key(filename: &str) -> io::Result<PrivateKey> {
|
||||
pkcs8_private_keys(&mut BufReader::new(File::open(filename)?))
|
||||
.map_err(|e| io::Error::new(
|
||||
ErrorKind::InvalidInput, format!("Invalid key: {}", e)))
|
||||
.map(|mut keys| PrivateKey(keys.remove(0)))
|
||||
.and_then(|keys| keys.first().cloned()
|
||||
.ok_or_else(|| io::Error::new(ErrorKind::Other, "No keys found")))
|
||||
.map(PrivateKey)
|
||||
}
|
||||
|
||||
pub trait IterJoin {
|
||||
type Output;
|
||||
|
||||
/// Like [`Iterator::fold`] but drops the trailing separator
|
||||
fn join(self, sep: impl AsRef<str>) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<I, T> IterJoin for I
|
||||
where
|
||||
I: Iterator<Item=T>,
|
||||
T: AsRef<str>,
|
||||
{
|
||||
type Output = String;
|
||||
|
||||
fn join(self, sep: impl AsRef<str>) -> Self::Output {
|
||||
let mut ret = self
|
||||
.fold(String::new(), |acc, x| acc + x.as_ref() + sep.as_ref());
|
||||
if ret.len() > sep.as_ref().len() {
|
||||
ret.replace_range((ret.len() - sep.as_ref().len()).., "");
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::IterJoin;
|
||||
|
||||
#[test]
|
||||
fn iter_join() {
|
||||
assert_eq!("a.b.c", ["a", "b", "c"].iter().join("."));
|
||||
assert_eq!("a", std::iter::once("a").join("x"));
|
||||
assert_eq!("", std::iter::empty::<&str>().join("x"));
|
||||
}
|
||||
}
|
||||
|
||||
12
macros/Cargo.toml
Normal file
12
macros/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "=1.0.60"
|
||||
quote = "=1.0.28"
|
||||
syn = "=1.0.109"
|
||||
36
macros/src/getter.rs
Normal file
36
macros/src/getter.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{Data, DataStruct, DeriveInput, Fields};
|
||||
|
||||
pub(crate) fn derive(input: TokenStream) -> TokenStream {
|
||||
let ast: DeriveInput = syn::parse(input).unwrap();
|
||||
|
||||
let fields = match &ast.data {
|
||||
Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => {
|
||||
fields.named.iter()
|
||||
.filter_map(|field| field.ident.as_ref()
|
||||
.zip(Some(&field.ty)))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
_ => panic!("`Getter` has to be used only with structs"),
|
||||
};
|
||||
|
||||
let funcs = fields.into_iter()
|
||||
.fold(quote!(), |stream, (name, ty)| {
|
||||
let fn_name = format_ident!("get_{name}");
|
||||
quote! {
|
||||
#stream
|
||||
pub fn #fn_name(&self) -> &#ty {
|
||||
&self.#name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let name = format_ident!("{}", ast.ident);
|
||||
let gen = quote! {
|
||||
impl #name {
|
||||
#funcs
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
82
macros/src/lib.rs
Normal file
82
macros/src/lib.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
mod getter;
|
||||
mod rt_doc;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// Collect docs for each identifier of the struct or enum and generate
|
||||
/// static methods to get the docs in runtime.
|
||||
///
|
||||
/// ```
|
||||
/// use macros::RuntimeDoc;
|
||||
///
|
||||
/// /// Trololo
|
||||
/// #[derive(RuntimeDoc)]
|
||||
/// enum Foo1 {
|
||||
/// /// Haha
|
||||
/// Bar,
|
||||
/// /// Hehe
|
||||
/// Baz,
|
||||
/// }
|
||||
///
|
||||
/// // Is equivalent to
|
||||
/// /// Trololo
|
||||
/// enum Foo2 {
|
||||
/// /// Haha
|
||||
/// Bar,
|
||||
/// /// Hehe
|
||||
/// Baz,
|
||||
/// }
|
||||
///
|
||||
/// impl Foo2 {
|
||||
/// pub fn doc() -> &'static str {
|
||||
/// "Trololo"
|
||||
/// }
|
||||
/// pub fn doc_bar() -> &'static str {
|
||||
/// "Haha"
|
||||
/// }
|
||||
/// pub fn doc_baz() -> &'static str {
|
||||
/// "Hehe"
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(Foo1::doc(), "Trololo");
|
||||
/// assert_eq!(Foo1::doc_bar(), "Haha");
|
||||
/// assert_eq!(Foo1::doc_baz(), "Hehe");
|
||||
/// ```
|
||||
#[proc_macro_derive(RuntimeDoc)]
|
||||
pub fn parse_rt_doc(input: TokenStream) -> TokenStream {
|
||||
rt_doc::derive(input)
|
||||
}
|
||||
|
||||
/// Generate getters for each field of the struct.
|
||||
///
|
||||
/// ```
|
||||
/// use macros::Getter;
|
||||
///
|
||||
/// #[derive(Getter)]
|
||||
/// struct Foo1 {
|
||||
/// x: usize,
|
||||
/// y: String,
|
||||
/// }
|
||||
///
|
||||
/// // Is equivalent to
|
||||
/// struct Foo2 {
|
||||
/// x: usize,
|
||||
/// y: String,
|
||||
/// }
|
||||
///
|
||||
/// impl Foo2 {
|
||||
/// pub fn get_x(&self) -> &usize {
|
||||
/// &self.x
|
||||
/// }
|
||||
/// pub fn get_y(&self) -> &String {
|
||||
/// &self.y
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(Foo1 { x: 42, y: Default::default() }.get_x(), &42);
|
||||
/// ```
|
||||
#[proc_macro_derive(Getter)]
|
||||
pub fn parse_getter(input: TokenStream) -> TokenStream {
|
||||
getter::derive(input)
|
||||
}
|
||||
78
macros/src/rt_doc.rs
Normal file
78
macros/src/rt_doc.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use proc_macro::TokenStream;
|
||||
use std::iter;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{Attribute, Data, DataEnum, DataStruct, DeriveInput, Fields, Lit, Meta, MetaNameValue};
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
const OS_LINE_ENDING: &str = "\n";
|
||||
#[cfg(target_family = "windows")]
|
||||
const OS_LINE_ENDING: &str = "\r\n";
|
||||
|
||||
pub(crate) fn derive(input: TokenStream) -> TokenStream {
|
||||
let ast: DeriveInput = syn::parse(input).unwrap();
|
||||
|
||||
let docs = match &ast.data {
|
||||
Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => {
|
||||
fields.named.iter()
|
||||
.filter_map(|field| field.ident.clone()
|
||||
.zip(Some(collect_docs(field.attrs.iter()))))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
Data::Struct(_) => Default::default(),
|
||||
Data::Enum(DataEnum { variants, .. }) => {
|
||||
variants.iter()
|
||||
.map(|variant| (
|
||||
format_ident!("{}", variant.ident.to_string().to_lowercase()),
|
||||
collect_docs(variant.attrs.iter())
|
||||
))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
_ => panic!("`RuntimeDoc` has to be used only with structs or enums"),
|
||||
};
|
||||
|
||||
let funcs = iter::once((None, collect_docs(ast.attrs.iter())))
|
||||
.chain(docs.into_iter().map(|(ident, doc)| (Some(ident), doc)))
|
||||
.fold(quote!(), |stream, (name, doc)| {
|
||||
if doc.is_empty() {
|
||||
return stream;
|
||||
}
|
||||
|
||||
let name = match name {
|
||||
Some(x) => format_ident!("doc_{x}"),
|
||||
None => format_ident!("doc"),
|
||||
};
|
||||
quote! {
|
||||
#stream
|
||||
pub fn #name() -> &'static str {
|
||||
#doc
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let name = format_ident!("{}", ast.ident);
|
||||
let gen = quote! {
|
||||
impl #name {
|
||||
#funcs
|
||||
}
|
||||
};
|
||||
gen.into()
|
||||
}
|
||||
|
||||
fn collect_docs<'a, I>(attrs: I) -> String
|
||||
where I: Iterator<Item=&'a Attribute>
|
||||
{
|
||||
attrs
|
||||
.filter_map(|attr| attr.parse_meta().ok())
|
||||
.filter(|meta| meta.path().is_ident("doc"))
|
||||
.filter_map(|meta| match meta {
|
||||
Meta::NameValue(
|
||||
MetaNameValue {
|
||||
lit: Lit::Str(lit), ..
|
||||
}
|
||||
) => Some(lit.value().trim().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|doc| !doc.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(OS_LINE_ENDING)
|
||||
}
|
||||
11
macros/tests/getter.rs
Normal file
11
macros/tests/getter.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use macros::Getter;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
#[derive(Getter)]
|
||||
struct Foo {
|
||||
x: usize,
|
||||
}
|
||||
|
||||
assert_eq!(Foo { x: 42 }.get_x(), &42);
|
||||
}
|
||||
58
macros/tests/rt_doc_enum.rs
Normal file
58
macros/tests/rt_doc_enum.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use macros::RuntimeDoc;
|
||||
|
||||
#[test]
|
||||
fn slashed() {
|
||||
/// Trololo
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
enum Foo {
|
||||
/// Haha
|
||||
Bar,
|
||||
/// Hehe
|
||||
Baz,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), "Trololo");
|
||||
assert_eq!(Foo::doc_bar(), "Haha");
|
||||
assert_eq!(Foo::doc_baz(), "Hehe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_attr() {
|
||||
#[doc = "Trololo"]
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
enum Foo {
|
||||
#[doc = "Haha"]
|
||||
Bar,
|
||||
#[doc = "Hehe"]
|
||||
Baz,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), "Trololo");
|
||||
assert_eq!(Foo::doc_bar(), "Haha");
|
||||
assert_eq!(Foo::doc_baz(), "Hehe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed() {
|
||||
/// - How much watch?
|
||||
#[doc = "- Six watch"]
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
enum Foo {
|
||||
/// - Such much?
|
||||
#[doc = "- For whom how"]
|
||||
Bar,
|
||||
/// - MGIMO finished?
|
||||
#[doc = "- Ask"]
|
||||
Baz,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), r#"- How much watch?
|
||||
- Six watch"#);
|
||||
assert_eq!(Foo::doc_bar(), r#"- Such much?
|
||||
- For whom how"#);
|
||||
assert_eq!(Foo::doc_baz(), r#"- MGIMO finished?
|
||||
- Ask"#);
|
||||
}
|
||||
47
macros/tests/rt_doc_struct.rs
Normal file
47
macros/tests/rt_doc_struct.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use macros::RuntimeDoc;
|
||||
|
||||
#[test]
|
||||
fn slashed() {
|
||||
/// Trololo
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
struct Foo {
|
||||
/// Haha
|
||||
pub x: u32,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), "Trololo");
|
||||
assert_eq!(Foo::doc_x(), "Haha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_attr() {
|
||||
#[doc = "Trololo"]
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
struct Foo {
|
||||
#[doc = "Haha"]
|
||||
pub x: u32,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), "Trololo");
|
||||
assert_eq!(Foo::doc_x(), "Haha");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed() {
|
||||
/// - How much watch?
|
||||
#[doc = "- Six watch"]
|
||||
#[allow(dead_code)]
|
||||
#[derive(RuntimeDoc)]
|
||||
struct Foo {
|
||||
/// - Such much?
|
||||
#[doc = "- For whom how"]
|
||||
pub x: u32,
|
||||
}
|
||||
|
||||
assert_eq!(Foo::doc(), r#"- How much watch?
|
||||
- Six watch"#);
|
||||
assert_eq!(Foo::doc_x(), r#"- Such much?
|
||||
- For whom how"#);
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
MANIFEST_FILE=$2
|
||||
|
||||
increment_version() {
|
||||
major=${1%%.*}
|
||||
minor=$(echo ${1#*.} | sed -e "s/\.[0-9]*//")
|
||||
@@ -9,7 +11,7 @@ increment_version() {
|
||||
echo ${major}.${minor}.$((revision+1))
|
||||
}
|
||||
|
||||
VERSION=$(cat Cargo.toml | grep "version = " | head -n 1 | sed -e 's/version = "\(.*\)"/\1/')
|
||||
VERSION=$(cat "$MANIFEST_FILE" | grep "version = " | head -n 1 | sed -e 's/version = "\(.*\)"/\1/')
|
||||
|
||||
argument_version=$1
|
||||
if [ -z "$argument_version" ]
|
||||
@@ -30,7 +32,7 @@ echo "New version is ${NEW_VERSION}"
|
||||
|
||||
set -x
|
||||
|
||||
sed -i -e "s/^version = \"${VERSION}\"$/version = \"${NEW_VERSION}\"/" Cargo.toml
|
||||
sed -i -e "s/^version = \"${VERSION}\"$/version = \"${NEW_VERSION}\"/" "$MANIFEST_FILE"
|
||||
|
||||
# Update changelog
|
||||
sed -i -e "3{
|
||||
|
||||
22
tools/Cargo.toml
Normal file
22
tools/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "vpn_endpoint_tools"
|
||||
version = "0.1.0"
|
||||
authors = ["Sergei Gunchenko <s.gunchenko@adguard.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "setup_wizard"
|
||||
path = "setup_wizard/main.rs"
|
||||
|
||||
[dependencies]
|
||||
chrono = "=0.4.26"
|
||||
clap = "=4.3.8"
|
||||
dialoguer = "=0.10.4"
|
||||
lazy_static = "=1.4.0"
|
||||
once_cell = "=1.18.0"
|
||||
rcgen = "=0.10.0"
|
||||
serde = "=1.0.164"
|
||||
toml = "=0.7.4"
|
||||
toml_edit = "=0.19.10"
|
||||
vpn_libs_endpoint = { version = "0.1", path = "../lib", features = ["rt_doc"] }
|
||||
x509-parser = "=0.15.0"
|
||||
143
tools/setup_wizard/composer.rs
Normal file
143
tools/setup_wizard/composer.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::iter::once;
|
||||
use toml_edit::{Document, value};
|
||||
use vpn_libs_endpoint::settings::{ForwardProtocolSettings, Http1Settings, Http2Settings, IcmpSettings, ListenProtocolSettings, MetricsSettings, QuicSettings, Settings};
|
||||
use vpn_libs_endpoint::utils::IterJoin;
|
||||
use crate::template_settings;
|
||||
use crate::template_settings::ToTomlComment;
|
||||
|
||||
pub fn compose_document(settings: &Settings, credentials_path: &str) -> String {
|
||||
once(compose_main_table(settings, credentials_path))
|
||||
.chain(once(compose_forward_protocol_table(settings.get_forward_protocol())))
|
||||
.chain(once(compose_listener_protocol_table(settings.get_listen_protocols())))
|
||||
.chain(once(compose_icmp_table(settings.get_icmp().as_ref())))
|
||||
.chain(once(compose_metrics_table(settings.get_metrics().as_ref())))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn compose_main_table(settings: &Settings, credentials_path: &str) -> String {
|
||||
let mut doc: Document = template_settings::MAIN_TABLE.parse().unwrap();
|
||||
|
||||
doc["listen_address"] = value(settings.get_listen_address().to_string());
|
||||
doc["credentials_file"] = value(credentials_path);
|
||||
doc["ipv6_available"] = value(*settings.get_ipv6_available());
|
||||
doc["allow_private_network_connections"] = value(*settings.get_allow_private_network_connections());
|
||||
doc["tls_handshake_timeout_secs"] = value(settings.get_tls_handshake_timeout().as_secs() as i64);
|
||||
doc["client_listener_timeout_secs"] = value(settings.get_client_listener_timeout().as_secs() as i64);
|
||||
doc["connection_establishment_timeout_secs"] = value(settings.get_connection_establishment_timeout().as_secs() as i64);
|
||||
doc["tcp_connections_timeout_secs"] = value(settings.get_tcp_connections_timeout().as_secs() as i64);
|
||||
doc["udp_connections_timeout_secs"] = value(settings.get_udp_connections_timeout().as_secs() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
|
||||
fn compose_forward_protocol_table(settings: &ForwardProtocolSettings) -> String {
|
||||
let spec = match settings {
|
||||
ForwardProtocolSettings::Direct(_) => template_settings::DIRECT_FORWARDER_TABLE.clone(),
|
||||
ForwardProtocolSettings::Socks5(x) => {
|
||||
let mut doc: Document = template_settings::SOCKS_FORWARDER_TABLE.parse().unwrap();
|
||||
let table = doc["forward_protocol"]["socks5"].as_table_mut().unwrap();
|
||||
table["address"] = value(x.get_address().to_string());
|
||||
table["extended_auth"] = value(*x.get_extended_auth());
|
||||
doc.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
format!("{}\n{}", *template_settings::FORWARD_PROTOCOL_COMMON_TABLE, spec)
|
||||
}
|
||||
|
||||
fn compose_listener_protocol_table(settings: &ListenProtocolSettings) -> String {
|
||||
once(template_settings::LISTENER_COMMON_TABLE.clone())
|
||||
.chain(once(compose_http1_listener_table(settings.http1.as_ref())))
|
||||
.chain(once(compose_http2_listener_table(settings.http2.as_ref())))
|
||||
.chain(once(compose_quic_listener_table(settings.quic.as_ref())))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn compose_http1_listener_table(settings: Option<&Http1Settings>) -> String {
|
||||
match settings {
|
||||
Some(x) => {
|
||||
let mut doc: Document = template_settings::HTTP1_LISTENER_TABLE.parse().unwrap();
|
||||
let table = doc["listen_protocols"]["http1"].as_table_mut().unwrap();
|
||||
|
||||
table["upload_buffer_size"] = value(*x.get_upload_buffer_size() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
None => template_settings::HTTP1_LISTENER_TABLE.to_toml_comment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_http2_listener_table(settings: Option<&Http2Settings>) -> String {
|
||||
match settings {
|
||||
Some(x) => {
|
||||
let mut doc: Document = template_settings::HTTP2_LISTENER_TABLE.parse().unwrap();
|
||||
let table = doc["listen_protocols"]["http2"].as_table_mut().unwrap();
|
||||
|
||||
table["initial_connection_window_size"] = value(*x.get_initial_connection_window_size() as i64);
|
||||
table["initial_stream_window_size"] = value(*x.get_initial_stream_window_size() as i64);
|
||||
table["max_concurrent_streams"] = value(*x.get_max_concurrent_streams() as i64);
|
||||
table["max_frame_size"] = value(*x.get_max_frame_size() as i64);
|
||||
table["header_table_size"] = value(*x.get_header_table_size() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
None => template_settings::HTTP2_LISTENER_TABLE.to_toml_comment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_quic_listener_table(settings: Option<&QuicSettings>) -> String {
|
||||
match settings {
|
||||
Some(x) => {
|
||||
let mut doc: Document = template_settings::QUIC_LISTENER_TABLE.parse().unwrap();
|
||||
let table = doc["listen_protocols"]["quic"].as_table_mut().unwrap();
|
||||
|
||||
table["recv_udp_payload_size"] = value(*x.get_recv_udp_payload_size() as i64);
|
||||
table["send_udp_payload_size"] = value(*x.get_send_udp_payload_size() as i64);
|
||||
table["initial_max_data"] = value(*x.get_initial_max_data() as i64);
|
||||
table["max_stream_data_bidi_local"] = value(*x.get_initial_max_stream_data_bidi_local() as i64);
|
||||
table["max_stream_data_bidi_remote"] = value(*x.get_initial_max_stream_data_bidi_remote() as i64);
|
||||
table["max_stream_data_uni"] = value(*x.get_initial_max_stream_data_uni() as i64);
|
||||
table["max_streams_bidi"] = value(*x.get_initial_max_streams_bidi() as i64);
|
||||
table["max_streams_uni"] = value(*x.get_initial_max_streams_uni() as i64);
|
||||
table["max_connection_window"] = value(*x.get_max_connection_window() as i64);
|
||||
table["max_stream_window"] = value(*x.get_max_stream_window() as i64);
|
||||
table["disable_active_migration"] = value(*x.get_disable_active_migration());
|
||||
table["enable_early_data"] = value(*x.get_enable_early_data());
|
||||
table["message_queue_capacity"] = value(*x.get_message_queue_capacity() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
None => template_settings::QUIC_LISTENER_TABLE.to_toml_comment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_icmp_table(settings: Option<&IcmpSettings>) -> String {
|
||||
match settings {
|
||||
Some(x) => {
|
||||
let mut doc: Document = template_settings::ICMP_TABLE.parse().unwrap();
|
||||
let table = doc["icmp"].as_table_mut().unwrap();
|
||||
|
||||
table["interface_name"] = value(x.get_interface_name());
|
||||
table["request_timeout_secs"] = value(x.get_request_timeout().as_secs() as i64);
|
||||
table["recv_message_queue_capacity"] = value(*x.get_recv_message_queue_capacity() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
None => template_settings::ICMP_TABLE.to_toml_comment(),
|
||||
}
|
||||
}
|
||||
|
||||
fn compose_metrics_table(settings: Option<&MetricsSettings>) -> String {
|
||||
match settings {
|
||||
Some(x) => {
|
||||
let mut doc: Document = template_settings::METRICS_TABLE.parse().unwrap();
|
||||
let table = doc["metrics"].as_table_mut().unwrap();
|
||||
|
||||
table["address"] = value(x.get_address().to_string());
|
||||
table["request_timeout_secs"] = value(x.get_request_timeout().as_secs() as i64);
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
None => template_settings::METRICS_TABLE.to_toml_comment(),
|
||||
}
|
||||
}
|
||||
107
tools/setup_wizard/library_settings.rs
Normal file
107
tools/setup_wizard/library_settings.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::fs;
|
||||
use toml_edit::{ArrayOfTables, Item, Key, Table};
|
||||
use vpn_libs_endpoint::settings::{Http1Settings, Http2Settings, ListenProtocolSettings, QuicSettings, Settings};
|
||||
use crate::Mode;
|
||||
use crate::user_interaction::{ask_for_agreement, ask_for_input, ask_for_password, checked_overwrite, select_variant};
|
||||
|
||||
pub const DEFAULT_CREDENTIALS_PATH: &str = "credentials.toml";
|
||||
|
||||
pub struct Built {
|
||||
pub settings: Settings,
|
||||
pub credentials_path: String,
|
||||
}
|
||||
|
||||
pub fn build() -> Built {
|
||||
let builder = Settings::builder()
|
||||
.listen_address(ask_for_input(
|
||||
Settings::doc_listen_address(),
|
||||
Some(crate::get_predefined_params().listen_address.clone()
|
||||
.unwrap_or(Settings::default_listen_address().to_string())),
|
||||
)).unwrap();
|
||||
|
||||
Built {
|
||||
settings: builder
|
||||
.listen_protocols(ListenProtocolSettings {
|
||||
http1: Some(Http1Settings::builder().build()),
|
||||
http2: Some(Http2Settings::builder().build()),
|
||||
quic: Some(QuicSettings::builder().build()),
|
||||
})
|
||||
.build().expect("Couldn't build the library settings"),
|
||||
credentials_path: build_authenticator(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_authenticator() -> String {
|
||||
let path = if crate::get_mode() != Mode::NonInteractive
|
||||
&& check_file_exists(".", DEFAULT_CREDENTIALS_PATH)
|
||||
&& ask_for_agreement(&format!("Reuse the existing credentials file: {DEFAULT_CREDENTIALS_PATH}?"))
|
||||
{
|
||||
DEFAULT_CREDENTIALS_PATH.into()
|
||||
} else {
|
||||
let path = ask_for_input::<String>(
|
||||
"Path to the credentials file",
|
||||
Some(DEFAULT_CREDENTIALS_PATH.into()),
|
||||
);
|
||||
|
||||
if checked_overwrite(&path, "Overwrite the existing credentials file?") {
|
||||
println!("Let's create user credentials");
|
||||
let users = build_user_list();
|
||||
fs::write(&path, compose_credentials_content(users.into_iter()))
|
||||
.expect("Couldn't write the credentials into a file");
|
||||
println!("The user credentials are written to file: {}", path);
|
||||
}
|
||||
|
||||
path
|
||||
};
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
fn build_user_list() -> Vec<(String, String)> {
|
||||
if let Some(x) = crate::get_predefined_params().credentials.clone() {
|
||||
return vec![x];
|
||||
}
|
||||
|
||||
let mut list = vec![(
|
||||
ask_for_input::<String>("Username", None),
|
||||
ask_for_password("Password"),
|
||||
)];
|
||||
|
||||
loop {
|
||||
if "no" == select_variant("Add one more user?", &["yes", "no"], Some(1)) {
|
||||
break;
|
||||
}
|
||||
|
||||
list.push((
|
||||
ask_for_input::<String>("Username", None),
|
||||
ask_for_password("Password"),
|
||||
));
|
||||
}
|
||||
|
||||
list
|
||||
}
|
||||
|
||||
fn compose_credentials_content(clients: impl Iterator<Item=(String, String)>) -> String {
|
||||
let mut doc = toml_edit::Document::new();
|
||||
|
||||
let x = clients
|
||||
.map(|(u, p)| Table::from_iter(
|
||||
std::iter::once(("username", u))
|
||||
.chain(std::iter::once(("password", p)))
|
||||
))
|
||||
.collect::<ArrayOfTables>();
|
||||
|
||||
doc.insert_formatted(&Key::new("client"), Item::ArrayOfTables(x));
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
|
||||
fn check_file_exists(path: &str, name: &str) -> bool {
|
||||
match fs::read_dir(path) {
|
||||
Ok(x) => x.filter_map(Result::ok)
|
||||
.filter(|entry| entry.metadata()
|
||||
.map(|meta| meta.is_file()).unwrap_or_default())
|
||||
.any(|entry| Ok(name) == entry.file_name().into_string().as_ref().map(String::as_str)),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
188
tools/setup_wizard/main.rs
Normal file
188
tools/setup_wizard/main.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std::fs;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use vpn_libs_endpoint::settings::{Settings, TlsHostsSettings};
|
||||
use crate::user_interaction::{ask_for_agreement, ask_for_input, checked_overwrite};
|
||||
|
||||
mod composer;
|
||||
mod library_settings;
|
||||
mod template_settings;
|
||||
mod tls_hosts_settings;
|
||||
mod user_interaction;
|
||||
|
||||
const MODE_PARAM_NAME: &str = "mode";
|
||||
const MODE_NON_INTERACTIVE: &str = "non-interactive";
|
||||
const LISTEN_ADDRESS_PARAM_NAME: &str = "addr";
|
||||
const CREDENTIALS_PARAM_NAME: &str = "creds";
|
||||
const HOSTNAME_PARAM_NAME: &str = "host";
|
||||
const LIBRARY_SETTINGS_FILE_PARAM_NAME: &str = "lib_settings";
|
||||
const TLS_HOSTS_SETTINGS_FILE_PARAM_NAME: &str = "hosts_settings";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum Mode {
|
||||
NonInteractive,
|
||||
Interactive,
|
||||
}
|
||||
|
||||
static MODE: Mutex<Mode> = Mutex::new(Mode::Interactive);
|
||||
|
||||
pub fn get_mode() -> Mode {
|
||||
*MODE.lock().unwrap()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PredefinedParameters {
|
||||
listen_address: Option<String>,
|
||||
credentials: Option<(String, String)>,
|
||||
hostname: Option<String>,
|
||||
library_settings_file: Option<String>,
|
||||
tls_hosts_settings_file: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref PREDEFINED_PARAMS: Mutex<PredefinedParameters> = Mutex::default();
|
||||
}
|
||||
|
||||
pub fn get_predefined_params() -> MutexGuard<'static, PredefinedParameters> {
|
||||
PREDEFINED_PARAMS.lock().unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = clap::Command::new("VPN endpoint setup wizard")
|
||||
.args(&[
|
||||
clap::Arg::new(MODE_PARAM_NAME)
|
||||
.short('m')
|
||||
.long("mode")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(["interactive", MODE_NON_INTERACTIVE])
|
||||
.default_value("interactive")
|
||||
.help(r#"Available wizard running modes:
|
||||
* interactive - set up only the essential without deep diving into details
|
||||
* non-interactive - prepare the setup without interacting with a user,
|
||||
requires some parameters set up via command-line arguments
|
||||
"#),
|
||||
clap::Arg::new(LISTEN_ADDRESS_PARAM_NAME)
|
||||
.short('a')
|
||||
.long("address")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(MODE_PARAM_NAME, MODE_NON_INTERACTIVE)
|
||||
.help(Settings::doc_listen_address()),
|
||||
clap::Arg::new(CREDENTIALS_PARAM_NAME)
|
||||
.short('c')
|
||||
.long("creds")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(MODE_PARAM_NAME, MODE_NON_INTERACTIVE)
|
||||
.help(r#"A user credentials formatted as: <username>:<password>.
|
||||
Required in non-interactive mode."#),
|
||||
clap::Arg::new(HOSTNAME_PARAM_NAME)
|
||||
.short('n')
|
||||
.long("hostname")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(MODE_PARAM_NAME, MODE_NON_INTERACTIVE)
|
||||
.help(r#"A hostname of the certificate for serving TLS connections.
|
||||
Required in non-interactive mode."#),
|
||||
clap::Arg::new(LIBRARY_SETTINGS_FILE_PARAM_NAME)
|
||||
.long("lib-settings")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(MODE_PARAM_NAME, MODE_NON_INTERACTIVE)
|
||||
.help("Path to store the library settings file. Required in non-interactive mode."),
|
||||
clap::Arg::new(TLS_HOSTS_SETTINGS_FILE_PARAM_NAME)
|
||||
.long("hosts-settings")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(MODE_PARAM_NAME, MODE_NON_INTERACTIVE)
|
||||
.help("Path to store the TLS hosts settings file. Required in non-interactive mode."),
|
||||
])
|
||||
.get_matches();
|
||||
|
||||
*MODE.lock().unwrap() = match args.get_one::<String>(MODE_PARAM_NAME).map(String::as_str) {
|
||||
None => Mode::Interactive,
|
||||
Some(MODE_NON_INTERACTIVE) => Mode::NonInteractive,
|
||||
Some("interactive") => Mode::Interactive,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
*PREDEFINED_PARAMS.lock().unwrap() = PredefinedParameters {
|
||||
listen_address: args.get_one::<String>(LISTEN_ADDRESS_PARAM_NAME).cloned(),
|
||||
credentials: args.get_one::<String>(CREDENTIALS_PARAM_NAME)
|
||||
.map(|x| x.splitn(2, ':'))
|
||||
.and_then(|mut x| x.next().zip(x.next()))
|
||||
.map(|(a, b)| (a.to_string(), b.to_string())),
|
||||
hostname: args.get_one::<String>(HOSTNAME_PARAM_NAME).cloned(),
|
||||
library_settings_file: args.get_one::<String>(LIBRARY_SETTINGS_FILE_PARAM_NAME).cloned(),
|
||||
tls_hosts_settings_file: args.get_one::<String>(TLS_HOSTS_SETTINGS_FILE_PARAM_NAME).cloned(),
|
||||
};
|
||||
|
||||
println!("Welcome to the setup wizard");
|
||||
|
||||
let library_settings_path = find_existent_settings::<Settings>(".")
|
||||
.and_then(|fname|
|
||||
ask_for_agreement(&format!("Use the existing library settings {}?", fname))
|
||||
.then_some(fname)
|
||||
)
|
||||
.or_else(|| {
|
||||
println!("Let's build the library settings");
|
||||
let built = library_settings::build();
|
||||
println!("The library settings are successfully built\n");
|
||||
|
||||
let path = ask_for_input::<String>(
|
||||
"Path to a file to store the library settings",
|
||||
Some(get_predefined_params().library_settings_file.clone()
|
||||
.unwrap_or("vpn.toml".into())),
|
||||
);
|
||||
if checked_overwrite(&path, "Overwrite the existing library settings file?") {
|
||||
let doc = composer::compose_document(&built.settings, &built.credentials_path);
|
||||
fs::write(&path, doc)
|
||||
.expect("Couldn't write the library settings to a file");
|
||||
}
|
||||
Some(path)
|
||||
});
|
||||
|
||||
let hosts_settings_path = find_existent_settings::<TlsHostsSettings>(".")
|
||||
.and_then(|fname|
|
||||
ask_for_agreement(&format!("Use the existing TLS hosts settings {}?", fname))
|
||||
.then_some(fname)
|
||||
)
|
||||
.or_else(|| {
|
||||
println!("Let's build the TLS hosts settings");
|
||||
let settings = tls_hosts_settings::build();
|
||||
println!("The TLS hosts settings are successfully built\n");
|
||||
|
||||
let path = ask_for_input::<String>(
|
||||
"Path to a file to store the TLS hosts settings",
|
||||
Some(get_predefined_params().tls_hosts_settings_file.clone()
|
||||
.unwrap_or("hosts.toml".into())),
|
||||
);
|
||||
if checked_overwrite(&path, "Overwrite the existing TLS hosts settings file?") {
|
||||
fs::write(
|
||||
&path,
|
||||
toml::ser::to_string(&settings)
|
||||
.expect("Couldn't serialize the TLS hosts settings"),
|
||||
).expect("Couldn't write the TLS hosts settings to a file");
|
||||
}
|
||||
Some(path)
|
||||
});
|
||||
|
||||
if let (Some(l), Some(h)) = (library_settings_path, hosts_settings_path) {
|
||||
println!("To start endpoint, run the following command:");
|
||||
println!("\tvpn_endpoint {} {}", l, h);
|
||||
}
|
||||
println!("To see full set of the available options, run the following command:");
|
||||
println!("\tvpn_endpoint -h");
|
||||
}
|
||||
|
||||
fn find_existent_settings<T: serde::de::DeserializeOwned>(path: &str) -> Option<String> {
|
||||
(get_mode() != Mode::NonInteractive)
|
||||
.then(|| fs::read_dir(path).ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.metadata()
|
||||
.map(|meta| meta.is_file()).unwrap_or_default())
|
||||
.filter_map(|entry| entry.file_name().into_string().ok())
|
||||
.filter_map(|fname| fs::read_to_string(&fname).ok().zip(Some(fname)))
|
||||
.find_map(|(content, fname)| toml::from_str::<T>(&content).map(|_| fname).ok())
|
||||
)
|
||||
.flatten()
|
||||
}
|
||||
250
tools/setup_wizard/template_settings.rs
Normal file
250
tools/setup_wizard/template_settings.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use vpn_libs_endpoint::settings::{ForwardProtocolSettings, Http1Settings, Http2Settings, IcmpSettings, ListenProtocolSettings, MetricsSettings, QuicSettings, Settings, Socks5ForwarderSettings};
|
||||
use vpn_libs_endpoint::utils::IterJoin;
|
||||
|
||||
pub trait ToTomlComment {
|
||||
/// Prepend each line of string with "# " turning
|
||||
/// the whole string it into TOML comment.
|
||||
fn to_toml_comment(&self) -> String;
|
||||
}
|
||||
|
||||
impl ToTomlComment for &str {
|
||||
fn to_toml_comment(&self) -> String {
|
||||
self.lines()
|
||||
.map(|x| format!("# {x}"))
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTomlComment for String {
|
||||
fn to_toml_comment(&self) -> String {
|
||||
self.as_str().to_toml_comment()
|
||||
}
|
||||
}
|
||||
|
||||
pub static MAIN_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}
|
||||
listen_address = ""
|
||||
|
||||
# The path to a TOML file in the following format:
|
||||
#
|
||||
# ```
|
||||
# [[client]]
|
||||
# username = "a"
|
||||
# password = "b"
|
||||
#
|
||||
# [[client]]
|
||||
# ...
|
||||
# ```
|
||||
credentials_file = "{}"
|
||||
|
||||
{}
|
||||
ipv6_available = {}
|
||||
|
||||
{}
|
||||
allow_private_network_connections = {}
|
||||
|
||||
{}
|
||||
tls_handshake_timeout_secs = {}
|
||||
|
||||
{}
|
||||
client_listener_timeout_secs = {}
|
||||
|
||||
{}
|
||||
connection_establishment_timeout_secs = {}
|
||||
|
||||
{}
|
||||
tcp_connections_timeout_secs = {}
|
||||
|
||||
{}
|
||||
udp_connections_timeout_secs = {}
|
||||
"#,
|
||||
Settings::doc_listen_address().to_toml_comment(),
|
||||
crate::library_settings::DEFAULT_CREDENTIALS_PATH,
|
||||
Settings::doc_ipv6_available().to_toml_comment(),
|
||||
Settings::default_ipv6_available(),
|
||||
Settings::doc_allow_private_network_connections().to_toml_comment(),
|
||||
Settings::default_allow_private_network_connections(),
|
||||
format!("{}. In seconds.", Settings::doc_tls_handshake_timeout()).to_toml_comment(),
|
||||
Settings::default_tls_handshake_timeout().as_secs(),
|
||||
format!("{}. In seconds.", Settings::doc_client_listener_timeout()).to_toml_comment(),
|
||||
Settings::default_client_listener_timeout().as_secs(),
|
||||
format!("{} In seconds.", Settings::doc_connection_establishment_timeout()).to_toml_comment(),
|
||||
Settings::default_connection_establishment_timeout().as_secs(),
|
||||
format!("{}. In seconds.", Settings::doc_tcp_connections_timeout()).to_toml_comment(),
|
||||
Settings::default_tcp_connections_timeout().as_secs(),
|
||||
format!("{}. In seconds.", Settings::doc_udp_connections_timeout()).to_toml_comment(),
|
||||
Settings::default_udp_connections_timeout().as_secs(),
|
||||
));
|
||||
|
||||
pub static FORWARD_PROTOCOL_COMMON_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
# Possible values:
|
||||
# * direct: a direct forwarder routes a connection directly to its target host,
|
||||
# * socks5: a SOCKS5 forwarder routes a connection though a SOCKS5 proxy.
|
||||
# Default is direct
|
||||
[forward_protocol]
|
||||
"#,
|
||||
ForwardProtocolSettings::doc().to_toml_comment(),
|
||||
));
|
||||
|
||||
pub static DIRECT_FORWARDER_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
[forward_protocol.direct]"#,
|
||||
ForwardProtocolSettings::doc_direct().to_toml_comment(),
|
||||
));
|
||||
|
||||
pub static SOCKS_FORWARDER_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
[forward_protocol.socks5]
|
||||
{}
|
||||
address = "127.0.0.1:1080"
|
||||
{}
|
||||
extended_auth = false"#,
|
||||
ForwardProtocolSettings::doc_socks5().to_toml_comment(),
|
||||
Socks5ForwarderSettings::doc_address().to_toml_comment(),
|
||||
Socks5ForwarderSettings::doc_extended_auth().to_toml_comment(),
|
||||
));
|
||||
|
||||
pub static LISTENER_COMMON_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
# Possible values:
|
||||
# * http1: enables HTTP1 codec,
|
||||
# * http2: enables HTTP2 codec,
|
||||
# * quic: enables QUIC/HTTP3 codec.
|
||||
# At least one listener codec MUST be specified.
|
||||
[listen_protocols]
|
||||
"#,
|
||||
ListenProtocolSettings::doc().to_toml_comment(),
|
||||
));
|
||||
|
||||
pub static HTTP1_LISTENER_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
[listen_protocols.http1]
|
||||
{}
|
||||
upload_buffer_size = {}
|
||||
"#,
|
||||
Http1Settings::doc().to_toml_comment(),
|
||||
Http1Settings::doc_upload_buffer_size().to_toml_comment(),
|
||||
Http1Settings::default_upload_buffer_size(),
|
||||
));
|
||||
|
||||
pub static HTTP2_LISTENER_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
[listen_protocols.http2]
|
||||
{}
|
||||
initial_connection_window_size = {}
|
||||
{}
|
||||
initial_stream_window_size = {}
|
||||
{}
|
||||
max_concurrent_streams = {}
|
||||
{}
|
||||
max_frame_size = {}
|
||||
{}
|
||||
header_table_size = {}
|
||||
"#,
|
||||
Http2Settings::doc().to_toml_comment(),
|
||||
Http2Settings::doc_initial_connection_window_size().to_toml_comment(),
|
||||
Http2Settings::default_initial_connection_window_size(),
|
||||
Http2Settings::doc_initial_stream_window_size().to_toml_comment(),
|
||||
Http2Settings::default_initial_stream_window_size(),
|
||||
Http2Settings::doc_max_concurrent_streams().to_toml_comment(),
|
||||
Http2Settings::default_max_concurrent_streams(),
|
||||
Http2Settings::doc_max_frame_size().to_toml_comment(),
|
||||
Http2Settings::default_max_frame_size(),
|
||||
Http2Settings::doc_header_table_size().to_toml_comment(),
|
||||
Http2Settings::default_header_table_size(),
|
||||
));
|
||||
|
||||
pub static QUIC_LISTENER_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}.
|
||||
[listen_protocols.quic]
|
||||
{}
|
||||
recv_udp_payload_size = {}
|
||||
{}
|
||||
send_udp_payload_size = {}
|
||||
{}
|
||||
initial_max_data = {}
|
||||
{}
|
||||
max_stream_data_bidi_local = {}
|
||||
{}
|
||||
max_stream_data_bidi_remote = {}
|
||||
{}
|
||||
max_stream_data_uni = {}
|
||||
{}
|
||||
max_streams_bidi = {}
|
||||
{}
|
||||
max_streams_uni = {}
|
||||
{}
|
||||
max_connection_window = {}
|
||||
{}
|
||||
max_stream_window = {}
|
||||
{}
|
||||
disable_active_migration = {}
|
||||
{}
|
||||
enable_early_data = {}
|
||||
{}
|
||||
message_queue_capacity = {}
|
||||
"#,
|
||||
QuicSettings::doc().to_toml_comment(),
|
||||
QuicSettings::doc_recv_udp_payload_size().to_toml_comment(),
|
||||
QuicSettings::default_recv_udp_payload_size(),
|
||||
QuicSettings::doc_send_udp_payload_size().to_toml_comment(),
|
||||
QuicSettings::default_send_udp_payload_size(),
|
||||
QuicSettings::doc_initial_max_data().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_data(),
|
||||
QuicSettings::doc_initial_max_stream_data_bidi_local().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_stream_data_bidi_local(),
|
||||
QuicSettings::doc_initial_max_stream_data_bidi_remote().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_stream_data_bidi_remote(),
|
||||
QuicSettings::doc_initial_max_stream_data_uni().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_stream_data_uni(),
|
||||
QuicSettings::doc_initial_max_streams_bidi().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_streams_bidi(),
|
||||
QuicSettings::doc_initial_max_streams_uni().to_toml_comment(),
|
||||
QuicSettings::default_initial_max_streams_uni(),
|
||||
QuicSettings::doc_max_connection_window().to_toml_comment(),
|
||||
QuicSettings::default_max_connection_window(),
|
||||
QuicSettings::doc_max_stream_window().to_toml_comment(),
|
||||
QuicSettings::default_max_stream_window(),
|
||||
QuicSettings::doc_disable_active_migration().to_toml_comment(),
|
||||
QuicSettings::default_disable_active_migration(),
|
||||
QuicSettings::doc_enable_early_data().to_toml_comment(),
|
||||
QuicSettings::default_enable_early_data(),
|
||||
QuicSettings::doc_message_queue_capacity().to_toml_comment(),
|
||||
QuicSettings::default_message_queue_capacity(),
|
||||
));
|
||||
|
||||
pub static ICMP_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}
|
||||
[icmp]
|
||||
{}
|
||||
interface_name = "{}"
|
||||
{}
|
||||
request_timeout_secs = {}
|
||||
{}
|
||||
recv_message_queue_capacity = {}
|
||||
"#,
|
||||
IcmpSettings::doc().to_toml_comment(),
|
||||
IcmpSettings::doc_interface_name().to_toml_comment(),
|
||||
IcmpSettings::default_interface_name(),
|
||||
IcmpSettings::doc_request_timeout().to_toml_comment(),
|
||||
IcmpSettings::default_request_timeout().as_secs(),
|
||||
IcmpSettings::doc_recv_message_queue_capacity().to_toml_comment(),
|
||||
IcmpSettings::default_message_queue_capacity(),
|
||||
));
|
||||
|
||||
pub static METRICS_TABLE: Lazy<String> = Lazy::new(|| format!(
|
||||
r#"{}
|
||||
[metrics]
|
||||
{}
|
||||
address = "{}"
|
||||
{}
|
||||
request_timeout_secs = {}
|
||||
"#,
|
||||
MetricsSettings::doc().to_toml_comment(),
|
||||
MetricsSettings::doc_address().to_toml_comment(),
|
||||
MetricsSettings::default_listen_address(),
|
||||
MetricsSettings::doc_request_timeout().to_toml_comment(),
|
||||
MetricsSettings::default_request_timeout().as_secs(),
|
||||
));
|
||||
194
tools/setup_wizard/tls_hosts_settings.rs
Normal file
194
tools/setup_wizard/tls_hosts_settings.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use chrono::Datelike;
|
||||
use rcgen::DnType;
|
||||
use x509_parser::extensions::GeneralName;
|
||||
use vpn_libs_endpoint::settings::{TlsHostInfo, TlsHostsSettings};
|
||||
use vpn_libs_endpoint::utils;
|
||||
use vpn_libs_endpoint::utils::Either;
|
||||
use crate::Mode;
|
||||
use crate::user_interaction::{ask_for_agreement, ask_for_input, checked_overwrite};
|
||||
|
||||
const DEFAULT_CERTIFICATE_DURATION_DAYS: u64 = 365;
|
||||
const DEFAULT_CERTIFICATE_FOLDER: &str = "certs";
|
||||
const DEFAULT_HOSTNAME: &str = "vpn.endpoint";
|
||||
|
||||
pub fn build() -> TlsHostsSettings {
|
||||
let cert = lookup_existent_cert()
|
||||
.and_then(|x| (crate::get_mode() != Mode::NonInteractive
|
||||
&& ask_for_agreement(&format!("Use an existent certificate? {:?}", x)))
|
||||
.then_some(x))
|
||||
.or_else(|| (crate::get_mode() == Mode::NonInteractive
|
||||
|| ask_for_agreement("Generate a self-signed certificate?"))
|
||||
.then(generate_cert).flatten())
|
||||
.or_else(|| {
|
||||
let pair = ask_for_input::<String>(
|
||||
"Path to key/certificate pair. Divide by space if they are in separate files.\n",
|
||||
None,
|
||||
);
|
||||
|
||||
let mut iter = pair.splitn(2, char::is_whitespace);
|
||||
let x = match (iter.next().unwrap(), iter.next()) {
|
||||
(a, None) => Either::Left(a),
|
||||
(a, Some(b)) => Either::Right((a, b)),
|
||||
};
|
||||
|
||||
let x = parse_cert(x);
|
||||
if x.is_none() {
|
||||
println!("Couldn't parse the provided key/certificate pair");
|
||||
}
|
||||
x
|
||||
});
|
||||
|
||||
let hostname = cert.as_ref().map(|x| x.common_name.clone())
|
||||
.unwrap_or_else(|| ask_for_input::<String>(
|
||||
"Endpoint hostname (used for serving TLS connections)",
|
||||
Some(crate::get_predefined_params().hostname.clone()
|
||||
.unwrap_or_else(|| DEFAULT_HOSTNAME.into())),
|
||||
));
|
||||
|
||||
TlsHostsSettings::builder()
|
||||
.main_hosts(vec![TlsHostInfo {
|
||||
hostname: hostname.clone(),
|
||||
cert_chain_path: cert.as_ref().unwrap().cert_path.clone(),
|
||||
private_key_path: cert.as_ref().unwrap().key_path.clone(),
|
||||
}])
|
||||
.ping_hosts(vec![TlsHostInfo {
|
||||
hostname: format!("ping.{}", hostname),
|
||||
cert_chain_path: cert.as_ref().unwrap().cert_path.clone(),
|
||||
private_key_path: cert.as_ref().unwrap().key_path.clone(),
|
||||
}])
|
||||
.speedtest_hosts(vec![TlsHostInfo {
|
||||
hostname: format!("speed.{}", hostname),
|
||||
cert_chain_path: cert.as_ref().unwrap().cert_path.clone(),
|
||||
private_key_path: cert.as_ref().unwrap().key_path.clone(),
|
||||
}])
|
||||
.build().expect("Couldn't build TLS hosts settings")
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Cert {
|
||||
common_name: String,
|
||||
#[allow(dead_code)] // needed only for logging
|
||||
alt_names: Vec<String>,
|
||||
#[allow(dead_code)] // needed only for logging
|
||||
expiration_date: String,
|
||||
cert_path: String,
|
||||
key_path: String,
|
||||
}
|
||||
|
||||
fn lookup_existent_cert() -> Option<Cert> {
|
||||
let files = fs::read_dir(DEFAULT_CERTIFICATE_FOLDER).ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.metadata().map(|meta| meta.is_file()).unwrap_or_default())
|
||||
.filter_map(|entry| entry.path().to_str().map(String::from))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let cert_key_pair = match files.as_slice() {
|
||||
[a] => Either::Left(a.as_str()),
|
||||
[a, b] => Either::Right((a.as_str(), b.as_str())),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
parse_cert(cert_key_pair)
|
||||
}
|
||||
|
||||
fn parse_cert(cert: Either<&str, (&str, &str)>) -> Option<Cert> {
|
||||
let (chain, cert_path, key_path) = cert.map(
|
||||
|pair| Some((
|
||||
utils::load_private_key(pair).and_then(|_| utils::load_certs(pair)).ok()?,
|
||||
pair,
|
||||
pair,
|
||||
)),
|
||||
|(a, b)|
|
||||
match (
|
||||
utils::load_certs(a), utils::load_private_key(b),
|
||||
utils::load_certs(b), utils::load_private_key(a),
|
||||
) {
|
||||
(Ok(chain), Ok(_), _, _) => Some((chain, a, b)),
|
||||
(_, _, Ok(chain), Ok(_)) => Some((chain, b, a)),
|
||||
_ => None,
|
||||
},
|
||||
)?;
|
||||
|
||||
let cert = x509_parser::parse_x509_certificate(chain.first()?.0.as_slice()).ok()?.1;
|
||||
Some(Cert {
|
||||
common_name: cert.validity.is_valid()
|
||||
.then(|| {
|
||||
let x = cert.subject.to_string();
|
||||
x.as_str()
|
||||
.strip_prefix("CN=")
|
||||
.map(String::from)
|
||||
.unwrap_or(x)
|
||||
})?,
|
||||
alt_names: cert.subject_alternative_name().ok().flatten()
|
||||
.map(|x| x.value.general_names.iter().map(GeneralName::to_string).collect())
|
||||
.unwrap_or_default(),
|
||||
expiration_date: cert.validity.not_after.to_string(),
|
||||
cert_path: cert_path.into(),
|
||||
key_path: key_path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_cert() -> Option<Cert> {
|
||||
let (common_name, alt_names) = {
|
||||
println!("Let's generate a self-signed certificate.");
|
||||
let name = ask_for_input::<String>(
|
||||
"Endpoint hostname (used for serving TLS connections)",
|
||||
Some(crate::get_predefined_params().hostname.clone()
|
||||
.unwrap_or_else(|| DEFAULT_HOSTNAME.into())),
|
||||
);
|
||||
(name.clone(), vec![name.clone(), format!("*.{}", name)])
|
||||
};
|
||||
let mut params = rcgen::CertificateParams::new(alt_names.clone());
|
||||
params.alg = &rcgen::PKCS_ECDSA_P256_SHA256;
|
||||
let now = chrono::Local::now();
|
||||
let end_date = now.checked_add_days(
|
||||
chrono::Days::new(DEFAULT_CERTIFICATE_DURATION_DAYS)
|
||||
).unwrap();
|
||||
params.not_before = rcgen::date_time_ymd(now.year(), now.month() as u8, now.day() as u8);
|
||||
params.not_after = rcgen::date_time_ymd(end_date.year(), end_date.month() as u8, end_date.day() as u8);
|
||||
params.distinguished_name.push(DnType::CommonName, &common_name);
|
||||
|
||||
let cert = rcgen::Certificate::from_params(params).unwrap();
|
||||
let cert_path = format!("{DEFAULT_CERTIFICATE_FOLDER}/cert.pem");
|
||||
if !checked_overwrite(&cert_path, "Overwrite the existing certificate file?") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let key_path = format!("{DEFAULT_CERTIFICATE_FOLDER}/key.pem");
|
||||
if !checked_overwrite(&cert_path, "Overwrite the existing private key file?") {
|
||||
return None;
|
||||
}
|
||||
|
||||
fs::create_dir_all(Path::new(&cert_path).parent().unwrap())
|
||||
.expect("Couldn't create certificate directory path");
|
||||
fs::write(&cert_path, cert.serialize_pem().unwrap())
|
||||
.expect("Couldn't write the certificate into a file");
|
||||
println!("The generated certificate is stored in file: {}", cert_path);
|
||||
|
||||
fs::create_dir_all(Path::new(&cert_path).parent().unwrap())
|
||||
.expect("Couldn't create private key directory path");
|
||||
if key_path != cert_path {
|
||||
fs::write(key_path.clone(), cert.serialize_private_key_pem())
|
||||
.expect("Couldn't write the private key into a file");
|
||||
} else {
|
||||
fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.append(true)
|
||||
.open(key_path.clone())
|
||||
.expect("Couldn't open a file for writing the private key")
|
||||
.write_all(cert.serialize_private_key_pem().as_bytes())
|
||||
.expect("Couldn't write the private key into a file");
|
||||
}
|
||||
println!("The generated private key is stored in file: {}", key_path);
|
||||
|
||||
Some(Cert {
|
||||
common_name,
|
||||
alt_names,
|
||||
expiration_date: end_date.to_string(),
|
||||
cert_path,
|
||||
key_path,
|
||||
})
|
||||
}
|
||||
85
tools/setup_wizard/user_interaction.rs
Normal file
85
tools/setup_wizard/user_interaction.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::fs;
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use dialoguer::{Confirm, Input, Password, Select};
|
||||
use dialoguer::theme::ColorfulTheme;
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::Mode;
|
||||
|
||||
pub static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
|
||||
|
||||
/// Ask user to enter a value.
|
||||
/// If [`default`] is [`Some`], suggest the value in the prompt.
|
||||
pub fn ask_for_input<T>(message: &str, default: Option<T>) -> T
|
||||
where
|
||||
T: Clone + Default + FromStr + ToString,
|
||||
<T as FromStr>::Err: ToString,
|
||||
{
|
||||
if crate::get_mode() == Mode::NonInteractive {
|
||||
return default.expect("Expecting a user input in non-interactive mode");
|
||||
}
|
||||
|
||||
if default.is_some() {
|
||||
Input::<T>::with_theme(THEME.deref())
|
||||
.with_prompt(message)
|
||||
.show_default(default.is_some())
|
||||
.default(default.unwrap_or_default())
|
||||
.interact().unwrap()
|
||||
} else {
|
||||
Input::<T>::with_theme(THEME.deref())
|
||||
.with_prompt(message)
|
||||
.interact().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask if one wants to do something (yes/no)
|
||||
pub fn ask_for_agreement(message: &str) -> bool {
|
||||
assert_ne!(crate::get_mode(), Mode::NonInteractive, "Expecting a user input in non-interactive mode");
|
||||
Confirm::with_theme(THEME.deref())
|
||||
.with_prompt(message)
|
||||
.default(false)
|
||||
.show_default(true)
|
||||
.interact()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Ask user to enter a password in a secure way
|
||||
pub fn ask_for_password(message: &str) -> String {
|
||||
assert_ne!(crate::get_mode(), Mode::NonInteractive, "Expecting a user input in non-interactive mode");
|
||||
Password::with_theme(THEME.deref())
|
||||
.with_prompt(message)
|
||||
.interact()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Check if a file exists and if it does, ask if one wants to overwrite it
|
||||
pub fn checked_overwrite(path: &str, message: &str) -> bool {
|
||||
crate::get_mode() == Mode::NonInteractive
|
||||
|| !fs::metadata(Path::new(&path)).as_ref()
|
||||
.map(fs::Metadata::is_file)
|
||||
.unwrap_or_default()
|
||||
|| ask_for_agreement(message)
|
||||
}
|
||||
|
||||
/// Ask user to select a variant. Returns index of the selected variant.
|
||||
pub fn select_index<S: Into<String>>(prompt: S, variants: &[&str], default: Option<usize>) -> usize {
|
||||
if crate::get_mode() == Mode::NonInteractive {
|
||||
return default.expect("Expecting a user input in non-interactive mode");
|
||||
}
|
||||
|
||||
Select::with_theme(THEME.deref())
|
||||
.with_prompt(prompt)
|
||||
.items(variants)
|
||||
.report(true)
|
||||
.default(default.unwrap_or_default())
|
||||
.interact_opt().expect("Interaction failure")
|
||||
.expect("None selected")
|
||||
}
|
||||
|
||||
/// Ask user to select a variant. Returns the selected variant.
|
||||
pub fn select_variant<'a, S>(prompt: S, variants: &[&'a str], default: Option<usize>) -> &'a str
|
||||
where S: Into<String>
|
||||
{
|
||||
variants[select_index(prompt, variants, default)]
|
||||
}
|
||||
Reference in New Issue
Block a user