Pull request #73: Introduce a docker image to set up an endpoint

Merge in ADGUARD-CORE-LIBS/vpn-libs-endpoint from feature/AG-22970 to master

Squashed commit of the following:

commit 98ba1f1c8157786afa476077463dd23598a2cfea
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Jul 17 11:08:15 2023 +0300

    clean up

commit 2917030a1bd15323904693da021dfbff64b7d691
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Jul 17 11:06:06 2023 +0300

    changelog

commit bad35fd0a99c932e0a2e6ca2d00ae2c61f80eefc
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Jul 17 10:53:34 2023 +0300

    Introduce a docker image to set up an endpoint and a makefile to ease the setup procedure

commit 5c9d1148320dff2ca75c366fc911d478678bcf26
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Jul 17 10:46:50 2023 +0300

    wizard: do not ask for input if a parameter is predefined

commit 82222837b220ec30d0b5e1d08bff9077333da242
Author: Sergei Gunchenko <s.gunchenko@adguard.com>
Date:   Mon Jul 17 10:41:16 2023 +0300

    bench: minor
This commit is contained in:
Sergei Gunchenko
2023-07-18 10:11:39 +03:00
parent 08f07a91da
commit 51713b8e0f
7 changed files with 161 additions and 30 deletions

View File

@@ -1,5 +1,11 @@
# CHANGELOG
* Added a [docker image](docker/Dockerfile) with a configured and running endpoint.
* Added a [Makefile](Makefile) to simplify building and running the endpoint.
* Setup Wizard now doesn't ask for parameters specified through command line arguments.
E.g., with `setup_wizard --lib-settings vpn.toml` it won't ask a user for the library
settings file path.
## 0.9.47
* Removed RADIUS-based authenticator

87
Makefile Normal file
View File

@@ -0,0 +1,87 @@
BUILD_TYPE ?= release
ifeq ($(BUILD_TYPE), release)
CARGO_BUILD_TYPE = --release
endif
LOG_LEVEL ?= trace
CONFIG_FILE ?= vpn.toml
HOSTS_CONFIG_FILE ?= hosts.toml
DOCKER_IMAGE_NAME ?= adguard-vpn-endpoint
ENDPOINT_URL ?= git@github.com:AdguardTeam/VpnLibsEndpointPrivate.git
ENDPOINT_VERSION ?= master
ENDPOINT_HOSTNAME ?= vpn.endpoint
DOCKER_DIR = docker
DOCKER_ENDPOINT_DIR = vpn-libs-endpoint
DOCKER_ENDPOINT_CONFIG_DIR = config
LISTEN_ADDRESS ?= 0.0.0.0
LISTEN_PORT ?= 443
.PHONY: endpoint/build-wizard
## Build the setup wizard
endpoint/build-wizard:
cargo build $(CARGO_BUILD_TYPE) --bin setup_wizard
.PHONY: endpoint/setup
## Run the setup wizard to create all the required configuration files
endpoint/setup: endpoint/build-wizard
cargo run $(CARGO_BUILD_TYPE) --bin setup_wizard -- \
--hostname "$(ENDPOINT_HOSTNAME)" \
--address "$(LISTEN_ADDRESS):$(LISTEN_PORT)" \
--lib-settings "$(CONFIG_FILE)" \
--hosts-settings "$(HOSTS_CONFIG_FILE)"
.PHONY: endpoint/build
## Build the endpoint
endpoint/build:
cargo build $(CARGO_BUILD_TYPE) --bin vpn_endpoint
.PHONY: endpoint/run
## Run the endpoint with the existing configuration files
endpoint/run: endpoint/build
cargo run $(CARGO_BUILD_TYPE) --bin vpn_endpoint -- \
-l "$(LOG_LEVEL)" "$(CONFIG_FILE)" "$(HOSTS_CONFIG_FILE)"
.PHONY: endpoint/clean
## Clean cargo artifacts
endpoint/clean:
cargo clean
.PHONY: docker/-checkout-repo
docker/-checkout-repo:
@if [ ! -d "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_DIR)" ]; then \
git clone "$(ENDPOINT_URL)" "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_DIR)" && \
git checkout "$(ENDPOINT_VERSION)"; \
fi
.PHONY: docker/build
## Build a docker image with the configured endpoint instance
docker/build: docker/-checkout-repo
docker build -t "$(DOCKER_IMAGE_NAME)" \
--build-arg LOG_LEVEL="$(LOG_LEVEL)" \
--build-arg CONFIG_FILE="$(CONFIG_FILE)" \
--build-arg HOSTS_CONFIG_FILE="$(HOSTS_CONFIG_FILE)" \
--build-arg ENDPOINT_DIR="$(DOCKER_ENDPOINT_DIR)" \
./docker
.PHONY: docker/run
## Run the docker image
docker/run: docker/build
docker run -d \
-p $(LISTEN_PORT):$(LISTEN_PORT) \
-p $(LISTEN_PORT):$(LISTEN_PORT)/udp \
"$(DOCKER_IMAGE_NAME)"
.PHONY: docker/setup-and-run
## Create an endpoint setup, build a docker image containing that setup and run the image.
## That is, it is a shorthand for the `endpoint/setup + docker/build + docker/run`.
docker/setup-and-run:
mkdir -p "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_CONFIG_DIR)"
cd "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_CONFIG_DIR)" && make -f ../../Makefile endpoint/setup
make docker/run
.PHONY: docker/clean
## Clean docker image
docker/clean:
docker image rm -f "$(DOCKER_IMAGE_NAME)"
rm -rf "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_DIR)"
rm -rf "$(DOCKER_DIR)/$(DOCKER_ENDPOINT_CONFIG_DIR)"

View File

@@ -7,8 +7,6 @@ ARG CONFIG_FILE="vpn.conf"
ARG TLS_HOSTS_SETTINGS_FILE="tls_hosts.conf"
ARG LOG_LEVEL="info"
RUN apt install -y openssl
COPY $ENDPOINT_DIR/lib/Cargo.toml /tmp/Cargo.toml
RUN cargo fetch --manifest-path /tmp/Cargo.toml && rm /tmp/Cargo.toml

36
docker/Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# syntax=docker/dockerfile:1
FROM python:3.11-slim-bullseye
ARG CERTS_DIR="certs"
ARG CONFIG_FILE="vpn.toml"
ARG CREDENTIALS_FILE="credentials.toml"
ARG ENDPOINT_DIR="vpn-libs-endpoint"
ARG CONFIG_DIR="config"
ARG HOSTS_CONFIG_FILE="hosts.toml"
ARG LOG_LEVEL="info"
ARG RUST_CHANNEL=1.67
RUN apt update && \
apt install -y build-essential cmake curl make
# Install Rust and Cargo
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $RUST_CHANNEL
ENV PATH="/root/.cargo/bin:$PATH"
COPY "$ENDPOINT_DIR" "/$ENDPOINT_DIR"
WORKDIR "$ENDPOINT_DIR"
RUN make endpoint/build
COPY "$CONFIG_DIR" "/$ENDPOINT_DIR/$CONFIG_DIR"
WORKDIR "/$ENDPOINT_DIR/$CONFIG_DIR"
ENV LOG_LEVEL="$LOG_LEVEL" \
CONFIG_FILE="$CONFIG_FILE" \
HOSTS_CONFIG_FILE="$HOSTS_CONFIG_FILE" \
RUST_BACKTRACE=1
ENTRYPOINT make -f ../Makefile endpoint/run \
LOG_LEVEL="$LOG_LEVEL" \
CONFIG_FILE="$CONFIG_FILE" \
HOSTS_CONFIG_FILE="$HOSTS_CONFIG_FILE" \
>>/tmp/vpn.log 2>&1

View File

@@ -13,11 +13,13 @@ pub struct Built {
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();
.listen_address(
crate::get_predefined_params().listen_address.clone()
.unwrap_or_else(|| ask_for_input(
Settings::doc_listen_address(),
Some(Settings::default_listen_address().to_string()),
))
).unwrap();
Built {
settings: builder

View File

@@ -128,11 +128,11 @@ Required in non-interactive mode."#),
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())),
);
let path = get_predefined_params().library_settings_file.clone()
.unwrap_or_else(|| ask_for_input::<String>(
"Path to a file to store the library settings",
Some("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)
@@ -151,11 +151,11 @@ Required in non-interactive mode."#),
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())),
);
let path = get_predefined_params().tls_hosts_settings_file.clone()
.unwrap_or_else(|| ask_for_input::<String>(
"Path to a file to store the TLS hosts settings",
Some("hosts.toml".into()),
));
if checked_overwrite(&path, "Overwrite the existing TLS hosts settings file?") {
fs::write(
&path,

View File

@@ -15,12 +15,14 @@ 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?"))
let cert = (crate::get_predefined_params().hostname.is_some()
|| crate::get_mode() == Mode::NonInteractive)
.then(generate_cert).flatten()
.or_else(|| 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(|| ask_for_agreement("Generate a self-signed certificate?")
.then(generate_cert).flatten())
.or_else(|| {
let pair = ask_for_input::<String>(
@@ -42,10 +44,10 @@ pub fn build() -> TlsHostsSettings {
});
let hostname = cert.as_ref().map(|x| x.common_name.clone())
.or_else(|| crate::get_predefined_params().hostname.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())),
Some(DEFAULT_HOSTNAME.into()),
));
TlsHostsSettings::builder()
@@ -134,11 +136,11 @@ fn parse_cert(cert: Either<&str, (&str, &str)>) -> Option<Cert> {
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())),
);
let name = crate::get_predefined_params().hostname.clone()
.unwrap_or_else(|| ask_for_input::<String>(
"Endpoint hostname (used for serving TLS connections)",
Some(DEFAULT_HOSTNAME.into()),
));
(name.clone(), vec![name.clone(), format!("*.{}", name)])
};
let mut params = rcgen::CertificateParams::new(alt_names.clone());