mirror of
https://github.com/TrustTunnel/TrustTunnel.git
synced 2026-07-09 17:18:36 +00:00
Pull request 174: feat vpn-libs-endpoint: improve docker support for trusttunnel
Squashed commit of the following:
commit 16a0c416f13fc32db3ae79ba7f0886ba3987f5aa
Author: Radmir Sadikov <r.sadikov@adguard.com>
Date: Thu Mar 5 12:01:45 2026 +0400
move healthcheck from Dockerfile to docker-compose.yml
commit 54aeb8d82462b7c8e7f5a70a744db2c8c8ad1e43
Author: Radmir Sadikov <r.sadikov@adguard.com>
Date: Wed Mar 4 15:50:26 2026 +0400
add port parameter for healthcheck
commit 0e375b9628a2ddcac9687d00068e9be7fa8a06a8
Author: Radmir Sadikov <r.sadikov@adguard.com>
Date: Tue Mar 3 17:47:27 2026 +0400
feat vpn-libs-endpoint: improve docker support for trusttunnel
This commit is contained in:
parent
18258db85f
commit
ebb1ff4d3b
9 changed files with 191 additions and 19 deletions
|
|
@ -87,6 +87,10 @@ The endpoint binary accepts the following command line arguments:
|
|||
|
||||
The main settings file contains core endpoint configuration. Example:
|
||||
|
||||
> Native deployments commonly use `listen_address = "0.0.0.0:443"`.
|
||||
> If you run Docker with host-to-container mapping `443:8443`, use
|
||||
> `listen_address = "0.0.0.0:8443"` inside `vpn.toml`.
|
||||
|
||||
```toml
|
||||
# The address to listen on
|
||||
listen_address = "0.0.0.0:443"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
- [Cross-compiling for Linux](#cross-compiling-for-linux)
|
||||
- [Usage](#usage)
|
||||
- [Setup](#setup)
|
||||
- [Run with External Configuration (CI/CD)](#run-with-external-configuration-cicd)
|
||||
- [Customized Configuration](#customized-configuration)
|
||||
- [See Also](#see-also)
|
||||
|
||||
|
|
@ -104,6 +105,38 @@ docker run -it trusttunnel-endpoint:latest --name trusttunnel-endpoint # Create
|
|||
docker start -i trusttunnel-endpoint # If you need to start your VPN endpoint again
|
||||
```
|
||||
|
||||
### Run with External Configuration (CI/CD)
|
||||
|
||||
If your CI/CD pipeline generates configuration files externally, you can run the container
|
||||
without setup wizard interaction by mounting those files into the working directory volume.
|
||||
|
||||
Expected files under `/trusttunnel_endpoint`:
|
||||
|
||||
- `vpn.toml`
|
||||
- `hosts.toml`
|
||||
- `credentials.toml`
|
||||
- Optional: `rules.toml`
|
||||
- Optional: `certs/` (if certificate files are referenced from `hosts.toml`)
|
||||
|
||||
`docker-compose.yml` maps host HTTPS to a high port inside the container:
|
||||
|
||||
- Host `443/tcp` -> Container `8443/tcp`
|
||||
- Host `443/udp` -> Container `8443/udp`
|
||||
|
||||
Therefore, make sure your `vpn.toml` listens on `0.0.0.0:8443`.
|
||||
|
||||
Start with compose:
|
||||
|
||||
```shell
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- No setup-related environment variables are required in this mode.
|
||||
- Port `80:80` is only needed for in-container Let's Encrypt HTTP-01 issuance/renewal.
|
||||
If certificates are provided externally, this mapping can be removed.
|
||||
|
||||
### Customized Configuration
|
||||
|
||||
For a more customized configuration experience, run the following commands:
|
||||
|
|
|
|||
16
Dockerfile
16
Dockerfile
|
|
@ -11,6 +11,7 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --defau
|
|||
ENV PATH="/root/.cargo/bin:$PATH"
|
||||
# Copy source files
|
||||
WORKDIR $ENDPOINT_DIR_NAME
|
||||
COPY deeplink/ ./deeplink
|
||||
COPY endpoint/ ./endpoint
|
||||
COPY lib/ ./lib
|
||||
COPY macros/ ./macros
|
||||
|
|
@ -21,14 +22,21 @@ RUN make endpoint/build
|
|||
RUN make endpoint/build-wizard
|
||||
|
||||
# Copy binaries
|
||||
FROM debian AS trusttunnel-endpoint
|
||||
FROM debian:bookworm-slim AS trusttunnel-endpoint
|
||||
ARG ENDPOINT_DIR_NAME="TrustTunnel"
|
||||
ARG LOG_LEVEL="info"
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates iproute2 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=build /home/$ENDPOINT_DIR_NAME/target/release/setup_wizard /bin/
|
||||
COPY --from=build /home/$ENDPOINT_DIR_NAME/target/release/trusttunnel_endpoint /bin/
|
||||
COPY --chmod=755 /docker-entrypoint.sh /scripts/
|
||||
WORKDIR /trusttunnel_endpoint
|
||||
VOLUME /trusttunnel_endpoint/
|
||||
ENTRYPOINT ["sh", "/scripts/docker-entrypoint.sh"]
|
||||
|
||||
# Persist endpoint state/configuration under this directory:
|
||||
# - vpn.toml
|
||||
# - hosts.toml
|
||||
# - credentials.toml
|
||||
# - rules.toml
|
||||
# - certs/
|
||||
VOLUME /trusttunnel_endpoint/
|
||||
ENTRYPOINT ["/scripts/docker-entrypoint.sh"]
|
||||
|
||||
|
|
|
|||
|
|
@ -183,8 +183,8 @@ The wizard will ask for the following fields, some of them have the default
|
|||
values you could safely use:
|
||||
|
||||
- **The address to listen on** - specify the address for the endpoint to listen
|
||||
on. Use the default `0.0.0.0:443` if you want the endpoint to listen on port
|
||||
443 (HTTPS) on all interfaces.
|
||||
on. Use `0.0.0.0:443` for native deployments (HTTPS on all interfaces).
|
||||
If you run with Docker port mapping `443:8443`, set it to `0.0.0.0:8443`.
|
||||
- **Path to credentials file** - path where the user credentials for
|
||||
authorization will be stored.
|
||||
- **Username** - the username the user will use for authorization.
|
||||
|
|
|
|||
29
docker-compose.yml
Normal file
29
docker-compose.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
services:
|
||||
trusttunnel-endpoint:
|
||||
build:
|
||||
context: .
|
||||
target: trusttunnel-endpoint
|
||||
image: trusttunnel-endpoint:local
|
||||
container_name: trusttunnel-endpoint
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "443:8443/tcp"
|
||||
- "443:8443/udp"
|
||||
# Required for Let's Encrypt HTTP-01 challenge (TT_CERT_TYPE=letsencrypt).
|
||||
# Remove this port if you do not issue/renew LE certificates in-container.
|
||||
- "80:80/tcp"
|
||||
environment:
|
||||
# Override if endpoint listens on a non-default container port.
|
||||
- TT_HEALTHCHECK_PORT=8443
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ss -ltn | grep -q ':${TT_HEALTHCHECK_PORT:-8443} '"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 20s
|
||||
retries: 3
|
||||
volumes:
|
||||
- trusttunnel_endpoint_data:/trusttunnel_endpoint
|
||||
|
||||
volumes:
|
||||
trusttunnel_endpoint_data:
|
||||
driver: local
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
check_file() {
|
||||
local file="$1"
|
||||
if [ ! -f "$file" ]; then
|
||||
|
|
@ -14,21 +16,73 @@ verify_configs() {
|
|||
|
||||
check_file "credentials.toml" || missing=1
|
||||
check_file "vpn.toml" || missing=1
|
||||
check_file "hosts.toml" || missing=1
|
||||
|
||||
return $missing
|
||||
}
|
||||
|
||||
main() {
|
||||
is_interactive=0
|
||||
[ ! -t 0 ] || is_interactive=1
|
||||
if [ ! verify_configs ] && [ ! -t 0 ]; then
|
||||
echo "Missing configuration file(s). Run this containter in an interactive mode to complete setup."
|
||||
exit 1
|
||||
else
|
||||
echo "Missing configuration file(s). Launching setup wizard."
|
||||
setup_wizard
|
||||
run_setup_wizard_noninteractive() {
|
||||
if [ -z "${TT_HOSTNAME:-}" ] || [ -z "${TT_CREDENTIALS:-}" ]; then
|
||||
echo "Error: TT_HOSTNAME and TT_CREDENTIALS are required for non-interactive setup"
|
||||
return 1
|
||||
fi
|
||||
trusttunnel_endpoint vpn.toml hosts.toml
|
||||
|
||||
local args=(
|
||||
"-m" "non-interactive"
|
||||
"-a" "${TT_LISTEN_ADDRESS:-0.0.0.0:8443}"
|
||||
"-c" "$TT_CREDENTIALS"
|
||||
"-n" "$TT_HOSTNAME"
|
||||
"--lib-settings" "vpn.toml"
|
||||
"--hosts-settings" "hosts.toml"
|
||||
)
|
||||
|
||||
case "${TT_CERT_TYPE:-self-signed}" in
|
||||
self-signed)
|
||||
args+=("--cert-type" "self-signed")
|
||||
;;
|
||||
letsencrypt)
|
||||
if [ -z "${TT_ACME_EMAIL:-}" ]; then
|
||||
echo "Error: TT_ACME_EMAIL is required when TT_CERT_TYPE=letsencrypt"
|
||||
return 1
|
||||
fi
|
||||
args+=("--cert-type" "letsencrypt" "--acme-email" "$TT_ACME_EMAIL")
|
||||
if [ "${TT_ACME_STAGING:-false}" = "true" ]; then
|
||||
args+=("--acme-staging")
|
||||
fi
|
||||
;;
|
||||
provided)
|
||||
if [ -z "${TT_CERT_PROVIDED_CHAIN_PATH:-}" ] || [ -z "${TT_CERT_PROVIDED_KEY_PATH:-}" ]; then
|
||||
echo "Error: TT_CERT_PROVIDED_CHAIN_PATH and TT_CERT_PROVIDED_KEY_PATH are required when TT_CERT_TYPE=provided"
|
||||
return 1
|
||||
fi
|
||||
args+=(
|
||||
"--cert-type" "provided"
|
||||
"--cert-chain-path" "$TT_CERT_PROVIDED_CHAIN_PATH"
|
||||
"--cert-key-path" "$TT_CERT_PROVIDED_KEY_PATH"
|
||||
)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported TT_CERT_TYPE='$TT_CERT_TYPE'. Supported: self-signed, letsencrypt, provided"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Missing configuration file(s). Running setup_wizard in non-interactive mode"
|
||||
setup_wizard "${args[@]}"
|
||||
}
|
||||
|
||||
main() {
|
||||
if ! verify_configs; then
|
||||
if [ -t 0 ]; then
|
||||
echo "Missing configuration file(s). Launching setup wizard in interactive mode"
|
||||
setup_wizard
|
||||
else
|
||||
run_setup_wizard_noninteractive
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting trusttunnel_endpoint"
|
||||
exec trusttunnel_endpoint vpn.toml hosts.toml
|
||||
}
|
||||
|
||||
main
|
||||
|
|
@ -27,7 +27,7 @@ pub fn build() -> Built {
|
|||
.unwrap_or_else(|| {
|
||||
ask_for_input(
|
||||
&format!(
|
||||
"{} (use 0.0.0.0:443 for all interfaces on HTTPS port)",
|
||||
"{} (native: 0.0.0.0:443; Docker with 443:8443 mapping: 0.0.0.0:8443)",
|
||||
Settings::doc_listen_address()
|
||||
),
|
||||
Some(Settings::default_listen_address().to_string()),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ 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";
|
||||
const CERT_TYPE_PARAM_NAME: &str = "cert_type";
|
||||
const CERT_CHAIN_PATH_PARAM_NAME: &str = "cert_chain_path";
|
||||
const CERT_KEY_PATH_PARAM_NAME: &str = "cert_key_path";
|
||||
const ACME_EMAIL_PARAM_NAME: &str = "acme_email";
|
||||
const ACME_CHALLENGE_PARAM_NAME: &str = "acme_challenge";
|
||||
const ACME_STAGING_PARAM_NAME: &str = "acme_staging";
|
||||
|
|
@ -45,6 +47,8 @@ pub struct PredefinedParameters {
|
|||
pub library_settings_file: Option<String>,
|
||||
pub tls_hosts_settings_file: Option<String>,
|
||||
pub cert_type: Option<String>,
|
||||
pub cert_chain_path: Option<String>,
|
||||
pub cert_key_path: Option<String>,
|
||||
pub acme_email: Option<String>,
|
||||
pub acme_challenge: Option<String>,
|
||||
pub acme_staging: bool,
|
||||
|
|
@ -141,8 +145,20 @@ Required in non-interactive mode."#,
|
|||
clap::Arg::new(CERT_TYPE_PARAM_NAME)
|
||||
.long("cert-type")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(["self-signed", "letsencrypt"])
|
||||
.help("Certificate type: 'self-signed' or 'letsencrypt'"),
|
||||
.value_parser(["self-signed", "letsencrypt", "provided"])
|
||||
.help("Certificate type: 'self-signed', 'letsencrypt', or 'provided'"),
|
||||
clap::Arg::new(CERT_CHAIN_PATH_PARAM_NAME)
|
||||
.long("cert-chain-path")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(CERT_TYPE_PARAM_NAME, "provided")
|
||||
.help("Path to provided certificate chain (required when --cert-type=provided)"),
|
||||
clap::Arg::new(CERT_KEY_PATH_PARAM_NAME)
|
||||
.long("cert-key-path")
|
||||
.action(clap::ArgAction::Set)
|
||||
.value_parser(clap::builder::NonEmptyStringValueParser::new())
|
||||
.required_if_eq(CERT_TYPE_PARAM_NAME, "provided")
|
||||
.help("Path to provided private key (required when --cert-type=provided)"),
|
||||
clap::Arg::new(ACME_EMAIL_PARAM_NAME)
|
||||
.long("acme-email")
|
||||
.action(clap::ArgAction::Set)
|
||||
|
|
@ -202,6 +218,8 @@ Required in non-interactive mode."#,
|
|||
.get_one::<String>(TLS_HOSTS_SETTINGS_FILE_PARAM_NAME)
|
||||
.cloned(),
|
||||
cert_type: args.get_one::<String>(CERT_TYPE_PARAM_NAME).cloned(),
|
||||
cert_chain_path: args.get_one::<String>(CERT_CHAIN_PATH_PARAM_NAME).cloned(),
|
||||
cert_key_path: args.get_one::<String>(CERT_KEY_PATH_PARAM_NAME).cloned(),
|
||||
acme_email: args.get_one::<String>(ACME_EMAIL_PARAM_NAME).cloned(),
|
||||
acme_challenge: args.get_one::<String>(ACME_CHALLENGE_PARAM_NAME).cloned(),
|
||||
acme_staging: args.get_flag(ACME_STAGING_PARAM_NAME),
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ pub fn build_with_runtime() -> Option<Cert> {
|
|||
if crate::get_mode() == Mode::NonInteractive {
|
||||
// Check if Let's Encrypt is requested via CLI
|
||||
if let Some(ref cert_type) = crate::get_predefined_params().cert_type {
|
||||
if cert_type == "provided" {
|
||||
return load_provided_cert_noninteractive();
|
||||
}
|
||||
if cert_type == "letsencrypt" {
|
||||
return generate_letsencrypt_cert_noninteractive();
|
||||
}
|
||||
|
|
@ -498,6 +501,29 @@ fn generate_letsencrypt_cert_noninteractive() -> Option<Cert> {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_provided_cert_noninteractive() -> Option<Cert> {
|
||||
let predefined = crate::get_predefined_params();
|
||||
let cert_chain_path = predefined
|
||||
.cert_chain_path
|
||||
.clone()
|
||||
.expect("Certificate chain path is required for provided cert type");
|
||||
let cert_key_path = predefined
|
||||
.cert_key_path
|
||||
.clone()
|
||||
.expect("Certificate key path is required for provided cert type");
|
||||
drop(predefined);
|
||||
|
||||
let cert = parse_cert(Either::Right((&cert_chain_path, &cert_key_path)));
|
||||
if cert.is_none() {
|
||||
eprintln!(
|
||||
"Failed to load provided certificate and key from '{}' and '{}'",
|
||||
cert_chain_path, cert_key_path
|
||||
);
|
||||
}
|
||||
|
||||
cert
|
||||
}
|
||||
|
||||
fn parse_cert_expiration(cert_pem: &str) -> Option<String> {
|
||||
let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes()).ok()?;
|
||||
let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents).ok()?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue