diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 7fff4808c..b85dddfda 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -208,6 +208,42 @@ jobs: name: vey-iploc-all-test token: ${{ secrets.CODECOV_TOKEN }} verbose: false + vey-dcgen-test: + name: vey-dcgen test + runs-on: ubuntu-26.04 + steps: + - name: Checkout sources + uses: actions/checkout@v7 + with: + submodules: true + - name: Install rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: llvm-tools + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install capnproto libjemalloc-dev libssl-dev pkg-config clang libbpf-dev + - name: Install binutils + run: | + cargo install cargo-binutils@0.3.6 + - name: Cargo clean + run: cargo clean + - name: run unit test + run: | + ./scripts/coverage/vey-dcgen.sh + + - name: Upload coverage data + uses: codecov/codecov-action@v7 + with: + fail_ci_if_error: true + disable_search: true + files: output.lcov + flags: vey-dcgen + name: vey-dcgen-all-test + token: ${{ secrets.CODECOV_TOKEN }} + verbose: false vey-proxy-test: name: vey-proxy test runs-on: ubuntu-26.04 diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml index cc616233e..a898b0ac6 100644 --- a/.github/workflows/sphinx.yml +++ b/.github/workflows/sphinx.yml @@ -21,6 +21,7 @@ jobs: - vey-gateway - vey-keyless - vey-iploc + - vey-dcgen - vey-statsd steps: - uses: actions/checkout@v7 diff --git a/Cargo.lock b/Cargo.lock index 7e46f7bcf..837082238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2894,10 +2894,15 @@ dependencies = [ [[package]] name = "vey-dcgen" -version = "0.9.0" +version = "0.10.0" dependencies = [ "anyhow", + "arc-swap", + "async-trait", + "capnp", + "capnp-rpc", "clap", + "foldhash", "kanal", "log", "snmalloc-rs", @@ -2907,10 +2912,12 @@ dependencies = [ "vey-build-env", "vey-cert-agent", "vey-daemon", + "vey-dcgen-proto", "vey-histogram", "vey-jemalloc", + "vey-macros", "vey-mimalloc", - "vey-socket", + "vey-reuseport", "vey-statsd-client", "vey-std-ext", "vey-tls-cert", @@ -2919,6 +2926,26 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "vey-dcgen-ctl" +version = "0.1.0" +dependencies = [ + "anyhow", + "capnp", + "clap", + "tokio", + "vey-ctl", + "vey-dcgen-proto", +] + +[[package]] +name = "vey-dcgen-proto" +version = "0.1.0" +dependencies = [ + "capnp", + "capnpc", +] + [[package]] name = "vey-dpi" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index cbe3097b2..f9a7d2b74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = [ "vey-bench", "vey-dcgen", + "vey-dcgen/proto", + "vey-dcgen/utils/ctl", "vey-iploc", "vey-iploc/proto", "vey-iploc/utils/ctl", @@ -75,6 +77,8 @@ members = [ default-members = [ "vey-bench", "vey-dcgen", + "vey-dcgen/proto", + "vey-dcgen/utils/ctl", "vey-iploc", "vey-iploc/proto", "vey-iploc/utils/ctl", diff --git a/scripts/coverage/vey-dcgen.sh b/scripts/coverage/vey-dcgen.sh new file mode 100755 index 000000000..fc4704196 --- /dev/null +++ b/scripts/coverage/vey-dcgen.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +set -e + +SCRIPTS_DIR=$(dirname "$0") +PROJECT_DIR=$(realpath "${SCRIPTS_DIR}/../..") + + +TEST_NAME="vey-dcgen-ci" +. "${SCRIPTS_DIR}/enter.sh" + +# build +FEATURES="jemalloc" +if command -v clang >/dev/null 2>&1 && pkg-config --exists libbpf; then + FEATURES="${FEATURES},ebpf" +fi +cargo build --features "${FEATURES}" -p vey-dcgen -p vey-dcgen-ctl + +all_binaries=$(find target/debug/ -maxdepth 1 -type f -perm /111 | awk '{print "-object "$0}') +all_objects=$(find target/debug/deps/ -type f -perm /111 -not -name "*.so" | awk '{print "-object "$0}') + +# run vey-dcgen tests + +cargo test --features "${FEATURES}" -p vey-dcgen -p vey-dcgen-ctl + +RUN_DIR="${PROJECT_DIR}/scripts/testcases/vey-dcgen" +. "${RUN_DIR}/run.sh" + +# get all profraw files generated in each test +profraw_files=$(find . -type f -regex ".*/${TEST_NAME}.*\.profraw") + +# get indexed profile data file +cargo profdata -- merge -o "${PROF_DATA_FILE}" ${profraw_files} + +# report to console + +IGNORE_FLAGS="--ignore-filename-regex=.cargo \ + --ignore-filename-regex=rustc \ + --ignore-filename-regex=target/debug/build \ + --ignore-filename-regex=vey-bench \ + --ignore-filename-regex=vey-iploc \ + --ignore-filename-regex=vey-mkcert \ + --ignore-filename-regex=vey-proxy \ + --ignore-filename-regex=vey-gateway \ + --ignore-filename-regex=vey-keyless \ + --ignore-filename-regex=vey-statsd" + +echo "==== Coverage for all ====" +cargo cov -- report --use-color --instr-profile="${PROF_DATA_FILE}" ${IGNORE_FLAGS} ${all_binaries} ${all_objects} +cargo cov -- export --format=lcov --instr-profile="${PROF_DATA_FILE}" ${IGNORE_FLAGS} ${all_binaries} ${all_objects} > output.lcov diff --git a/scripts/testcases/vey-dcgen/.gitignore b/scripts/testcases/vey-dcgen/.gitignore new file mode 100644 index 000000000..7a60b85e1 --- /dev/null +++ b/scripts/testcases/vey-dcgen/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/scripts/testcases/vey-dcgen/main.yaml b/scripts/testcases/vey-dcgen/main.yaml new file mode 100644 index 000000000..9637783d6 --- /dev/null +++ b/scripts/testcases/vey-dcgen/main.yaml @@ -0,0 +1,25 @@ +--- + +runtime: + task_wait_delay: 0s + task_wait_timeout: 5s + task_quit_timeout: 2s + +worker: + thread_number: 2 + +stat: + target: + udp: 127.0.0.1:8125 + +server: + - name: local + type: udp + listen: 127.0.0.1:2999 + - name: local_unix + type: unix + listen: /tmp/vey-dcgen-ci.sock + +issuer: + ca_certificate: ../../../vey-dcgen/examples/simple/G3-test.crt + ca_private_key: ../../../vey-dcgen/examples/simple/G3-test.key diff --git a/scripts/testcases/vey-dcgen/query.py b/scripts/testcases/vey-dcgen/query.py new file mode 100755 index 000000000..92ff481c1 --- /dev/null +++ b/scripts/testcases/vey-dcgen/query.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +"""Send one certificate-request datagram and wait for a reply. + +The payload is a MessagePack fixstr of the hostname. Exit 0 when a reply +contains a PEM certificate, 2 on timeout. +""" + +import os +import socket +import sys + + +def encode_host(host: str) -> bytes: + raw = host.encode() + if len(raw) > 31: + raise SystemExit(f"host {host} is too long for a msgpack fixstr") + return bytes([0xA0 | len(raw)]) + raw + + +def main() -> None: + if len(sys.argv) != 4: + raise SystemExit("usage: query.py udp|unix TARGET HOST|--invalid") + + kind, target, host = sys.argv[1:] + payload = b"not a request" if host == "--invalid" else encode_host(host) + + if kind == "udp": + host_addr, port = target.rsplit(":", 1) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(2.0) + sock.sendto(payload, (host_addr, int(port))) + elif kind == "unix": + client = target + ".client" + try: + os.unlink(client) + except FileNotFoundError: + pass + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.bind(client) + sock.settimeout(2.0) + try: + sock.sendto(payload, target) + except OSError: + os.unlink(client) + raise + else: + raise SystemExit(f"unknown transport {kind}") + + try: + data, _addr = sock.recvfrom(65535) + except (TimeoutError, socket.timeout): + raise SystemExit(2) + finally: + sock.close() + if kind == "unix": + try: + os.unlink(target + ".client") + except FileNotFoundError: + pass + + if b"BEGIN CERTIFICATE" not in data: + raise SystemExit("reply did not contain a certificate") + + +if __name__ == "__main__": + main() diff --git a/scripts/testcases/vey-dcgen/run.sh b/scripts/testcases/vey-dcgen/run.sh new file mode 100755 index 000000000..01d2f4de7 --- /dev/null +++ b/scripts/testcases/vey-dcgen/run.sh @@ -0,0 +1,109 @@ +# Binaries default to a coverage build. PGO points BIN_DIR at a directory +# that contains the instrumented daemon and the control client. +BIN_DIR=${BIN_DIR:-${PROJECT_DIR}/target/debug} + +UNIX_SOCK=/tmp/vey-dcgen-ci.sock + +cleanup() +{ + if [ -n "${DCGEN_PID:-}" ]; then + kill -INT "$DCGEN_PID" 2>/dev/null || true + wait "$DCGEN_PID" 2>/dev/null || true + fi + rm -f "${UNIX_SOCK}" "${UNIX_SOCK}.client" + if command -v clear_profiles >/dev/null 2>&1; then + clear_profiles + fi + if [ -n "${PGO_BIN_DIR:-}" ]; then + rm -rf "${PGO_BIN_DIR}" + fi + if [ -n "${PGO_RAW_DIR:-}" ]; then + rm -rf "${PGO_RAW_DIR}" + fi +} + +trap cleanup EXIT + +vey_dcgen_ctl() +{ + "${BIN_DIR}"/vey-dcgen-ctl -G ${TEST_NAME} -p $DCGEN_PID "$@" +} + +query_udp() +{ + python3 "${RUN_DIR}/query.py" udp 127.0.0.1:2999 "$1" +} + +query_unix() +{ + python3 "${RUN_DIR}/query.py" unix "${UNIX_SOCK}" "$1" +} + +expect_datagram_miss() +{ + if "$@"; then + echo "expected no datagram reply: $*" >&2 + exit 1 + fi +} + +wait_until_ready() +{ + i=0 + while [ "$i" -lt 25 ]; do + if ! kill -0 "$DCGEN_PID" 2>/dev/null; then + echo "vey-dcgen exited before the control socket was ready" >&2 + wait "$DCGEN_PID" || true + DCGEN_PID= + exit 1 + fi + if vey_dcgen_ctl version >/dev/null; then + return + fi + i=$((i + 1)) + sleep 0.2 + done + echo "timed out waiting for vey-dcgen" >&2 + exit 1 +} + +rm -f "${UNIX_SOCK}" "${UNIX_SOCK}.client" + +set -x + +"${BIN_DIR}"/vey-dcgen -Vvv + +for dir in $(ls "${PROJECT_DIR}/vey-dcgen/examples") +do + example_dir="${PROJECT_DIR}/vey-dcgen/examples/${dir}" + [ -d "${example_dir}" ] || continue + + "${BIN_DIR}"/vey-dcgen -c "${example_dir}" -t +done + +"${BIN_DIR}"/vey-dcgen -c "${RUN_DIR}" -t + +"${BIN_DIR}"/vey-dcgen -c "${RUN_DIR}" -G ${TEST_NAME} & +DCGEN_PID=$! + +wait_until_ready + +query_udp www.example.com +query_unix www.example.com +expect_datagram_miss query_udp --invalid +expect_datagram_miss query_unix --invalid + +vey_dcgen_ctl reload +query_udp www.example.com + +vey_dcgen_ctl reload-issuer +query_udp www.example.com + +# Let one stats emit interval pass before shutdown. +sleep 0.5 + +vey_dcgen_ctl offline +wait "$DCGEN_PID" +DCGEN_PID= + +set +x diff --git a/scripts/testcases/vey-proxy/vey-dcgen.yaml b/scripts/testcases/vey-proxy/vey-dcgen.yaml index 6c534d6fd..07df67b28 100644 --- a/scripts/testcases/vey-proxy/vey-dcgen.yaml +++ b/scripts/testcases/vey-proxy/vey-dcgen.yaml @@ -3,10 +3,15 @@ runtime: thread_number: 1 +server: + - name: local + type: udp + listen: 127.0.0.1:2999 + stat: target: udp: 127.0.0.1:8125 -backend: +issuer: ca_certificate: rootCA.pem ca_private_key: rootCA-key.pem diff --git a/sphinx/conf.py b/sphinx/conf.py index c073c1431..bb25cf88e 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -25,6 +25,9 @@ "iploc": { "path": "vey-iploc", }, + "dcgen": { + "path": "vey-dcgen", + }, "values": { "path": "vey-values", }, diff --git a/sphinx/project/index.rst b/sphinx/project/index.rst index 76dbff0c8..89c7e7d24 100644 --- a/sphinx/project/index.rst +++ b/sphinx/project/index.rst @@ -45,6 +45,10 @@ Follow the links below for the documentation of each application: An IP location lookup service used by ``vey-proxy`` GeoIP routing. +* `vey-dcgen`_ + + A dynamic certificate generator for ``vey-proxy`` TLS interception. + Shared Reference ================ @@ -58,4 +62,5 @@ Those are documented in the common `vey-values`_ reference. .. _vey-gateway: /projects/gateway/en/latest/ .. _vey-keyless: /projects/keyless/en/latest/ .. _vey-iploc: /projects/iploc/en/latest/ +.. _vey-dcgen: /projects/dcgen/en/latest/ .. _vey-values: /projects/values/en/latest/ diff --git a/sphinx/vey-dcgen/.gitignore b/sphinx/vey-dcgen/.gitignore new file mode 100644 index 000000000..e35d8850c --- /dev/null +++ b/sphinx/vey-dcgen/.gitignore @@ -0,0 +1 @@ +_build diff --git a/sphinx/vey-dcgen/Makefile b/sphinx/vey-dcgen/Makefile new file mode 100644 index 000000000..6b983ba2e --- /dev/null +++ b/sphinx/vey-dcgen/Makefile @@ -0,0 +1,21 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(MAKE) -C ../vey-values $@ + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/sphinx/vey-dcgen/README.md b/sphinx/vey-dcgen/README.md new file mode 100644 index 000000000..8b3b23cfd --- /dev/null +++ b/sphinx/vey-dcgen/README.md @@ -0,0 +1,7 @@ +This directory contains the sphinx docs about the configuration and metrics for vey-dcgen. + +To view the docs: + +1. [install sphinx](https://www.sphinx-doc.org/en/master/usage/installation.html) +2. run `make html` in this directory +3. open *_build/html/index.html* with your favourite browser diff --git a/sphinx/vey-dcgen/conf.py b/sphinx/vey-dcgen/conf.py new file mode 100644 index 000000000..3ddc395c8 --- /dev/null +++ b/sphinx/vey-dcgen/conf.py @@ -0,0 +1,37 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +project = 'vey-dcgen' +copyright = '2025 - %Y, Zhang Jingqiang' +author = 'Zhang Jingqiang' +release = '0.10.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.intersphinx", +] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# -- Custom Options ---------------------------------------------------------- + +# Set the master document, which contains the root toctree directive. +# The default changed from 'contents' to 'index' from sphinx version 2.0, +# so we need to explicitly set it in order to be compatible with old versions. +master_doc = 'index' + +intersphinx_mapping = { + "values": ("https://vey.readthedocs.io/projects/values/en/latest/", + ('../vey-values/_build/html/objects.inv', None)), +} diff --git a/sphinx/vey-dcgen/configuration/index.rst b/sphinx/vey-dcgen/configuration/index.rst new file mode 100644 index 000000000..16b9786f9 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/index.rst @@ -0,0 +1,97 @@ +.. _configuration: + +############# +Configuration +############# + +``vey-dcgen`` uses YAML for configuration. + +The main configuration file is specified with the ``-c`` command-line option. +If a directory is set, the main configuration file is loaded automatically from +it based on the executable binary name. +Its top-level keys are listed below. + +At a high level, ``vey-dcgen`` is organized around a few main object types: + +* ``server`` accepts certificate-request datagrams on UDP or a Unix datagram socket +* ``issuer`` names the CA certificate and private key used to issue certificates + +Process-level settings such as ``runtime``, ``worker``, ``stat``, and +``controller`` control how the daemon runs and how it reports operational +state. + +.. list-table:: + :header-rows: 1 + + * - Key + - Type + - Reload + - Description + * - runtime + - Map + - no + - Runtime configuration, see :doc:`runtime` + * - worker + - Map [#w]_ + - no + - Starts unaided worker runtimes if present, see :doc:`runtime` + * - stat + - Map + - no + - Metrics configuration, see :doc:`stat` + * - controller + - Map + - no + - Local control socket timeouts. The optional ``local`` map accepts ``recv_timeout`` and ``send_timeout`` in seconds. Defaults are 30 and 1 + * - server + - Mix [#m]_ + - yes + - Listen sockets, see :doc:`servers/index`. At least one server is required at startup + * - issuer + - Map + - yes + - CA certificate and private key used to issue certificates. Required at startup. ``duration_stats`` is read only at startup + +Startup fails when ``server`` is missing or empty, or when ``issuer`` does +not name a CA certificate and private key. + +.. _configuration_reload: + +Reload +====== + +``runtime``, ``worker``, ``stat``, and ``controller`` are read only at startup. +``issuer.duration_stats`` is also read only at startup. + +``vey-dcgen-ctl reload`` re-reads ``server`` and ``issuer`` from the config +file. A new or changed server is started, and a removed server is stopped. An +unchanged server keeps running. Changing ``type`` for an existing name +replaces that server. Changing ``listen`` on the same type restarts the +listener and keeps its statistics. + +The reloaded issuer replaces the CA certificate, private key, and the +``no_append_ca_cert``, ``keep_serial``, and ``max_ttl`` settings. The files +are read completely before the running CA is replaced. A request already +being signed keeps the previous CA. Omitting ``issuer`` leaves the running +CA unchanged. An issuer that fails to load leaves the running CA unchanged +and aborts the reload. + +``vey-dcgen-ctl reload-issuer`` re-reads only ``issuer``. Listeners keep +running. The same swap rules apply: the files are read completely before the +running CA is replaced, an in-flight signing keeps the previous CA, an omitted +``issuer`` leaves the running CA unchanged, and a failed load aborts the +reload. + +.. rubric:: Footnotes + +.. [#m] See :external+values:ref:`hybrid map ` for the actual format. +.. [#w] See :external+values:ref:`unaided runtime config `. + +.. toctree:: + :hidden: + + runtime + stat + servers/index + issuer + values diff --git a/sphinx/vey-dcgen/configuration/issuer.rst b/sphinx/vey-dcgen/configuration/issuer.rst new file mode 100644 index 000000000..c7be301d5 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/issuer.rst @@ -0,0 +1,63 @@ +.. _configuration_issuer: + +****** +Issuer +****** + +**required**, **type**: map + +The certificate issuer. The private CA named here signs certificates. +``reload`` and ``reload-issuer`` re-read this section and swap the CA after +the files load. ``reload-issuer`` leaves listeners running. +``duration_stats`` stays as it was at startup. + +ca_certificate +============== + +**required**, **type**: file path + +CA certificate chain, in a PEM file. The path may be absolute or relative to +the config directory. The first certificate signs issued certificates. The +encoded chain is appended to each issued certificate unless +``no_append_ca_cert`` is true. + +ca_private_key +============== + +**required**, **type**: file path + +Private key of the signing certificate, in a PEM file. The path may be +absolute or relative to the config directory. + +no_append_ca_cert +================= + +**optional**, **type**: bool + +Do not append the CA certificate chain to issued certificates. Default is +false. + +keep_serial +=========== + +**optional**, **type**: bool + +Keep the serial number when mimicking an upstream certificate. Default is +false. + +max_ttl +======= + +**optional**, **type**: i32 + +Upper bound, in seconds, on the TTL reported to ``vey-proxy``. Values below +300 are raised to 300. Default is 86400. + +duration_stats +============== + +**optional**, **type**: :external+values:ref:`histogram metrics ` + +Histogram of the time from receiving a request datagram to writing the +response. ``duration_metrics`` is an alias. This key is applied only at +startup. A later reload keeps the histogram that is already running. diff --git a/sphinx/vey-dcgen/configuration/runtime.rst b/sphinx/vey-dcgen/configuration/runtime.rst new file mode 100644 index 000000000..5c089f519 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/runtime.rst @@ -0,0 +1,30 @@ +.. _configuration_runtime: + +******* +Runtime +******* + +The optional ``runtime`` section in ``vey-dcgen`` uses the shared +:external+values:ref:`daemon runtime config ` +value type. + +The main runtime stays single-threaded. ``vey-dcgen`` sets the default +``thread_number`` to ``0``, which selects a current-thread runtime, unless the +config sets another value. + +If the optional ``worker`` section is present, it uses the shared +:external+values:ref:`unaided runtime config ` +value type. + +``thread_number`` in that section is the worker count. Leave +``thread_number_per_runtime`` at its default of 1 so each worker runtime has +one thread. + +Certificate generation runs on those workers. UDP servers place one reuseport +socket on each worker when at least one worker is running. Without workers, +each UDP server binds its sockets on the main runtime, and certificate +generation also runs there. Unix servers always run on the main runtime. + +See :external+values:doc:`configuration/values/runtime` for the full runtime +reference, including worker-thread settings, CPU-affinity-related value types, +and graceful-shutdown timers. diff --git a/sphinx/vey-dcgen/configuration/servers/dummy.rst b/sphinx/vey-dcgen/configuration/servers/dummy.rst new file mode 100644 index 000000000..7fa35f86d --- /dev/null +++ b/sphinx/vey-dcgen/configuration/servers/dummy.rst @@ -0,0 +1,17 @@ +.. _configuration_server_dummy: + +***** +dummy +***** + +This server has a name and does not open a listen socket. It does not receive +requests. + +.. versionadded:: 0.10.0 + +The following common keys are supported: + +* :ref:`name ` +* :ref:`type ` + +This server type has no server-specific configuration keys. diff --git a/sphinx/vey-dcgen/configuration/servers/index.rst b/sphinx/vey-dcgen/configuration/servers/index.rst new file mode 100644 index 000000000..73cf798c4 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/servers/index.rst @@ -0,0 +1,81 @@ +.. _configuration_server: + +****** +Server +****** + +.. versionadded:: 0.10.0 + +Each server definition is a map with two required keys: + +* :ref:`name `, which sets the server name +* :ref:`type `, which selects the concrete server type + and therefore the remaining valid keys + +The supported server types are: + +* :ref:`udp `, which listens on one UDP address +* :ref:`unix `, which listens on one Unix datagram + socket path +* :ref:`dummy `, which does not listen + +At least one server is required at startup. ``server`` can be reloaded. A new +or changed server is started, a removed server is stopped, and an unchanged +server keeps running. Changing ``type`` for an existing name replaces that +server. Changing ``listen`` on the same type restarts the listener and keeps +its statistics. + +The main runtime stays single-threaded. See :doc:`../runtime` for how UDP +sockets are placed on workers. + +Example:: + + server: + - name: local + type: udp + listen: 127.0.0.1:2999 + - name: public + type: udp + listen: "[::]:2999" + - name: local_unix + type: unix + listen: /run/vey-dcgen/dcgen.sock + +Servers +======= + +.. toctree:: + :maxdepth: 2 + + udp + unix + dummy + +Common Keys +=========== + +This section describes keys shared by server types. + +.. _conf_server_common_name: + +name +---- + +**required**, **type**: :external+values:ref:`metric node name ` + +Set the server name. This name is the ``server`` tag on the server's metrics. + +.. _conf_server_common_type: + +type +---- + +**required**, **type**: str + +Set the server type. + +The supported values are: + +* ``udp`` +* ``unix``, which is rejected on non-Unix platforms +* ``dummy`` diff --git a/sphinx/vey-dcgen/configuration/servers/udp.rst b/sphinx/vey-dcgen/configuration/servers/udp.rst new file mode 100644 index 000000000..8347daeeb --- /dev/null +++ b/sphinx/vey-dcgen/configuration/servers/udp.rst @@ -0,0 +1,38 @@ +.. _configuration_server_udp: + +*** +udp +*** + +This server answers certificate requests on one UDP address. + +When at least one worker is running, the server binds one reuseport socket on +each worker and ignores ``listen.instance``. Without workers, it binds the +configured instance count on the main runtime. The default instance count is 1. + +A datagram larger than 16384 bytes is truncated. + +On Linux, a build with the ``ebpf`` feature attaches the UDP reuseport selector +used for a lossless upgrade. The selector is attached when ``use_ebpf`` is +true, or when ``use_ebpf`` is omitted and the process uid is 0. A non-root +process does not attach it unless ``use_ebpf`` is set. If attachment fails and +``fail_on_ebpf_error`` is false, the server logs a warning and continues +without the selector. + +.. versionadded:: 0.10.0 + +The following common keys are supported: + +* :ref:`name ` +* :ref:`type ` + +listen +====== + +**required**, **type**: :external+values:ref:`udp listen ` + +Set the listening socket for this server. + +The value may be a port, a socket address string, or the full listen map. +``-G`` names the daemon group and its control socket. It does not select this +address. diff --git a/sphinx/vey-dcgen/configuration/servers/unix.rst b/sphinx/vey-dcgen/configuration/servers/unix.rst new file mode 100644 index 000000000..d31914027 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/servers/unix.rst @@ -0,0 +1,27 @@ +.. _configuration_server_unix: + +**** +unix +**** + +This server answers certificate requests on one Unix datagram socket. + +The listener always runs on the main runtime. It is available only on Unix. +On other platforms a server of this type is rejected when the config is loaded. + +.. versionadded:: 0.10.0 + +The following common keys are supported: + +* :ref:`name ` +* :ref:`type ` + +listen +====== + +**required**, **type**: :external+values:ref:`absolute path ` + +Set the datagram socket path. A relative path is rejected. + +The path is the socket address this process binds. It does not have to exist +before startup. The parent directory must exist. diff --git a/sphinx/vey-dcgen/configuration/stat.rst b/sphinx/vey-dcgen/configuration/stat.rst new file mode 100644 index 000000000..d39225d98 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/stat.rst @@ -0,0 +1,12 @@ +.. _configuration_stat: + +**** +Stat +**** + +This section describes metrics-export configuration. It is optional and cannot +be reloaded. If set, it must reside in the main configuration file. + +The value type is :external+values:ref:`statsd client config `, +with the default ``prefix`` set to the executable name. A binary named +``vey-dcgen`` therefore uses the prefix ``vey-dcgen``. diff --git a/sphinx/vey-dcgen/configuration/values.rst b/sphinx/vey-dcgen/configuration/values.rst new file mode 100644 index 000000000..8664be830 --- /dev/null +++ b/sphinx/vey-dcgen/configuration/values.rst @@ -0,0 +1,12 @@ +****************** +Common Value Types +****************** + +The reusable configuration value types referenced by ``vey-dcgen`` are +documented in the shared ``vey-values`` Sphinx project. + +See :external+values:doc:`configuration/values/index` for the common value +reference. + +The shared value reference also includes availability notes so you can see +which common value types are supported by ``vey-dcgen``. diff --git a/sphinx/vey-dcgen/index.rst b/sphinx/vey-dcgen/index.rst new file mode 100644 index 000000000..3d89bf8d1 --- /dev/null +++ b/sphinx/vey-dcgen/index.rst @@ -0,0 +1,36 @@ +######################## +``vey-dcgen`` Reference +######################## + +``vey-dcgen`` issues per-host certificates for ``vey-proxy`` TLS interception. +``vey-proxy`` sends certificate requests to it over UDP or a Unix datagram +socket. The request protocol is the `cert generator helper protocol`_. + +A typical deployment runs ``vey-dcgen`` on the same host as ``vey-proxy``. +Each listen socket is a ``server`` entry. Certificates are signed by the +certificate authority named in ``issuer``. + +The core configuration model is organized around a few main object types: + +* ``server`` accepts certificate-request datagrams +* ``issuer`` names the CA certificate and private key +* ``runtime``, ``worker``, ``stat``, and ``controller`` control how the daemon + runs and how it reports operational state + +This reference is organized into two sections: + +* :doc:`configuration/index` documents all configuration objects and their + relationships +* :doc:`metrics/index` documents the exported StatsD metrics + +If you are setting up ``vey-dcgen`` for the first time, start with the +configuration reference, then review the metrics pages for operational +visibility. + +.. _cert generator helper protocol: https://vey.readthedocs.io/projects/proxy/en/latest/protocol/helper/cert_generator.html + +.. toctree:: + :maxdepth: 1 + + Configuration Reference + Metrics Definition diff --git a/sphinx/vey-dcgen/make.bat b/sphinx/vey-dcgen/make.bat new file mode 100644 index 000000000..954237b9b --- /dev/null +++ b/sphinx/vey-dcgen/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/sphinx/vey-dcgen/metrics/index.rst b/sphinx/vey-dcgen/metrics/index.rst new file mode 100644 index 000000000..13b39c1a0 --- /dev/null +++ b/sphinx/vey-dcgen/metrics/index.rst @@ -0,0 +1,58 @@ +.. _metrics: + +####### +Metrics +####### + +Metrics are currently exported through StatsD. See :ref:`stat ` +for configuration details. + +The default metric prefix is the executable name. + +Common Tags +=========== + +.. _metrics_tag_daemon_group: + +* daemon_group + + Daemon group configured in the config file or with ``-G``. Present on every + metric. + +.. _metrics_tag_stat_id: + +* stat_id + + A machine-local unique ``stat_id`` used for deduplication. It should be + **dropped** by the StatsD aggregation pipeline so that metrics with the same + remaining tags can be aggregated. + + This tag is present on listen metrics and Tokio runtime metrics. It is not + present on server request metrics, issuer metrics, or allocator metrics. + +.. _metrics_tag_server: + +* server + + Server name. Present on server request metrics and listen metrics. + +.. _metrics_tag_online: + +* online + + Whether the server is online. The value is either ``y`` or ``n``. Present on + listen metrics. + +.. _metrics_tag_quantile: + +* quantile + + Histogram quantile. Present on ``issuer.request_duration``. + +Metric Types +============ + +.. toctree:: + + server + runtime diff --git a/sphinx/vey-dcgen/metrics/runtime.rst b/sphinx/vey-dcgen/metrics/runtime.rst new file mode 100644 index 000000000..33970d7b9 --- /dev/null +++ b/sphinx/vey-dcgen/metrics/runtime.rst @@ -0,0 +1,107 @@ +.. _metrics_runtime: + +############### +Runtime Metrics +############### + +Runtime metrics are available for runtime implementations that expose metric +reporting. Allocator metrics are emitted only when the binary is built with +that allocator. + +The following tag is present on all runtime metrics: + +* :ref:`daemon_group ` + +The following tags are also present on Tokio runtime metrics: + +* :ref:`stat_id ` + +* runtime_id + + The runtime ID or label. + + There may be multiple instances of the same runtime type. This tag + distinguishes between them. + +.. _metrics_runtime_jemalloc: + +Jemalloc Runtime Metrics +======================== + +These metrics come from the jemalloc memory allocator. + +* runtime.jemalloc.approximate_active + + **type**: gauge + + The total number of bytes in active pages collected in an unsynchronized + manner, without requiring an epoch update. + +.. _metrics_runtime_mimalloc: + +Mimalloc Runtime Metrics +======================== + +These metrics come from the mimalloc memory allocator. + +* runtime.mimalloc.current_pages + + **type**: gauge + + Current count of mimalloc pages. + +* runtime.mimalloc.peak_pages + + **type**: gauge + + Peak count of mimalloc pages. + +* runtime.mimalloc.current_commit + + **type**: gauge + + Current committed memory (backed by the page file). + +* runtime.mimalloc.peak_commit + + **type**: gauge + + Peak committed memory (backed by the page file). + +.. _metrics_runtime_snmalloc: + +snmalloc Runtime Metrics +======================== + +These metrics come from the snmalloc memory allocator. + +* runtime.snmalloc.current_memory_usage + + **type**: gauge + + Bytes currently reserved from the OS. + +* runtime.snmalloc.peak_memory_usage + + **type**: gauge + + High-water mark of current memory usage. + +.. _metrics_runtime_tokio: + +Tokio Runtime Metrics +===================== + +These metrics come from the Tokio runtime. + +* runtime.tokio.alive_tasks + + **type**: gauge + + Current number of live tasks in the runtime. + +* runtime.tokio.global_queue_depth + + **type**: gauge + + Number of tasks currently scheduled in the runtime's global queue. diff --git a/sphinx/vey-dcgen/metrics/server.rst b/sphinx/vey-dcgen/metrics/server.rst new file mode 100644 index 000000000..ea6ae6bc8 --- /dev/null +++ b/sphinx/vey-dcgen/metrics/server.rst @@ -0,0 +1,153 @@ +.. _metrics_server: + +############## +Server Metrics +############## + +Server metrics describe certificate requests and listen sockets for each +configured server. Issuer metrics describe the certificate generator itself +and are not tagged with a server name. + +Request +======= + +The following tags are present: + +* :ref:`daemon_group ` +* :ref:`server ` + +The metric names are: + +* server.request_total + + **type**: count + + Number of datagrams delivered to this server. This includes invalid requests. + +* server.request_invalid + + **type**: count + + Number of datagrams that were not a valid certificate request. No response + is sent. + +* server.request_alive + + **type**: gauge + + Number of requests currently being handled by this server, including time + spent waiting for certificate generation. + +* server.response_total + + **type**: count + + Number of response datagrams encoded for a client. An encoding error is + logged and is not counted here. A failed certificate generation is logged + and is not counted here. + +* server.response_fail + + **type**: count + + Number of encoded responses that could not be written back to the client. + +Issuer +====== + +The following tag is present: + +* :ref:`daemon_group ` + +The metric names are: + +* issuer.refresh_total + + **type**: count + + Number of attempts to refresh the certificate builder clock and key. + +* issuer.refresh_ok + + **type**: count + + Number of those refresh attempts that succeeded. + +* issuer.request_total + + **type**: count + + Number of certificate generations started. + +* issuer.request_ok + + **type**: count + + Number of certificate generations that produced a certificate and key. + +* issuer.request_duration + + **type**: gauge + + Time from receiving a request datagram to writing the response, in + nanoseconds. The :ref:`quantile ` tag selects the + histogram bucket. + +Listen +====== + +The following tags are present: + +* :ref:`daemon_group ` +* :ref:`stat_id ` +* :ref:`server ` +* :ref:`online ` + +The metric names are: + +* listen.instance.count + + **type**: gauge + + Number of listening sockets currently running for this server. A dummy server + stays at zero. + +Datagram listeners also report the counters below. A counter that has stayed at +zero is not emitted. + +* listen.datagram.recv.total + + **type**: count + + Number of datagrams read from the listen socket. + +* listen.datagram.recv.dropped + + **type**: count + + Number of received datagrams discarded before handling. A datagram is dropped + when it has no source address. + +* listen.datagram.recv.ok + + **type**: count + + Number of received datagrams delivered to the server. + +* listen.datagram.send.queued + + **type**: count + + Number of datagrams queued for sending on the listen socket. + +* listen.datagram.send.failed + + **type**: count + + Number of queued datagrams the listen socket failed to send. + +* listen.datagram.send.ok + + **type**: count + + Number of datagrams written to the listen socket. diff --git a/sphinx/vey-dcgen/requirements.txt b/sphinx/vey-dcgen/requirements.txt new file mode 100644 index 000000000..b3ae47e7e --- /dev/null +++ b/sphinx/vey-dcgen/requirements.txt @@ -0,0 +1,2 @@ +sphinx == 8.2.3 +sphinx-rtd-theme diff --git a/sphinx/vey-values/configuration/values/fs.rst b/sphinx/vey-values/configuration/values/fs.rst index 4a864686d..495bb74bc 100644 --- a/sphinx/vey-values/configuration/values/fs.rst +++ b/sphinx/vey-values/configuration/values/fs.rst @@ -12,6 +12,7 @@ Filesystem - ``vey-keyless``: available - ``vey-statsd``: available - ``vey-iploc``: available + - ``vey-dcgen``: available .. _conf_value_hybrid_map: diff --git a/sphinx/vey-values/configuration/values/index.rst b/sphinx/vey-values/configuration/values/index.rst index cfc261b93..e6e214b63 100644 --- a/sphinx/vey-values/configuration/values/index.rst +++ b/sphinx/vey-values/configuration/values/index.rst @@ -14,6 +14,7 @@ sub-structures in one place. - ``vey-keyless``: uses this shared reference from ``0.5.0`` onward - ``vey-statsd``: uses this shared reference from ``0.2.0`` onward - ``vey-iploc``: uses this shared reference from ``0.5.0`` onward + - ``vey-dcgen``: uses this shared reference from ``0.10.0`` onward Each value-family page below includes a page-level availability summary. Where an individual shared value is only available in a subset of applications, or diff --git a/sphinx/vey-values/configuration/values/metrics.rst b/sphinx/vey-values/configuration/values/metrics.rst index 56e2f8900..6ffa2b84d 100644 --- a/sphinx/vey-values/configuration/values/metrics.rst +++ b/sphinx/vey-values/configuration/values/metrics.rst @@ -11,6 +11,7 @@ Metrics - ``vey-keyless``: available - ``vey-statsd``: available - ``vey-iploc``: available + - ``vey-dcgen``: available .. _conf_value_metric_value: diff --git a/sphinx/vey-values/configuration/values/network.rst b/sphinx/vey-values/configuration/values/network.rst index bf5f3b904..dcf756d07 100644 --- a/sphinx/vey-values/configuration/values/network.rst +++ b/sphinx/vey-values/configuration/values/network.rst @@ -11,6 +11,7 @@ Network - ``vey-keyless``: available - ``vey-statsd``: available - ``vey-iploc``: available + - ``vey-dcgen``: available .. _conf_value_sockaddr_str: diff --git a/sphinx/vey-values/configuration/values/runtime.rst b/sphinx/vey-values/configuration/values/runtime.rst index b24a8818a..203111e94 100644 --- a/sphinx/vey-values/configuration/values/runtime.rst +++ b/sphinx/vey-values/configuration/values/runtime.rst @@ -11,6 +11,7 @@ Runtime - ``vey-keyless``: available - ``vey-statsd``: available - ``vey-iploc``: available + - ``vey-dcgen``: available .. _conf_value_cpu_id_list_str: diff --git a/vey-dcgen/CHANGELOG b/vey-dcgen/CHANGELOG index 751869265..7acfd60b8 100644 --- a/vey-dcgen/CHANGELOG +++ b/vey-dcgen/CHANGELOG @@ -1,5 +1,9 @@ -v0.9.1: +v0.10.0: + - Breaking: listen sockets are server entries. `-G port` and `UDP_LISTEN_ADDR` no longer select the listen address. Configure `server` in YAML. `vey-dcgen-ctl` reloads servers and the issuer, and requests offline shutdown + - Breaking: the CA config section is `issuer`. The key `backend` is no longer accepted + - Feature: `vey-dcgen-ctl reload-issuer` re-reads `issuer` and swaps the running CA without restarting listeners + - Breaking: certificate generator metrics use the `issuer` prefix. `backend.refresh_total`, `backend.refresh_ok`, `backend.request_total`, `backend.request_ok`, and `backend.request_duration` are now `issuer.refresh_total`, `issuer.refresh_ok`, `issuer.request_total`, `issuer.request_ok`, and `issuer.request_duration` - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - Breaking: the default control socket directory is `/run/vey-dcgen` for root on Linux and `/var/run/vey-dcgen` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-dcgen` - BUG FIX: a large Linux journald record can be sealed and sent diff --git a/vey-dcgen/Cargo.toml b/vey-dcgen/Cargo.toml index 0bdcb9bd5..12c41efa8 100644 --- a/vey-dcgen/Cargo.toml +++ b/vey-dcgen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vey-dcgen" -version = "0.9.0" +version = "0.10.0" license.workspace = true edition.workspace = true rust-version.workspace = true @@ -10,11 +10,16 @@ description = "Dynamic certificate generator for TLS interception" [dependencies] anyhow.workspace = true +arc-swap.workspace = true +async-trait.workspace = true +capnp.workspace = true +capnp-rpc.workspace = true clap.workspace = true +foldhash.workspace = true log = { workspace = true, features = ["max_level_trace", "release_max_level_debug"] } openssl.workspace = true openssl-sys.workspace = true # for openssl variant detection -tokio = { workspace = true, features = ["macros", "net", "io-util", "time", "signal"] } +tokio = { workspace = true, features = ["macros", "net", "io-util", "rt", "sync", "time", "signal"] } kanal = { workspace = true, features = ["async"] } yaml-rust.workspace = true snmalloc = { workspace = true, optional = true } @@ -22,11 +27,13 @@ vey-std-ext.workspace = true vey-types.workspace = true vey-yaml = { workspace = true, features = ["histogram", "openssl"] } vey-daemon.workspace = true +vey-macros.workspace = true vey-statsd-client.workspace = true vey-histogram.workspace = true vey-tls-cert.workspace = true vey-cert-agent.workspace = true -vey-socket.workspace = true +vey-dcgen-proto = { path = "proto" } +vey-reuseport = { workspace = true, optional = true } vey-jemalloc = { workspace = true, optional = true } vey-mimalloc = { workspace = true, optional = true } @@ -41,6 +48,9 @@ vendored-mimalloc = ["mimalloc", "vey-mimalloc?/vendored"] secure-vendored-mimalloc = ["mimalloc", "vey-mimalloc?/secure"] snmalloc = ["dep:snmalloc", "vey-daemon/snmalloc"] secure-snmalloc = ["snmalloc", "snmalloc?/check"] +ebpf = ["vey-daemon/ebpf"] +vendored-libbpf = ["ebpf", "vey-reuseport/vendored-libbpf"] +vendored-ebpf = ["ebpf", "vey-reuseport/vendored"] vendored-openssl = ["openssl/vendored"] vendored-tongsuo = ["openssl/tongsuo", "vey-cert-agent/tongsuo"] vendored-boringssl = ["openssl/boringssl"] diff --git a/vey-dcgen/README.md b/vey-dcgen/README.md index e49b2ec85..1f997003f 100644 --- a/vey-dcgen/README.md +++ b/vey-dcgen/README.md @@ -35,37 +35,19 @@ How to run the published images is described in [docker/README.md](docker/README See this [simple example](examples/simple). -You can run `cargo run --bin vey-dcgen -- -c vey-dcgen/examples/simple/ -G port2999 -vv` to start it. +You can run `cargo run --bin vey-dcgen -- -c vey-dcgen/examples/simple/ -G example -vv` to start it. +The example listens on `127.0.0.1:2999`, which is the default connect address in vey-proxy. -### Set UDP listen address +### Listen sockets -The default UDP listen address is *127.0.0.1:2999*, which is also the same default connect address in vey-proxy. +Each listen socket is a `server` entry in the config file. A UDP server sets `listen` to an address. +A Unix datagram server sets `listen` to an absolute socket path. At least one server is required. -There are two ways to change the UDP listen port: +`-G` / `--group-name` sets the instance group and the control socket name. It does not select the listen address. -- Via command line options - - You can set **-G port** or **--group-name port** to change the UDP listen port, - the final listen address will be **[::]:**. - - You can set the systemd instance name to *port*, so when you run `systemctl start vey-dcgen@port`, - it will listen to the correct port automatically. - -- Via environment variables - - You can use the environment variable **UDP_LISTEN_ADDR** to change the UDP listen address. - You can add this environment variable to `/etc/vey-dcgen//env` file to use this with - systemd managed vey-dcgen service. - -### Hot Restart - -It is not possible to do hot restart gracefully without using two ports. - -If vey-dcgen is running at port 3000, and vey-proxy is also using port 3000, the steps are: - -1. start a new vey-dcgen service at port 3001 with the new config -2. reload vey-proxy to use vey-dcgen port 3001 -3. stop vey-dcgen running at port 3000 +`vey-dcgen-ctl -G reload` re-reads `server` and `issuer`. Listeners apply the difference in place. The CA certificate, private key, `keep_serial`, and `max_ttl` are swapped after those files load. A request already being signed keeps the previous CA. `duration_stats` stays as it was at startup. +`vey-dcgen-ctl -G reload-issuer` re-reads only `issuer` and swaps the running CA. Listeners keep running. +`vey-dcgen-ctl -G offline` stops listeners and lets in-flight requests finish. ## Command line options diff --git a/vey-dcgen/debian/changelog b/vey-dcgen/debian/changelog index ceed73fb3..280cd0dcf 100644 --- a/vey-dcgen/debian/changelog +++ b/vey-dcgen/debian/changelog @@ -1,5 +1,10 @@ -vey-dcgen (0.9.0-1) UNRELEASED; urgency=medium +vey-dcgen (0.10.0-1) UNRELEASED; urgency=medium * New upstream release. + * Listen sockets are server entries. UDP reuseport workers, Unix + datagrams, and the issuer can be reloaded in place. The CA config + section is issuer. Certificate generator metrics use the issuer + prefix. vey-dcgen-ctl reload-issuer reloads only the issuer. + vey-dcgen-ctl stops and reloads the daemon. - -- VEY-OSS Developers Sun, 26 Jul 2026 22:39:16 +0800 + -- VEY-OSS Developers Sun, 01 Nov 2026 00:33:00 +0800 diff --git a/vey-dcgen/debian/control b/vey-dcgen/debian/control index 7f8a51651..92671cc04 100644 --- a/vey-dcgen/debian/control +++ b/vey-dcgen/debian/control @@ -2,7 +2,8 @@ Source: vey-dcgen Section: net Priority: optional Maintainer: VEY-OSS Developers -Build-Depends: debhelper-compat (= 13), pkg-config, cmake, g++, libssl-dev +Build-Depends: debhelper-compat (= 13), pkg-config, capnproto, cmake, g++, + libssl-dev, clang, libbpf-dev Standards-Version: 4.6.0 Homepage: https://github.com/VEY-OSS/vey diff --git a/vey-dcgen/debian/rules b/vey-dcgen/debian/rules index deec8423c..036475257 100755 --- a/vey-dcgen/debian/rules +++ b/vey-dcgen/debian/rules @@ -4,6 +4,7 @@ PACKAGE_NAME := vey-dcgen BUILD_PROFILE := release-lto DEB_VERSION ?= $(shell dpkg-parsechangelog -SVersion) +EBPF_FEATURE ?= $(shell scripts/package/detect_ebpf_feature.sh) %: dh $@ @@ -18,11 +19,12 @@ override_dh_auto_build: VEY_PACKAGE_VERSION=$(DEB_VERSION) \ VEY_PGO_PROFILE='$(VEY_PGO_PROFILE)' \ scripts/package/cargo_with_pgo.sh build --frozen --offline --profile $(BUILD_PROFILE) \ - --no-default-features --features secure-snmalloc \ - --package vey-dcgen + --no-default-features --features secure-snmalloc,$(EBPF_FEATURE) \ + --package vey-dcgen --package vey-dcgen-ctl override_dh_auto_install: install -m 755 -D target/$(BUILD_PROFILE)/vey-dcgen debian/tmp/usr/bin/vey-dcgen + install -m 755 -D target/$(BUILD_PROFILE)/vey-dcgen-ctl debian/tmp/usr/bin/vey-dcgen-ctl override_dh_installchangelogs: dh_installchangelogs $(PACKAGE_NAME)/CHANGELOG diff --git a/vey-dcgen/debian/vey-dcgen.install b/vey-dcgen/debian/vey-dcgen.install index 0f3963d08..df283509d 100644 --- a/vey-dcgen/debian/vey-dcgen.install +++ b/vey-dcgen/debian/vey-dcgen.install @@ -1 +1,2 @@ usr/bin/vey-dcgen +usr/bin/vey-dcgen-ctl diff --git a/vey-dcgen/debian/vey-dcgen@.service b/vey-dcgen/debian/vey-dcgen@.service index caf8c6f7b..1ee9fbfce 100644 --- a/vey-dcgen/debian/vey-dcgen@.service +++ b/vey-dcgen/debian/vey-dcgen@.service @@ -1,11 +1,21 @@ [Unit] Description=Dynamic Certificate Generator at %i +After=syslog.target network-online.target +Wants=network-online.target [Service] Type=exec +RuntimeDirectory=vey-dcgen +RuntimeDirectoryPreserve=yes EnvironmentFile=-/etc/vey-dcgen/%i/env -ExecStart=/usr/bin/vey-dcgen -c /etc/vey-dcgen/%i/ -s -G %i -ExecStop=/bin/kill -INT $MAINPID +ExecStart=/usr/bin/vey-dcgen -c /etc/vey-dcgen/%i/ --control-dir $RUNTIME_DIRECTORY -s -G %i +ExecReload=/bin/kill -HUP $MAINPID +ExecStop=-/usr/bin/vey-dcgen-ctl --control-dir $RUNTIME_DIRECTORY -G %i -p $MAINPID offline +KillMode=none +Restart=on-failure +SuccessExitStatus=SIGQUIT +RestartPreventExitStatus=255 +TimeoutStartSec=10 [Install] WantedBy=multi-user.target diff --git a/vey-dcgen/docker/README.md b/vey-dcgen/docker/README.md index 086827ed5..76f9f6b80 100644 --- a/vey-dcgen/docker/README.md +++ b/vey-dcgen/docker/README.md @@ -16,7 +16,7 @@ The image starts `vey-dcgen -c /etc/vey-dcgen/ -G default -v` with [`config/main `vey-dcgen` cannot load without a CA certificate and private key. The example under `examples/simple` points at files that are not in the image. The container config points at `/var/lib/vey-dcgen/ca.crt` and `/var/lib/vey-dcgen/ca.key`. On first start, [`entrypoint.sh`](entrypoint.sh) runs `vey-mkcert --root --ec256` to create a P-256 root CA there when both files are missing. If only one of them exists, the entrypoint exits instead of overwriting it. -`UDP_LISTEN_ADDR` defaults to `0.0.0.0:2999`. The process otherwise listens on `127.0.0.1`, which other containers cannot reach. `-V` and `--help` skip CA generation. +The shipped config listens on `0.0.0.0:2999`, so other containers can reach it. `-V` and `--help` skip CA generation. A CA created in the writable container layer disappears when the container is removed. Mount a volume on `/var/lib/vey-dcgen` to keep it. @@ -56,14 +56,7 @@ docker run --rm -d --name vey-dcgen \ ghcr.io/vey-oss/vey-dcgen: ``` -To change more than the CA paths, mount a directory that contains `main.yaml` over `/etc/vey-dcgen`. - -Change the listen address with the environment variable: - -```shell -docker run --rm -e UDP_LISTEN_ADDR=0.0.0.0:3000 -p 3000:3000/udp \ - ghcr.io/vey-oss/vey-dcgen: -``` +To change the listen address or anything else, mount a directory that contains `main.yaml` over `/etc/vey-dcgen`. The `server` entry in that file sets the listen address. ## Build locally diff --git a/vey-dcgen/docker/alpine.Dockerfile b/vey-dcgen/docker/alpine.Dockerfile index 2a11a2f50..b2fd69096 100644 --- a/vey-dcgen/docker/alpine.Dockerfile +++ b/vey-dcgen/docker/alpine.Dockerfile @@ -1,11 +1,11 @@ FROM rust:alpine AS builder WORKDIR /usr/src/vey COPY . . -RUN apk add --no-cache musl-dev openssl-dev cmake clang make +RUN apk add --no-cache musl-dev openssl-dev cmake clang make capnproto pkgconfig libbpf-dev ENV RUSTFLAGS="-Ctarget-feature=-crt-static" ARG VEY_PGO_PROFILE ENV VEY_PGO_PROFILE=${VEY_PGO_PROFILE} -RUN scripts/package/cargo_with_pgo.sh build --profile release-lto --features secure-vendored-mimalloc -p vey-dcgen -p vey-mkcert +RUN scripts/package/cargo_with_pgo.sh build --profile release-lto --features secure-vendored-mimalloc,ebpf -p vey-dcgen -p vey-mkcert FROM alpine:latest RUN apk add --no-cache libgcc libssl3 @@ -21,7 +21,6 @@ LABEL org.opencontainers.image.title="vey-dcgen" \ org.opencontainers.image.documentation="https://github.com/VEY-OSS/vey/blob/main/vey-dcgen/docker/README.md" \ org.opencontainers.image.licenses="Apache-2.0" \ org.opencontainers.image.vendor="VEY-OSS" -ENV UDP_LISTEN_ADDR=0.0.0.0:2999 EXPOSE 2999/udp ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["-c", "/etc/vey-dcgen/", "-G", "default", "-v"] diff --git a/vey-dcgen/docker/config/main.yaml b/vey-dcgen/docker/config/main.yaml index a7d8e118a..09fe5c112 100644 --- a/vey-dcgen/docker/config/main.yaml +++ b/vey-dcgen/docker/config/main.yaml @@ -1,6 +1,11 @@ # Default config shipped in the container image. # The entrypoint creates ca.crt and ca.key on first start when they are absent. # See docker/README.md. -backend: +server: + - name: local + type: udp + listen: 0.0.0.0:2999 + +issuer: ca_certificate: /var/lib/vey-dcgen/ca.crt ca_private_key: /var/lib/vey-dcgen/ca.key diff --git a/vey-dcgen/docker/debian.Dockerfile b/vey-dcgen/docker/debian.Dockerfile index 2b3424454..93d0c0eca 100644 --- a/vey-dcgen/docker/debian.Dockerfile +++ b/vey-dcgen/docker/debian.Dockerfile @@ -1,11 +1,11 @@ FROM rust:trixie AS builder WORKDIR /usr/src/vey COPY . . -RUN apt-get update && apt-get install -y cmake g++ \ +RUN apt-get update && apt-get install -y cmake g++ capnproto clang pkg-config libbpf-dev \ && rm -rf /var/lib/apt/lists/* ARG VEY_PGO_PROFILE ENV VEY_PGO_PROFILE=${VEY_PGO_PROFILE} -RUN scripts/package/cargo_with_pgo.sh build --profile release-lto --features secure-snmalloc,vendored-openssl -p vey-dcgen -p vey-mkcert +RUN scripts/package/cargo_with_pgo.sh build --profile release-lto --features secure-snmalloc,vendored-openssl,ebpf -p vey-dcgen -p vey-mkcert FROM debian:trixie-slim COPY --from=builder /usr/src/vey/target/release-lto/vey-dcgen /usr/bin/vey-dcgen @@ -20,7 +20,6 @@ LABEL org.opencontainers.image.title="vey-dcgen" \ org.opencontainers.image.documentation="https://github.com/VEY-OSS/vey/blob/main/vey-dcgen/docker/README.md" \ org.opencontainers.image.licenses="Apache-2.0" \ org.opencontainers.image.vendor="VEY-OSS" -ENV UDP_LISTEN_ADDR=0.0.0.0:2999 EXPOSE 2999/udp ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["-c", "/etc/vey-dcgen/", "-G", "default", "-v"] diff --git a/vey-dcgen/examples/simple/main.yaml b/vey-dcgen/examples/simple/main.yaml index f1bde362b..85bb87986 100644 --- a/vey-dcgen/examples/simple/main.yaml +++ b/vey-dcgen/examples/simple/main.yaml @@ -3,6 +3,11 @@ runtime: thread_number: 1 -backend: +server: + - name: local + type: udp + listen: 127.0.0.1:2999 + +issuer: ca_certificate: G3-test.crt ca_private_key: G3-test.key diff --git a/vey-dcgen/proto/Cargo.toml b/vey-dcgen/proto/Cargo.toml new file mode 100644 index 000000000..844fa56ac --- /dev/null +++ b/vey-dcgen/proto/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "vey-dcgen-proto" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +capnp.workspace = true + +[build-dependencies] +capnpc.workspace = true diff --git a/vey-dcgen/proto/build.rs b/vey-dcgen/proto/build.rs new file mode 100644 index 000000000..3caffa74c --- /dev/null +++ b/vey-dcgen/proto/build.rs @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::env; +use std::path::PathBuf; + +fn main() { + if env::var("VEY_CAPNP_USE_PRECOMPILED").is_ok() { + let gen_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("gen"); + println!( + "cargo:rustc-env=VEY_CAPNP_GENERATE_DIR={}", + gen_dir.display() + ); + } else { + println!( + "cargo:rustc-env=VEY_CAPNP_GENERATE_DIR={}", + env::var("OUT_DIR").unwrap() + ); + capnpc::CompilerCommand::new() + .src_prefix("schema") + .file("schema/types.capnp") + .file("schema/proc.capnp") + .run() + .unwrap(); + } +} diff --git a/vey-dcgen/proto/schema/proc.capnp b/vey-dcgen/proto/schema/proc.capnp new file mode 100644 index 000000000..e3425ae86 --- /dev/null +++ b/vey-dcgen/proto/schema/proc.capnp @@ -0,0 +1,16 @@ +@0xe31a7c90b4d26f58; + +using Types = import "types.capnp"; + +interface ProcControl { + version @0 () -> (version :Text); + offline @1 () -> (result :Types.OperationResult); + cancelShutdown @2 () -> (result :Types.OperationResult); + releaseController @3 () -> (result :Types.OperationResult); + + # Re-read server entries from the config file and apply the difference. + reload @4 () -> (result :Types.OperationResult); + + # Re-read the issuer section and swap the running CA. Listeners stay up. + reloadIssuer @5 () -> (result :Types.OperationResult); +} diff --git a/vey-dcgen/proto/schema/types.capnp b/vey-dcgen/proto/schema/types.capnp new file mode 100644 index 000000000..68df25c69 --- /dev/null +++ b/vey-dcgen/proto/schema/types.capnp @@ -0,0 +1,13 @@ +@0xb7c2e14a9d05f683; + +struct Error { + code @0 :Int32 = -1; + reason @1 :Text; +} + +struct OperationResult { + union { + ok @0 :Text; + err @1 :Error; + } +} diff --git a/vey-dcgen/proto/src/lib.rs b/vey-dcgen/proto/src/lib.rs new file mode 100644 index 000000000..d6a3cd6a5 --- /dev/null +++ b/vey-dcgen/proto/src/lib.rs @@ -0,0 +1,16 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +#![allow(clippy::needless_lifetimes)] +#![allow(clippy::uninlined_format_args)] + +pub mod types_capnp { + #![allow(clippy::extra_unused_type_parameters)] + include!(concat!(env!("VEY_CAPNP_GENERATE_DIR"), "/types_capnp.rs")); +} + +pub mod proc_capnp { + include!(concat!(env!("VEY_CAPNP_GENERATE_DIR"), "/proc_capnp.rs")); +} diff --git a/vey-dcgen/src/backend/mod.rs b/vey-dcgen/src/backend/mod.rs deleted file mode 100644 index 669fdb45d..000000000 --- a/vey-dcgen/src/backend/mod.rs +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. - * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. - */ - -use std::sync::Arc; -use std::time::Duration; - -use anyhow::anyhow; -use log::{debug, error, info, warn}; -use openssl::pkey::{PKey, Private}; -use openssl::x509::X509; -use tokio::runtime::Handle; - -use vey_cert_agent::Request; -use vey_tls_cert::builder::{MimicCertBuilder, ServerCertBuilder, TlsServerCertBuilder}; -use vey_tls_cert::ext::X509Ext; -use vey_types::net::TlsCertUsage; - -use super::{BackendRequest, BackendResponse}; -use crate::config::OpensslBackendConfig; -use crate::frontend::GeneratedData; - -mod stats; -pub(crate) use stats::BackendStats; - -pub(crate) struct OpensslBackend { - config: Arc, - builder: ServerCertBuilder, - stats: Arc, -} - -impl OpensslBackend { - pub(crate) fn new( - config: &Arc, - stats: &Arc, - ) -> anyhow::Result { - let builder = TlsServerCertBuilder::new_ec256()?; - Ok(OpensslBackend { - config: Arc::clone(config), - builder, - stats: Arc::clone(stats), - }) - } - - pub(crate) fn refresh(&mut self) -> anyhow::Result<()> { - self.stats.add_refresh_total(); - self.builder.refresh_datetime()?; - self.builder.refresh_ec256()?; - self.stats.add_refresh_ok(); - Ok(()) - } - - fn generate(&mut self, req: &Request) -> anyhow::Result { - self.stats.add_request_total(); - if let Some(mimic_cert) = req.cert() { - self.generate_mimic(mimic_cert, req.cert_usage()) - } else { - self.builder.refresh_serial()?; - let cert = self.builder.build_fake( - req.host(), - &self.config.ca_cert, - &self.config.ca_key, - None, - )?; - let ttl = self.builder.valid_seconds()?; - self.pack_data(cert, self.builder.pkey(), ttl) - } - } - - fn generate_mimic( - &self, - mimic_cert: &X509, - cert_usage: TlsCertUsage, - ) -> anyhow::Result { - let mut mimic_builder = MimicCertBuilder::new(mimic_cert)?; - mimic_builder.set_keep_serial(self.config.keep_serial); - - let cert = match cert_usage { - TlsCertUsage::TlsServer => { - mimic_builder.build_tls_cert(&self.config.ca_cert, &self.config.ca_key, None)? - } - TlsCertUsage::TLsServerTongsuo => mimic_builder.build_tls_cert_with_new_usage( - &self.config.ca_cert, - &self.config.ca_key, - None, - )?, - TlsCertUsage::TlcpServerEncryption => mimic_builder.build_tlcp_enc_cert( - &self.config.ca_cert, - &self.config.ca_key, - None, - )?, - TlsCertUsage::TlcpServerSignature => mimic_builder.build_tlcp_sign_cert( - &self.config.ca_cert, - &self.config.ca_key, - None, - )?, - }; - - let ttl = mimic_builder.valid_seconds()?; - - self.pack_data(cert, mimic_builder.pkey(), ttl) - } - - fn pack_data( - &self, - cert: X509, - pkey: &PKey, - ttl: i32, - ) -> anyhow::Result { - let ttl = ttl.clamp(0, self.config.max_ttl) as u32; - let mut cert_pem = cert - .to_pem() - .map_err(|e| anyhow!("failed to encode cert to PEM format: {e}"))?; - if !self.config.ca_cert_pem.is_empty() { - cert_pem.extend_from_slice(&self.config.ca_cert_pem); - } - let key = pkey - .private_key_to_der() - .map_err(|e| anyhow!("failed to encode pkey to DER format: {e}"))?; - - let data = GeneratedData { - cert: unsafe { String::from_utf8_unchecked(cert_pem) }, - key, - ttl, - }; - self.stats.add_request_ok(); - Ok(data) - } - - pub(crate) fn spawn( - mut self, - handle: &Handle, - id: usize, - req_receiver: kanal::AsyncReceiver, - rsp_sender: kanal::AsyncSender, - ) { - handle.spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(300)); - loop { - tokio::select! { - _ = interval.tick() => { - if let Err(e) = self.refresh() { - warn!("failed to refresh backend: {e:?}"); - } - } - r = req_receiver.recv() => { - let Ok(req) = r else { - break - }; - - if let Some(cert) = req.user_req.cert() { - info!("{}", cert.dump()); - } - - let host = req.user_req.host().clone(); - debug!("{host} - [#{id}] start cert generation"); - match self.generate(&req.user_req) { - Ok(data) => { - debug!("{host} - [#{id}] cert generated"); - if let Err(e) = rsp_sender.send(req.into_response(data)).await { - error!("{host} - [#{id}] failed to send cert to frontend: {e}"); - break; - } - } - Err(e) => { - warn!("{host} - [#{id}] cert generation failed: {e:?}"); - } - } - } - } - } - }); - } -} diff --git a/vey-dcgen/src/config/backend.rs b/vey-dcgen/src/config/issuer.rs similarity index 57% rename from vey-dcgen/src/config/backend.rs rename to vey-dcgen/src/config/issuer.rs index c132dba59..5b4b536b6 100644 --- a/vey-dcgen/src/config/backend.rs +++ b/vey-dcgen/src/config/issuer.rs @@ -4,22 +4,29 @@ */ use std::sync::Arc; -use std::sync::OnceLock; use anyhow::{Context, anyhow}; +use arc_swap::ArcSwapOption; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use yaml_rust::Yaml; use vey_histogram::HistogramMetricsConfig; -static BACKEND_CONFIG_LOCK: OnceLock> = OnceLock::new(); +static ISSUER_CONFIG: ArcSwapOption = ArcSwapOption::const_empty(); -pub(crate) fn get_config() -> Option> { - BACKEND_CONFIG_LOCK.get().cloned() +pub(crate) fn get_config() -> Option> { + ISSUER_CONFIG.load_full() } -pub(crate) struct OpensslBackendConfig { +pub(crate) fn replace_config(mut config: OpensslIssuerConfig) { + if let Some(current) = get_config() { + config.duration_stats = current.duration_stats.clone(); + } + ISSUER_CONFIG.store(Some(Arc::new(config))); +} + +pub(crate) struct OpensslIssuerConfig { pub(crate) ca_cert: X509, pub(crate) ca_key: PKey, pub(crate) ca_cert_pem: Vec, @@ -29,6 +36,14 @@ pub(crate) struct OpensslBackendConfig { } pub(super) fn load_config(value: &Yaml) -> anyhow::Result<()> { + if get_config().is_some() { + return Err(anyhow!("duplicate issuer config")); + } + replace_config(parse_config(value)?); + Ok(()) +} + +pub(super) fn parse_config(value: &Yaml) -> anyhow::Result { if let Yaml::Hash(map) = value { let mut no_append_ca_cert = false; let mut ca_cert_pem = Vec::new(); @@ -95,20 +110,61 @@ pub(super) fn load_config(value: &Yaml) -> anyhow::Result<()> { if no_append_ca_cert { ca_cert_pem.clear(); } - BACKEND_CONFIG_LOCK - .set(Arc::new(OpensslBackendConfig { - ca_cert, - ca_key, - ca_cert_pem, - keep_serial, - max_ttl, - duration_stats, - })) - .map_err(|_| anyhow!("duplicate backend config"))?; - Ok(()) + Ok(OpensslIssuerConfig { + ca_cert, + ca_key, + ca_cert_pem, + keep_serial, + max_ttl, + duration_stats, + }) } else { Err(anyhow!( - "yam value type for the backend config should be 'map'" + "yaml value type for the issuer config should be 'map'" )) } } + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use vey_histogram::HistogramMetricsConfig; + + use super::*; + + fn example_config(max_ttl: i32) -> OpensslIssuerConfig { + let cert_pem = include_bytes!("../../examples/simple/G3-test.crt"); + let key_pem = include_bytes!("../../examples/simple/G3-test.key"); + OpensslIssuerConfig { + ca_cert: X509::from_pem(cert_pem).unwrap(), + ca_key: PKey::private_key_from_pem(key_pem).unwrap(), + ca_cert_pem: cert_pem.to_vec(), + keep_serial: false, + max_ttl, + duration_stats: HistogramMetricsConfig::default(), + } + } + + #[test] + fn parse_rejects_non_map() { + match parse_config(&Yaml::Integer(1)) { + Err(err) => assert!(err.to_string().contains("map")), + Ok(_) => panic!("non-map issuer config was accepted"), + } + } + + #[test] + fn replace_swaps_signing_options_and_keeps_duration_stats() { + let mut initial = example_config(400); + initial.duration_stats = HistogramMetricsConfig::with_rotate(Duration::from_secs(9)); + let expected_duration = initial.duration_stats.clone(); + replace_config(initial); + replace_config(example_config(900)); + + let current = get_config().unwrap(); + assert_eq!(current.max_ttl, 900); + assert_eq!(current.duration_stats, expected_duration); + assert!(!current.ca_cert_pem.is_empty()); + } +} diff --git a/vey-dcgen/src/config/mod.rs b/vey-dcgen/src/config/mod.rs index 6e95f9330..2a768c997 100644 --- a/vey-dcgen/src/config/mod.rs +++ b/vey-dcgen/src/config/mod.rs @@ -5,12 +5,17 @@ */ use std::path::Path; +use std::sync::Mutex; use anyhow::anyhow; use yaml_rust::{Yaml, yaml}; -mod backend; -pub(crate) use backend::{OpensslBackendConfig, get_config as get_backend_config}; +mod issuer; +pub(crate) use issuer::{OpensslIssuerConfig, get_config as get_issuer_config}; + +pub(crate) mod server; + +static RELOAD_ISSUER: Mutex> = Mutex::new(None); pub fn load(program_name: &str) -> anyhow::Result<&'static Path> { let config_file = @@ -22,15 +27,92 @@ pub fn load(program_name: &str) -> anyhow::Result<&'static Path> { _ => Err(anyhow!("yaml doc root should be hash")), })?; + if server::is_empty() { + return Err(anyhow!("no server configured")); + } + if get_issuer_config().is_none() { + return Err(anyhow!("no issuer config available")); + } + Ok(config_file) } +pub(crate) async fn reload() -> anyhow::Result<()> { + tokio::task::spawn_blocking(reload_blocking) + .await + .map_err(|e| anyhow!("failed to join reload task: {e}"))? +} + +pub(crate) async fn reload_issuer() -> anyhow::Result<()> { + tokio::task::spawn_blocking(reload_issuer_blocking) + .await + .map_err(|e| anyhow!("failed to join issuer reload task: {e}"))? +} + +fn reload_blocking() -> anyhow::Result<()> { + server::clear(); + RELOAD_ISSUER.lock().unwrap().take(); + if let Some(config_file) = vey_daemon::opts::config_file() { + vey_yaml::foreach_doc(config_file, |_, doc| match doc { + Yaml::Hash(map) => reload_doc(map), + _ => Err(anyhow!("yaml doc root should be hash")), + })?; + } + if let Some(config) = RELOAD_ISSUER.lock().unwrap().take() { + issuer::replace_config(config); + } + Ok(()) +} + +fn reload_issuer_blocking() -> anyhow::Result<()> { + RELOAD_ISSUER.lock().unwrap().take(); + if let Some(config_file) = vey_daemon::opts::config_file() { + vey_yaml::foreach_doc(config_file, |_, doc| match doc { + Yaml::Hash(map) => reload_issuer_doc(map), + _ => Err(anyhow!("yaml doc root should be hash")), + })?; + } + if let Some(config) = RELOAD_ISSUER.lock().unwrap().take() { + issuer::replace_config(config); + } + Ok(()) +} + +fn stash_issuer(value: &Yaml) -> anyhow::Result<()> { + let mut pending = RELOAD_ISSUER.lock().unwrap(); + if pending.is_some() { + return Err(anyhow!("duplicate issuer config")); + } + *pending = Some(issuer::parse_config(value)?); + Ok(()) +} + +fn reload_issuer_doc(map: &yaml::Hash) -> anyhow::Result<()> { + vey_yaml::foreach_kv(map, |k, v| match vey_yaml::key::normalize(k).as_str() { + "issuer" => stash_issuer(v), + _ => Ok(()), + })?; + Ok(()) +} + +fn reload_doc(map: &yaml::Hash) -> anyhow::Result<()> { + vey_yaml::foreach_kv(map, |k, v| match vey_yaml::key::normalize(k).as_str() { + "runtime" | "worker" | "stat" | "controller" => Ok(()), + "issuer" => stash_issuer(v), + "server" => server::load_all(v), + _ => Ok(()), + })?; + Ok(()) +} + fn load_doc(map: &yaml::Hash, program_name: &str) -> anyhow::Result<()> { vey_yaml::foreach_kv(map, |k, v| match vey_yaml::key::normalize(k).as_str() { "runtime" => vey_daemon::runtime::config::load(v), "worker" => vey_daemon::runtime::config::load_worker(v), "stat" => vey_daemon::stat::config::load(v, program_name), - "backend" => backend::load_config(v), + "controller" => vey_daemon::control::config::load(v), + "issuer" => issuer::load_config(v), + "server" => server::load_all(v), _ => Err(anyhow!("invalid key {k} in main conf")), })?; Ok(()) diff --git a/vey-dcgen/src/config/server/dummy.rs b/vey-dcgen/src/config/server/dummy.rs new file mode 100644 index 000000000..481f2bfa6 --- /dev/null +++ b/vey-dcgen/src/config/server/dummy.rs @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::anyhow; +use yaml_rust::{Yaml, yaml}; + +use vey_types::metrics::NodeName; + +use super::{AnyServerConfig, ServerConfig, ServerConfigDiffAction}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct DummyServerConfig { + name: NodeName, +} + +impl DummyServerConfig { + pub(crate) fn new(name: &NodeName) -> Self { + DummyServerConfig { name: name.clone() } + } + + pub(super) fn parse(map: &yaml::Hash) -> anyhow::Result { + let mut server = DummyServerConfig { + name: NodeName::default(), + }; + vey_yaml::foreach_kv(map, |k, v| server.set(k, v))?; + server.check()?; + Ok(server) + } + + fn set(&mut self, k: &str, v: &Yaml) -> anyhow::Result<()> { + match vey_yaml::key::normalize(k).as_str() { + super::CONFIG_KEY_SERVER_TYPE => Ok(()), + "name" => { + self.name = vey_yaml::value::as_metric_node_name(v)?; + Ok(()) + } + _ => Err(anyhow!("invalid key {k}")), + } + } + + fn check(&self) -> anyhow::Result<()> { + if self.name.is_empty() { + return Err(anyhow!("name is not set")); + } + Ok(()) + } +} + +impl ServerConfig for DummyServerConfig { + fn name(&self) -> &NodeName { + &self.name + } + + fn diff_action(&self, new: &AnyServerConfig) -> ServerConfigDiffAction { + let AnyServerConfig::Dummy(new) = new else { + return ServerConfigDiffAction::SpawnNew; + }; + if self == new { + ServerConfigDiffAction::NoAction + } else { + ServerConfigDiffAction::ReloadAndRespawn + } + } +} diff --git a/vey-dcgen/src/config/server/mod.rs b/vey-dcgen/src/config/server/mod.rs new file mode 100644 index 000000000..7383d1895 --- /dev/null +++ b/vey-dcgen/src/config/server/mod.rs @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::{Context, anyhow}; +use yaml_rust::{Yaml, yaml}; + +use vey_macros::AnyConfig; +use vey_types::metrics::NodeName; +use vey_yaml::HybridParser; + +mod registry; +pub(crate) use registry::{clear, get_all, is_empty}; + +mod dummy; +pub(crate) use dummy::DummyServerConfig; + +mod udp; +pub(crate) use udp::UdpServerConfig; + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub(crate) use unix::UnixServerConfig; + +const CONFIG_KEY_SERVER_TYPE: &str = "type"; + +pub(crate) enum ServerConfigDiffAction { + NoAction, + SpawnNew, + ReloadAndRespawn, +} + +pub(crate) trait ServerConfig { + fn name(&self) -> &NodeName; + fn diff_action(&self, new: &AnyServerConfig) -> ServerConfigDiffAction; +} + +#[derive(Clone, Debug, Eq, PartialEq, AnyConfig)] +#[def_fn(name, &NodeName)] +#[def_fn(diff_action, &Self, ServerConfigDiffAction)] +pub(crate) enum AnyServerConfig { + Dummy(DummyServerConfig), + Udp(UdpServerConfig), + #[cfg(unix)] + Unix(UnixServerConfig), +} + +pub(crate) fn load_all(v: &Yaml) -> anyhow::Result<()> { + let conf_dir = vey_daemon::opts::config_dir() + .ok_or_else(|| anyhow!("no valid config dir has been set"))?; + let parser = HybridParser::new(conf_dir, vey_daemon::opts::config_file_extension()); + parser.foreach_map(v, |map, _position| { + let server = load_server(map)?; + if let Some(old) = registry::add(server) { + Err(anyhow!("server with name {} already exists", old.name())) + } else { + Ok(()) + } + })?; + Ok(()) +} + +fn load_server(map: &yaml::Hash) -> anyhow::Result { + let server_type = vey_yaml::hash_get_required_str(map, CONFIG_KEY_SERVER_TYPE)?; + match vey_yaml::key::normalize(server_type).as_str() { + "dummy" => { + let server = + DummyServerConfig::parse(map).context("failed to load this Dummy server")?; + Ok(AnyServerConfig::Dummy(server)) + } + "udp" => { + let server = UdpServerConfig::parse(map).context("failed to load this UDP server")?; + Ok(AnyServerConfig::Udp(server)) + } + #[cfg(unix)] + "unix" => { + let server = UnixServerConfig::parse(map).context("failed to load this Unix server")?; + Ok(AnyServerConfig::Unix(server)) + } + #[cfg(not(unix))] + "unix" => Err(anyhow!("unix server is not supported on this platform")), + _ => Err(anyhow!("unsupported server type {server_type}")), + } +} diff --git a/vey-dcgen/src/config/server/registry.rs b/vey-dcgen/src/config/server/registry.rs new file mode 100644 index 000000000..f08ab3abc --- /dev/null +++ b/vey-dcgen/src/config/server/registry.rs @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::collections::BTreeMap; +use std::sync::Mutex; + +use vey_types::metrics::NodeName; + +use super::AnyServerConfig; + +static SERVER_CONFIG_REGISTRY: Mutex> = + Mutex::new(BTreeMap::new()); + +pub(super) fn add(server: AnyServerConfig) -> Option { + let name = server.name().clone(); + let mut registry = SERVER_CONFIG_REGISTRY.lock().unwrap(); + registry.insert(name, server) +} + +pub(crate) fn clear() { + SERVER_CONFIG_REGISTRY.lock().unwrap().clear(); +} + +pub(crate) fn is_empty() -> bool { + SERVER_CONFIG_REGISTRY.lock().unwrap().is_empty() +} + +pub(crate) fn get_all() -> Vec { + SERVER_CONFIG_REGISTRY + .lock() + .unwrap() + .values() + .cloned() + .collect() +} diff --git a/vey-dcgen/src/config/server/udp.rs b/vey-dcgen/src/config/server/udp.rs new file mode 100644 index 000000000..25714447e --- /dev/null +++ b/vey-dcgen/src/config/server/udp.rs @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::{Context, anyhow}; +use yaml_rust::{Yaml, yaml}; + +use vey_types::metrics::NodeName; +use vey_types::net::UdpListenConfig; + +use super::{AnyServerConfig, ServerConfig, ServerConfigDiffAction}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct UdpServerConfig { + name: NodeName, + pub(crate) listen: UdpListenConfig, +} + +impl UdpServerConfig { + fn new() -> Self { + UdpServerConfig { + name: NodeName::default(), + listen: UdpListenConfig::default(), + } + } + + pub(super) fn parse(map: &yaml::Hash) -> anyhow::Result { + let mut server = UdpServerConfig::new(); + vey_yaml::foreach_kv(map, |k, v| server.set(k, v))?; + server.check()?; + Ok(server) + } + + fn set(&mut self, k: &str, v: &Yaml) -> anyhow::Result<()> { + match vey_yaml::key::normalize(k).as_str() { + super::CONFIG_KEY_SERVER_TYPE => Ok(()), + "name" => { + self.name = vey_yaml::value::as_metric_node_name(v)?; + Ok(()) + } + "listen" => { + self.listen = vey_yaml::value::as_udp_listen_config(v) + .context(format!("invalid udp listen config value for key {k}"))?; + Ok(()) + } + _ => Err(anyhow!("invalid key {k}")), + } + } + + fn check(&mut self) -> anyhow::Result<()> { + if self.name.is_empty() { + return Err(anyhow!("name is not set")); + } + self.listen.check().context("invalid listen config")?; + Ok(()) + } +} + +impl ServerConfig for UdpServerConfig { + fn name(&self) -> &NodeName { + &self.name + } + + fn diff_action(&self, new: &AnyServerConfig) -> ServerConfigDiffAction { + let AnyServerConfig::Udp(new) = new else { + return ServerConfigDiffAction::SpawnNew; + }; + if self == new { + ServerConfigDiffAction::NoAction + } else { + ServerConfigDiffAction::ReloadAndRespawn + } + } +} + +#[cfg(test)] +mod tests { + use super::super::ServerConfig; + use super::*; + + fn parse(text: &str) -> anyhow::Result { + let docs = yaml_rust::YamlLoader::load_from_str(text).unwrap(); + let Yaml::Hash(map) = &docs[0] else { + panic!("yaml root should be a map"); + }; + UdpServerConfig::parse(map) + } + + #[test] + fn parse_udp_listen() { + let config = parse("name: local\ntype: udp\nlisten: 127.0.0.1:2999\n").unwrap(); + assert_eq!(config.name().as_str(), "local"); + assert_eq!(config.listen.address().to_string(), "127.0.0.1:2999"); + } + + #[test] + fn parse_requires_name_and_listen() { + let err = parse("type: udp\nlisten: 127.0.0.1:2999\n").unwrap_err(); + assert!(err.to_string().contains("name is not set")); + + let err = parse("name: local\ntype: udp\n").unwrap_err(); + assert!(err.to_string().contains("invalid listen config")); + } +} diff --git a/vey-dcgen/src/config/server/unix.rs b/vey-dcgen/src/config/server/unix.rs new file mode 100644 index 000000000..1a9de9a92 --- /dev/null +++ b/vey-dcgen/src/config/server/unix.rs @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::path::PathBuf; + +use anyhow::{Context, anyhow}; +use yaml_rust::{Yaml, yaml}; + +use vey_types::metrics::NodeName; + +use super::{AnyServerConfig, ServerConfig, ServerConfigDiffAction}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct UnixServerConfig { + name: NodeName, + pub(crate) listen: PathBuf, +} + +impl UnixServerConfig { + fn new() -> Self { + UnixServerConfig { + name: NodeName::default(), + listen: PathBuf::new(), + } + } + + pub(super) fn parse(map: &yaml::Hash) -> anyhow::Result { + let mut server = UnixServerConfig::new(); + vey_yaml::foreach_kv(map, |k, v| server.set(k, v))?; + server.check()?; + Ok(server) + } + + fn set(&mut self, k: &str, v: &Yaml) -> anyhow::Result<()> { + match vey_yaml::key::normalize(k).as_str() { + super::CONFIG_KEY_SERVER_TYPE => Ok(()), + "name" => { + self.name = vey_yaml::value::as_metric_node_name(v)?; + Ok(()) + } + "listen" => { + self.listen = vey_yaml::value::as_absolute_path(v) + .context(format!("invalid unix listen path value for key {k}"))?; + Ok(()) + } + _ => Err(anyhow!("invalid key {k}")), + } + } + + fn check(&mut self) -> anyhow::Result<()> { + if self.name.is_empty() { + return Err(anyhow!("name is not set")); + } + if self.listen.as_os_str().is_empty() { + return Err(anyhow!("listen path is not set")); + } + Ok(()) + } +} + +impl ServerConfig for UnixServerConfig { + fn name(&self) -> &NodeName { + &self.name + } + + fn diff_action(&self, new: &AnyServerConfig) -> ServerConfigDiffAction { + let AnyServerConfig::Unix(new) = new else { + return ServerConfigDiffAction::SpawnNew; + }; + if self == new { + ServerConfigDiffAction::NoAction + } else { + ServerConfigDiffAction::ReloadAndRespawn + } + } +} + +#[cfg(test)] +mod tests { + use super::super::ServerConfig; + use super::*; + + fn parse(text: &str) -> anyhow::Result { + let docs = yaml_rust::YamlLoader::load_from_str(text).unwrap(); + let Yaml::Hash(map) = &docs[0] else { + panic!("yaml root should be a map"); + }; + UnixServerConfig::parse(map) + } + + #[test] + fn parse_unix_listen() { + let config = + parse("name: local_unix\ntype: unix\nlisten: /run/vey-dcgen/dcgen.sock\n").unwrap(); + assert_eq!(config.name().as_str(), "local_unix"); + assert_eq!(config.listen.as_os_str(), "/run/vey-dcgen/dcgen.sock"); + } + + #[test] + fn parse_rejects_relative_path() { + let err = parse("name: local_unix\ntype: unix\nlisten: dcgen.sock\n").unwrap_err(); + let message = format!("{err:#}"); + assert!( + message.contains("absolute path"), + "unexpected error: {message}" + ); + } +} diff --git a/vey-dcgen/src/control/capnp/common.rs b/vey-dcgen/src/control/capnp/common.rs new file mode 100644 index 000000000..706cb6ac4 --- /dev/null +++ b/vey-dcgen/src/control/capnp/common.rs @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use vey_dcgen_proto::types_capnp::operation_result; + +pub(super) fn set_operation_result( + mut builder: operation_result::Builder<'_>, + r: anyhow::Result<()>, +) { + match r { + Ok(_) => builder.set_ok("success"), + Err(e) => { + let mut ev = builder.init_err(); + ev.set_code(-1); + ev.set_reason(format!("{e:?}").as_str()); + } + } +} diff --git a/vey-dcgen/src/control/capnp/mod.rs b/vey-dcgen/src/control/capnp/mod.rs new file mode 100644 index 000000000..7dcddfc08 --- /dev/null +++ b/vey-dcgen/src/control/capnp/mod.rs @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use vey_dcgen_proto::proc_capnp::proc_control; + +mod common; +use common::set_operation_result; +mod proc; + +pub fn stop_working_thread() { + vey_daemon::control::capnp::stop_working_thread(); +} + +fn build_capnp_client() -> capnp::capability::Client { + let control_client: proc_control::Client = capnp_rpc::new_client(proc::ProcControlImpl); + control_client.client +} + +pub async fn spawn_working_thread() -> anyhow::Result> { + vey_daemon::control::capnp::spawn_working_thread(&build_capnp_client).await +} diff --git a/vey-dcgen/src/control/capnp/proc.rs b/vey-dcgen/src/control/capnp/proc.rs new file mode 100644 index 000000000..2ba5c63a9 --- /dev/null +++ b/vey-dcgen/src/control/capnp/proc.rs @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::rc::Rc; + +use vey_dcgen_proto::proc_capnp::proc_control; + +use super::set_operation_result; + +pub(super) struct ProcControlImpl; + +impl proc_control::Server for ProcControlImpl { + async fn version( + self: Rc, + _params: proc_control::VersionParams, + mut results: proc_control::VersionResults, + ) -> capnp::Result<()> { + results.get().set_version(crate::build::VERSION); + Ok(()) + } + + async fn offline( + self: Rc, + _params: proc_control::OfflineParams, + mut results: proc_control::OfflineResults, + ) -> capnp::Result<()> { + vey_daemon::control::quit::start_graceful_shutdown().await; + set_operation_result(results.get().init_result(), Ok(())); + Ok(()) + } + + async fn cancel_shutdown( + self: Rc, + _params: proc_control::CancelShutdownParams, + mut results: proc_control::CancelShutdownResults, + ) -> capnp::Result<()> { + let r = vey_daemon::control::quit::cancel_graceful_shutdown().await; + set_operation_result(results.get().init_result(), r); + Ok(()) + } + + async fn release_controller( + self: Rc, + _params: proc_control::ReleaseControllerParams, + mut results: proc_control::ReleaseControllerResults, + ) -> capnp::Result<()> { + let r = vey_daemon::control::quit::release_controller().await; + set_operation_result(results.get().init_result(), r); + Ok(()) + } + + async fn reload( + self: Rc, + _params: proc_control::ReloadParams, + mut results: proc_control::ReloadResults, + ) -> capnp::Result<()> { + let r = crate::signal::reload().await; + set_operation_result(results.get().init_result(), r); + Ok(()) + } + + async fn reload_issuer( + self: Rc, + _params: proc_control::ReloadIssuerParams, + mut results: proc_control::ReloadIssuerResults, + ) -> capnp::Result<()> { + let r = crate::signal::reload_issuer().await; + set_operation_result(results.get().init_result(), r); + Ok(()) + } +} diff --git a/vey-dcgen/src/control/local.rs b/vey-dcgen/src/control/local.rs new file mode 100644 index 000000000..0c5d8e0d4 --- /dev/null +++ b/vey-dcgen/src/control/local.rs @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::future::Future; + +use log::debug; + +use vey_daemon::control::LocalController; + +pub struct UniqueController {} +pub struct DaemonController {} + +impl UniqueController { + pub fn start(program_name: &str) -> anyhow::Result { + LocalController::start_unique(program_name, crate::opts::daemon_group()) + } + + pub(super) async fn abort() { + debug!("aborting unique controller"); + LocalController::abort_unique().await; + } +} + +impl DaemonController { + pub fn start(program_name: String) -> anyhow::Result { + LocalController::start_daemon(&program_name, crate::opts::daemon_group()) + } + + pub(super) async fn abort() { + debug!("aborting daemon controller"); + LocalController::abort_daemon().await; + } +} diff --git a/vey-dcgen/src/control/mod.rs b/vey-dcgen/src/control/mod.rs new file mode 100644 index 000000000..4209127ba --- /dev/null +++ b/vey-dcgen/src/control/mod.rs @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +mod quit; +pub use quit::QuitActor; + +mod upgrade; +pub use upgrade::UpgradeActor; + +mod local; +pub use local::{DaemonController, UniqueController}; + +pub mod capnp; diff --git a/vey-dcgen/src/control/quit.rs b/vey-dcgen/src/control/quit.rs new file mode 100644 index 000000000..ffca5247b --- /dev/null +++ b/vey-dcgen/src/control/quit.rs @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use log::debug; + +use vey_daemon::control::quit::QuitAction; + +use super::local::{DaemonController, UniqueController}; + +#[derive(Default)] +pub struct QuitActor {} + +impl QuitAction for QuitActor { + async fn release_controller(&self) { + DaemonController::abort().await; + } + + fn resume_controller(&self, program_name: String) -> anyhow::Result<()> { + let daemon_ctl = DaemonController::start(program_name)?; + tokio::spawn(async move { + daemon_ctl.await; + }); + Ok(()) + } + + async fn start_shutdown(&self) { + debug!("stopping all servers"); + crate::serve::stop_all().await; + debug!("stopped all servers"); + } + + async fn wait_graceful_shutdown(&self) { + let delay = vey_daemon::runtime::config::get_task_wait_delay(); + debug!("will delay {delay:?} before waiting tasks"); + tokio::time::sleep(delay).await; + let wait = vey_daemon::runtime::config::get_task_wait_timeout(); + let quit = vey_daemon::runtime::config::get_task_quit_timeout(); + crate::serve::wait_all_tasks(wait, quit, |name, left| { + debug!("{left} tasks left on server {name}"); + }) + .await; + } + + async fn shutdown(&self) { + UniqueController::abort().await + } +} diff --git a/vey-dcgen/src/control/upgrade.rs b/vey-dcgen/src/control/upgrade.rs new file mode 100644 index 000000000..01f5f5265 --- /dev/null +++ b/vey-dcgen/src/control/upgrade.rs @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::anyhow; +use capnp_rpc::RpcSystem; +use capnp_rpc::rpc_twoparty_capnp::Side; + +use vey_daemon::control::LocalController; +use vey_daemon::control::upgrade::UpgradeAction; + +use vey_dcgen_proto::proc_capnp::proc_control; +use vey_dcgen_proto::types_capnp::operation_result; + +pub struct UpgradeActor { + proc_control: proc_control::Client, +} + +impl UpgradeAction for UpgradeActor { + async fn connect_rpc(program_name: &str) -> anyhow::Result<(RpcSystem, Self)> { + LocalController::connect_rpc::( + program_name, + crate::opts::daemon_group(), + ) + .await + .map(|(r, proc_control)| (r, UpgradeActor { proc_control })) + } + + async fn cancel_shutdown(&self) -> anyhow::Result<()> { + let req = self.proc_control.cancel_shutdown_request(); + let rsp = req.send().promise.await?; + check_operation_result(rsp.get()?.get_result()?) + } + + async fn release_controller(&self) -> anyhow::Result<()> { + let req = self.proc_control.release_controller_request(); + let rsp = req.send().promise.await?; + check_operation_result(rsp.get()?.get_result()?) + } + + async fn confirm_shutdown(&self) -> anyhow::Result<()> { + let req = self.proc_control.offline_request(); + let rsp = req.send().promise.await?; + check_operation_result(rsp.get()?.get_result()?) + } +} + +fn check_operation_result(r: operation_result::Reader<'_>) -> anyhow::Result<()> { + match r.which().map_err(|e| anyhow!("rpc protocol error: {e}"))? { + operation_result::Which::Ok(_) => Ok(()), + operation_result::Which::Err(err) => { + let e = err?; + let msg = e.get_reason()?.to_str()?; + Err(anyhow!("remote error: {} - {msg}", e.get_code())) + } + } +} diff --git a/vey-dcgen/src/frontend/mod.rs b/vey-dcgen/src/frontend/mod.rs deleted file mode 100644 index ab63a4eea..000000000 --- a/vey-dcgen/src/frontend/mod.rs +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. - */ - -use std::sync::Arc; - -use anyhow::anyhow; -use log::{debug, warn}; -use tokio::time::Instant; - -use vey_cert_agent::Request; -use vey_histogram::HistogramRecorder; -use vey_types::net::UdpListenConfig; - -use crate::{BackendRequest, BackendResponse}; - -mod stats; -pub(crate) use stats::FrontendStats; - -mod udp_dgram; -use udp_dgram::UdpDgramIo; - -#[derive(Debug)] -pub(crate) struct GeneratedData { - pub(crate) cert: String, - pub(crate) key: Vec, - pub(crate) ttl: u32, -} - -pub(super) struct Frontend { - io: UdpDgramIo, - stats: Arc, - duration_recorder: HistogramRecorder, - rsp_receiver: kanal::AsyncReceiver, -} - -impl Frontend { - pub(super) fn new( - listen_config: &UdpListenConfig, - duration_recorder: HistogramRecorder, - rsp_receiver: kanal::AsyncReceiver, - ) -> anyhow::Result { - let io = UdpDgramIo::new(listen_config)?; - Ok(Frontend { - io, - stats: Arc::new(FrontendStats::default()), - duration_recorder, - rsp_receiver, - }) - } - - pub(super) fn stats(&self) -> Arc { - self.stats.clone() - } - - pub(super) async fn run( - self, - req_sender: kanal::AsyncSender, - ) -> anyhow::Result<()> { - let mut clt_c = Box::pin(tokio::signal::ctrl_c()); - - let mut rcv_buf = [0u8; 16384]; - loop { - tokio::select! { - r = self.io.recv_req(&mut rcv_buf) => { - self.stats.add_request_total(); - let recv_time = Instant::now(); - match r { - Ok((len, peer)) => match Request::parse_req(&rcv_buf[0..len]) { - Ok(user_req) => { - debug!("{} - request received", user_req.host()); - let req = BackendRequest {user_req, peer, recv_time}; - if let Err(e) = req_sender.send(req).await { - return Err(anyhow!("failed to send request to backend: {e}")); - } - } - Err(e) => { - self.stats.add_request_invalid(); - warn!("invalid request from peer {peer}: {e:?}"); - } - } - Err(e) => return Err(anyhow!("frontend recv error: {e:?}")), - } - } - r = self.rsp_receiver.recv() => { - match r { - Ok(rsp) => self.handle_rsp(rsp).await, - Err(e) => return Err(anyhow!("recv from backend failed: {e}")), - } - } - r = &mut clt_c => { - if let Err(e) = r { - warn!("failed to read Ctrl-C signal: {e}"); - } else { - debug!("received Ctrl-C signal, start shutdown now"); - } - break; - } - } - } - - drop(req_sender); - while let Ok(rsp) = self.rsp_receiver.recv().await { - self.handle_rsp(rsp).await; - } - - debug!("all requests served, quit now"); - Ok(()) - } - - async fn handle_rsp(&self, rsp: BackendResponse) { - match rsp - .user_req - .encode_rsp(&rsp.generated.cert, &rsp.generated.key, rsp.generated.ttl) - { - Ok(buf) => { - self.stats.add_response_total(); - match self.io.send_rsp(buf.as_slice(), rsp.peer).await { - Ok(_) => { - let duration_nanos = rsp.duration(); - debug!( - "{} - duration: {}ns, rsp size: {}", - rsp.user_req.host(), - duration_nanos, - buf.len() - ); - let _ = self.duration_recorder.record(duration_nanos); - } - Err(e) => { - self.stats.add_response_fail(); - warn!("write response back error: {e:?}"); - } - } - } - Err(e) => warn!("response encode error: {e:?}"), - } - } -} diff --git a/vey-dcgen/src/frontend/stats.rs b/vey-dcgen/src/frontend/stats.rs deleted file mode 100644 index f5b8df651..000000000 --- a/vey-dcgen/src/frontend/stats.rs +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. - */ - -use std::sync::atomic::{AtomicU64, Ordering}; - -#[derive(Default)] -pub(crate) struct FrontendStats { - request_total: AtomicU64, - request_invalid: AtomicU64, - response_total: AtomicU64, - response_fail: AtomicU64, -} - -macro_rules! impl_for_field { - ($add:ident, $take:ident, $field:ident) => { - pub(crate) fn $add(&self) { - self.$field.fetch_add(1, Ordering::Relaxed); - } - - pub(crate) fn $take(&self) -> u64 { - self.$field.swap(0, Ordering::Relaxed) - } - }; -} - -impl FrontendStats { - impl_for_field!(add_request_total, take_request_total, request_total); - impl_for_field!(add_request_invalid, take_request_invalid, request_invalid); - impl_for_field!(add_response_total, take_response_total, response_total); - impl_for_field!(add_response_fail, take_response_fail, response_fail); -} diff --git a/vey-dcgen/src/frontend/udp_dgram.rs b/vey-dcgen/src/frontend/udp_dgram.rs deleted file mode 100644 index 8472b75f2..000000000 --- a/vey-dcgen/src/frontend/udp_dgram.rs +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. - */ - -use std::io; -use std::net::SocketAddr; - -use tokio::net::UdpSocket; - -use vey_types::net::UdpListenConfig; - -pub(crate) struct UdpDgramIo { - socket: UdpSocket, -} - -impl UdpDgramIo { - pub(crate) fn new(config: &UdpListenConfig) -> io::Result { - let socket = vey_socket::udp::new_std_bind_listen(config)?; - Ok(UdpDgramIo { - socket: UdpSocket::from_std(socket)?, - }) - } - - pub(crate) async fn recv_req(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> { - self.socket.recv_from(buf).await - } - - pub(crate) async fn send_rsp(&self, data: &[u8], peer: SocketAddr) -> io::Result<()> { - let nw = self.socket.send_to(data, peer).await?; - if nw != data.len() { - Err(io::Error::other(format!( - "not all data written, only {nw}/{}", - data.len() - ))) - } else { - Ok(()) - } - } -} diff --git a/vey-dcgen/src/issuer/mod.rs b/vey-dcgen/src/issuer/mod.rs new file mode 100644 index 000000000..db468b7cc --- /dev/null +++ b/vey-dcgen/src/issuer/mod.rs @@ -0,0 +1,260 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +use anyhow::anyhow; +use log::{debug, info, warn}; +use openssl::pkey::{PKey, Private}; +use openssl::x509::X509; +use tokio::runtime::Handle; + +use vey_cert_agent::Request; +use vey_histogram::{HistogramRecorder, HistogramStats}; +use vey_tls_cert::builder::{MimicCertBuilder, ServerCertBuilder, TlsServerCertBuilder}; +use vey_tls_cert::ext::X509Ext; +use vey_types::net::TlsCertUsage; + +use crate::config::OpensslIssuerConfig; + +static REQUEST_SENDER: OnceLock> = OnceLock::new(); +static DURATION_RECORDER: OnceLock> = OnceLock::new(); +static DURATION_STATS: OnceLock> = OnceLock::new(); +static ISSUER_STATS: OnceLock> = OnceLock::new(); + +pub(crate) struct GeneratedData { + pub(crate) cert: String, + pub(crate) key: Vec, + pub(crate) ttl: u32, +} + +pub(crate) struct IssuedCert { + pub(crate) user_req: Request, + pub(crate) cert: String, + pub(crate) key: Vec, + pub(crate) ttl: u32, +} + +pub(crate) enum GenerateError { + Queue(anyhow::Error), + Failed, + Dropped, +} + +struct CertRequest { + user_req: Request, + reply: tokio::sync::oneshot::Sender>, +} + +pub(crate) fn stats() -> Option> { + ISSUER_STATS.get().cloned() +} + +pub(crate) fn duration_stats() -> Option> { + DURATION_STATS.get().cloned() +} + +pub(crate) fn duration_recorder() -> &'static HistogramRecorder { + DURATION_RECORDER.get().expect("issuer is not running") +} + +pub(crate) async fn generate(user_req: Request) -> Result { + let (reply, response) = tokio::sync::oneshot::channel(); + REQUEST_SENDER + .get() + .expect("issuer is not running") + .send(CertRequest { user_req, reply }) + .await + .map_err(|e| GenerateError::Queue(anyhow!("failed to send request to issuer: {e}")))?; + match response.await { + Ok(Ok(issued)) => Ok(issued), + Ok(Err(())) => Err(GenerateError::Failed), + Err(_) => Err(GenerateError::Dropped), + } +} + +pub(crate) fn spawn_workers() -> anyhow::Result<()> { + let issuer_config = + crate::config::get_issuer_config().ok_or_else(|| anyhow!("no issuer config available"))?; + let issuer_stats = Arc::new(IssuerStats::default()); + let (duration_recorder, duration_stats) = issuer_config.duration_stats.build_spawned(None); + let (req_sender, req_receiver) = kanal::bounded_async::(1024); + + let workers = vey_daemon::runtime::worker::foreach(|h| { + let issuer = OpensslIssuer::new(&issuer_stats) + .map_err(|e| anyhow!("failed to build issuer for worker {}: {e}", h.id))?; + issuer.spawn(&h.handle, h.id, req_receiver.clone()); + Ok::<(), anyhow::Error>(()) + })?; + if workers < 1 { + let issuer = OpensslIssuer::new(&issuer_stats) + .map_err(|e| anyhow!("failed to build issuer for main runtime: {e}"))?; + issuer.spawn(&Handle::current(), 0, req_receiver); + } + + REQUEST_SENDER + .set(req_sender) + .map_err(|_| anyhow!("issuer is already running"))?; + DURATION_RECORDER + .set(duration_recorder) + .map_err(|_| anyhow!("issuer is already running"))?; + DURATION_STATS + .set(duration_stats) + .map_err(|_| anyhow!("issuer is already running"))?; + ISSUER_STATS + .set(issuer_stats) + .map_err(|_| anyhow!("issuer is already running"))?; + Ok(()) +} + +mod stats; +pub(crate) use stats::IssuerStats; + +pub(crate) struct OpensslIssuer { + builder: ServerCertBuilder, + stats: Arc, +} + +impl OpensslIssuer { + pub(crate) fn new(stats: &Arc) -> anyhow::Result { + let builder = TlsServerCertBuilder::new_ec256()?; + Ok(OpensslIssuer { + builder, + stats: Arc::clone(stats), + }) + } + + pub(crate) fn refresh(&mut self) -> anyhow::Result<()> { + self.stats.add_refresh_total(); + self.builder.refresh_datetime()?; + self.builder.refresh_ec256()?; + self.stats.add_refresh_ok(); + Ok(()) + } + + fn generate(&mut self, req: &Request) -> anyhow::Result { + let config = crate::config::get_issuer_config() + .ok_or_else(|| anyhow!("no issuer config available"))?; + self.stats.add_request_total(); + if let Some(mimic_cert) = req.cert() { + self.generate_mimic(&config, mimic_cert, req.cert_usage()) + } else { + self.builder.refresh_serial()?; + let cert = + self.builder + .build_fake(req.host(), &config.ca_cert, &config.ca_key, None)?; + let ttl = self.builder.valid_seconds()?; + self.pack_data(&config, cert, self.builder.pkey(), ttl) + } + } + + fn generate_mimic( + &self, + config: &OpensslIssuerConfig, + mimic_cert: &X509, + cert_usage: TlsCertUsage, + ) -> anyhow::Result { + let mut mimic_builder = MimicCertBuilder::new(mimic_cert)?; + mimic_builder.set_keep_serial(config.keep_serial); + + let cert = match cert_usage { + TlsCertUsage::TlsServer => { + mimic_builder.build_tls_cert(&config.ca_cert, &config.ca_key, None)? + } + TlsCertUsage::TLsServerTongsuo => mimic_builder.build_tls_cert_with_new_usage( + &config.ca_cert, + &config.ca_key, + None, + )?, + TlsCertUsage::TlcpServerEncryption => { + mimic_builder.build_tlcp_enc_cert(&config.ca_cert, &config.ca_key, None)? + } + TlsCertUsage::TlcpServerSignature => { + mimic_builder.build_tlcp_sign_cert(&config.ca_cert, &config.ca_key, None)? + } + }; + + let ttl = mimic_builder.valid_seconds()?; + + self.pack_data(config, cert, mimic_builder.pkey(), ttl) + } + + fn pack_data( + &self, + config: &OpensslIssuerConfig, + cert: X509, + pkey: &PKey, + ttl: i32, + ) -> anyhow::Result { + let ttl = ttl.clamp(0, config.max_ttl) as u32; + let mut cert_pem = cert + .to_pem() + .map_err(|e| anyhow!("failed to encode cert to PEM format: {e}"))?; + if !config.ca_cert_pem.is_empty() { + cert_pem.extend_from_slice(&config.ca_cert_pem); + } + let key = pkey + .private_key_to_der() + .map_err(|e| anyhow!("failed to encode pkey to DER format: {e}"))?; + + let data = GeneratedData { + cert: unsafe { String::from_utf8_unchecked(cert_pem) }, + key, + ttl, + }; + self.stats.add_request_ok(); + Ok(data) + } + + fn spawn( + mut self, + handle: &Handle, + id: usize, + req_receiver: kanal::AsyncReceiver, + ) { + handle.spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(300)); + loop { + tokio::select! { + _ = interval.tick() => { + if let Err(e) = self.refresh() { + warn!("failed to refresh issuer: {e:?}"); + } + } + r = req_receiver.recv() => { + let Ok(req) = r else { + break + }; + + if let Some(cert) = req.user_req.cert() { + info!("{}", cert.dump()); + } + + let host = req.user_req.host().clone(); + debug!("{host} - [#{id}] start cert generation"); + let issued = match self.generate(&req.user_req) { + Ok(data) => { + debug!("{host} - [#{id}] cert generated"); + Ok(IssuedCert { + user_req: req.user_req, + cert: data.cert, + key: data.key, + ttl: data.ttl, + }) + } + Err(e) => { + warn!("{host} - [#{id}] cert generation failed: {e:?}"); + Err(()) + } + }; + let _ = req.reply.send(issued); + } + } + } + }); + } +} diff --git a/vey-dcgen/src/backend/stats.rs b/vey-dcgen/src/issuer/stats.rs similarity index 94% rename from vey-dcgen/src/backend/stats.rs rename to vey-dcgen/src/issuer/stats.rs index 8aa7a058c..ad9c99b9c 100644 --- a/vey-dcgen/src/backend/stats.rs +++ b/vey-dcgen/src/issuer/stats.rs @@ -6,7 +6,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; #[derive(Default)] -pub(crate) struct BackendStats { +pub(crate) struct IssuerStats { refresh_total: AtomicU64, refresh_ok: AtomicU64, request_total: AtomicU64, @@ -25,7 +25,7 @@ macro_rules! impl_for_field { }; } -impl BackendStats { +impl IssuerStats { impl_for_field!(add_refresh_total, take_refresh_total, refresh_total); impl_for_field!(add_refresh_ok, take_refresh_ok, refresh_ok); impl_for_field!(add_request_total, take_request_total, request_total); diff --git a/vey-dcgen/src/lib.rs b/vey-dcgen/src/lib.rs index a12a39b0e..a5525e508 100644 --- a/vey-dcgen/src/lib.rs +++ b/vey-dcgen/src/lib.rs @@ -1,97 +1,21 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::net::SocketAddr; -use std::sync::Arc; - -use anyhow::{Context, anyhow}; -use tokio::runtime::Handle; -use tokio::time::Instant; - -use vey_cert_agent::Request; -use vey_std_ext::time::DurationExt; - pub mod config; mod build; -pub mod opts; -use opts::ProcArgs; - -mod stat; - -mod backend; -use backend::{BackendStats, OpensslBackend}; - -mod frontend; -use frontend::{Frontend, FrontendStats, GeneratedData}; +mod issuer; -struct BackendRequest { - user_req: Request, - peer: SocketAddr, - recv_time: Instant, -} +pub mod control; -struct BackendResponse { - user_req: Request, - generated: GeneratedData, - peer: SocketAddr, - recv_time: Instant, -} - -impl BackendRequest { - fn into_response(self, generated: GeneratedData) -> BackendResponse { - BackendResponse { - user_req: self.user_req, - generated, - peer: self.peer, - recv_time: self.recv_time, - } - } -} - -impl BackendResponse { - fn duration(&self) -> u64 { - self.recv_time.elapsed().as_nanos_u64() - } -} - -pub async fn run(proc_args: &ProcArgs) -> anyhow::Result<()> { - let (req_sender, req_receiver) = kanal::bounded_async::(1024); - let (rsp_sender, rsp_receiver) = kanal::bounded_async::(1024); - - let backend_config = - config::get_backend_config().ok_or_else(|| anyhow!("no backend config available"))?; - let backend_stats = Arc::new(BackendStats::default()); - - let (duration_recorder, duration_stats) = backend_config.duration_stats.build_spawned(None); - - let workers = vey_daemon::runtime::worker::foreach(|h| { - let backend = OpensslBackend::new(&backend_config, &backend_stats) - .context(format!("failed to build backend for worker {}", h.id))?; - backend.spawn(&h.handle, h.id, req_receiver.clone(), rsp_sender.clone()); - Ok::<(), anyhow::Error>(()) - })?; - if workers < 1 { - let backend = OpensslBackend::new(&backend_config, &backend_stats) - .context("failed to build backend for main runtime")?; - backend.spawn(&Handle::current(), 0, req_receiver, rsp_sender); - } else { - drop(rsp_sender); - } +pub mod opts; - let frontend = Frontend::new(proc_args.listen_config(), duration_recorder, rsp_receiver)?; +pub mod stat; - if let Some(stats_config) = vey_daemon::stat::config::get_global_stat_config() { - stat::spawn_working_thread( - stats_config, - backend_stats, - duration_stats, - frontend.stats(), - )?; - } +pub mod serve; - frontend.run(req_sender).await -} +pub mod signal; diff --git a/vey-dcgen/src/main.rs b/vey-dcgen/src/main.rs index 54682da69..8f9957558 100644 --- a/vey-dcgen/src/main.rs +++ b/vey-dcgen/src/main.rs @@ -7,6 +7,8 @@ use anyhow::Context; use log::{debug, error, info}; +use vey_daemon::control::{QuitAction, UpgradeAction}; + use vey_dcgen::opts::ProcArgs; cfg_select! { @@ -36,10 +38,18 @@ fn main() -> anyhow::Result<()> { // set up process logger early, only proc args is used inside vey_daemon::log::process::setup(&proc_args.daemon_config); + if proc_args.daemon_config.need_daemon_controller() { + vey_dcgen::control::UpgradeActor::connect_to_old_daemon(proc_args.program_name()); + } vey_daemon::runtime::config::set_default_thread_number(0); // default to use current thread - let config_file = vey_dcgen::config::load(&proc_args.daemon_config.program_name) - .context(format!("failed to load config, opts: {proc_args:?}"))?; + let config_file = match vey_dcgen::config::load(proc_args.program_name()) { + Ok(c) => c, + Err(e) => { + vey_daemon::control::upgrade::cancel_old_shutdown(); + return Err(e.context(format!("failed to load config, opts: {proc_args:?}"))); + } + }; debug!("loaded config from {}", config_file.display()); if proc_args.daemon_config.test_config { @@ -51,10 +61,26 @@ fn main() -> anyhow::Result<()> { #[cfg(all(unix, not(target_os = "macos")))] vey_daemon::daemonize::check_enter(&proc_args.daemon_config)?; + let stat_join = if let Some(stat_config) = vey_daemon::stat::config::get_global_stat_config() { + Some( + vey_dcgen::stat::spawn_working_threads(stat_config) + .context("failed to start stat thread")?, + ) + } else { + None + }; + let _workers_guard = vey_daemon::runtime::worker::spawn_workers().context("failed to spawn workers")?; let ret = tokio_run(&proc_args); + if let Some(handlers) = stat_join { + vey_dcgen::stat::stop_working_threads(); + for handle in handlers { + let _ = handle.join(); + } + } + match ret { Ok(_) => Ok(()), Err(e) => { @@ -71,8 +97,46 @@ fn tokio_run(args: &ProcArgs) -> anyhow::Result<()> { rt.block_on(async { vey_daemon::runtime::set_main_handle(); - // TODO setup signal handler + let ctl_thread_handler = vey_dcgen::control::capnp::spawn_working_thread().await?; - vey_dcgen::run(args).await + let unique_ctl = vey_dcgen::control::UniqueController::start(args.program_name()) + .context("failed to start unique controller")?; + if args.daemon_config.need_daemon_controller() { + vey_daemon::control::upgrade::release_old_controller().await; + let daemon_ctl = + vey_dcgen::control::DaemonController::start(args.program_name().to_owned()) + .context("failed to start daemon controller")?; + tokio::spawn(async move { + daemon_ctl.await; + }); + } + vey_dcgen::control::QuitActor::tokio_spawn_run(args.program_name()); + + vey_dcgen::signal::register().context("failed to setup signal handler")?; + vey_daemon::control::panic::set_hook(&args.daemon_config); + + match load_and_spawn().await { + Ok(_) => vey_daemon::control::upgrade::finish(), + Err(e) => { + vey_daemon::control::upgrade::cancel_old_shutdown(); + return Err(e); + } + } + + unique_ctl.await; + + vey_dcgen::control::capnp::stop_working_thread(); + let _ = ctl_thread_handler.join(); + + Ok(()) }) } + +async fn load_and_spawn() -> anyhow::Result<()> { + vey_dcgen::serve::spawn_issuer().context("failed to start issuer")?; + vey_dcgen::serve::spawn_offline_clean(); + vey_dcgen::serve::spawn_all() + .await + .context("failed to start servers")?; + Ok(()) +} diff --git a/vey-dcgen/src/opts.rs b/vey-dcgen/src/opts.rs index d2d05973d..6b2c7e8e5 100644 --- a/vey-dcgen/src/opts.rs +++ b/vey-dcgen/src/opts.rs @@ -4,47 +4,41 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::env; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::path::PathBuf; -use std::str::FromStr; use std::sync::OnceLock; use anyhow::{Context, anyhow}; use clap::{Arg, ArgAction, Command, ValueHint, value_parser}; use vey_daemon::opts::{DaemonArgs, DaemonArgsExt}; -use vey_types::net::{SocketBufferConfig, UdpListenConfig}; const GLOBAL_ARG_VERSION: &str = "version"; const GLOBAL_ARG_GROUP_NAME: &str = "group-name"; const GLOBAL_ARG_CONFIG_FILE: &str = "config-file"; -const GLOBAL_ARG_RECV_BUF_SIZE: &str = "recv-buf-size"; -const GLOBAL_ARG_SEND_BUF_SIZE: &str = "send-buf-size"; static DAEMON_GROUP: OnceLock = OnceLock::new(); #[derive(Debug)] pub struct ProcArgs { pub daemon_config: DaemonArgs, - listen: UdpListenConfig, } impl ProcArgs { fn new(name: String) -> Self { ProcArgs { daemon_config: DaemonArgs::new(name), - listen: UdpListenConfig::new(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2999)), } } -} -impl ProcArgs { - pub(crate) fn listen_config(&self) -> &UdpListenConfig { - &self.listen + pub fn program_name(&self) -> &str { + &self.daemon_config.program_name } } +pub(crate) fn daemon_group() -> &'static str { + DAEMON_GROUP.get().map(|s| s.as_str()).unwrap_or_default() +} + fn build_cli_args() -> Command { Command::new(crate::build::PKG_NAME) .disable_version_flag(true) @@ -59,7 +53,7 @@ fn build_cli_args() -> Command { ) .arg( Arg::new(GLOBAL_ARG_GROUP_NAME) - .help("Group name") + .help("Instance group name") .num_args(1) .value_name("GROUP NAME") .short('G') @@ -76,22 +70,6 @@ fn build_cli_args() -> Command { .short('c') .long(GLOBAL_ARG_CONFIG_FILE), ) - .arg( - Arg::new(GLOBAL_ARG_RECV_BUF_SIZE) - .help("Set receive buffer size for the listen socket") - .num_args(1) - .value_name("BUF SIZE") - .value_parser(value_parser!(usize)) - .long(GLOBAL_ARG_RECV_BUF_SIZE), - ) - .arg( - Arg::new(GLOBAL_ARG_SEND_BUF_SIZE) - .help("Set send buffer size for the listen socket") - .num_args(1) - .value_name("BUF SIZE") - .value_parser(value_parser!(usize)) - .long(GLOBAL_ARG_SEND_BUF_SIZE), - ) } pub fn parse_clap() -> anyhow::Result> { @@ -127,30 +105,7 @@ pub fn parse_clap() -> anyhow::Result> { DAEMON_GROUP .set(group_name.clone()) .map_err(|_| anyhow!("daemon group has already been set"))?; - - if let Some(s) = group_name.strip_prefix("port") - && let Ok(port) = u16::from_str(s) - { - proc_args - .listen - .set_socket_address(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)); - } - } - - if let Ok(s) = env::var("UDP_LISTEN_ADDR") - && let Ok(addr) = SocketAddr::from_str(&s) - { - proc_args.listen.set_socket_address(addr); - } - - let mut socket_buffer_config = SocketBufferConfig::default(); - if let Some(size) = args.get_one::(GLOBAL_ARG_RECV_BUF_SIZE) { - socket_buffer_config.set_recv_size(*size); - } - if let Some(size) = args.get_one::(GLOBAL_ARG_SEND_BUF_SIZE) { - socket_buffer_config.set_send_size(*size); } - proc_args.listen.set_socket_buffer(socket_buffer_config); Ok(Some(proc_args)) } diff --git a/vey-dcgen/src/serve/answer.rs b/vey-dcgen/src/serve/answer.rs new file mode 100644 index 000000000..113ec7049 --- /dev/null +++ b/vey-dcgen/src/serve/answer.rs @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::future::Future; +use std::io; + +use log::{debug, warn}; +use tokio::time::Instant; + +use vey_cert_agent::Request; +use vey_std_ext::time::DurationExt; + +use super::stats::ServerStats; +use crate::issuer; + +pub(super) async fn answer(stats: &ServerStats, packet: &[u8], peer: &str, send: F) +where + F: FnOnce(Vec) -> Fut, + Fut: Future>, +{ + let _alive = stats.enter_alive(); + stats.add_request_total(); + let recv_time = Instant::now(); + + let user_req = match Request::parse_req(packet) { + Ok(user_req) => user_req, + Err(e) => { + stats.add_request_invalid(); + warn!("invalid request from peer {peer}: {e:?}"); + return; + } + }; + debug!("{} - request received", user_req.host()); + + let generated = match issuer::generate(user_req).await { + Ok(generated) => generated, + Err(issuer::GenerateError::Failed) => return, + Err(issuer::GenerateError::Dropped) => { + warn!("issuer dropped the request"); + return; + } + Err(issuer::GenerateError::Queue(e)) => { + warn!("{e}"); + return; + } + }; + + let buf = match generated + .user_req + .encode_rsp(&generated.cert, &generated.key, generated.ttl) + { + Ok(buf) => buf, + Err(e) => { + warn!("response encode error: {e:?}"); + return; + } + }; + + stats.add_response_total(); + let rsp_len = buf.len(); + match send(buf).await { + Ok(()) => { + let duration_nanos = recv_time.elapsed().as_nanos_u64(); + debug!( + "{} - duration: {duration_nanos}ns, rsp size: {rsp_len}", + generated.user_req.host(), + ); + let _ = issuer::duration_recorder().record(duration_nanos); + } + Err(e) => { + stats.add_response_fail(); + warn!("write response back error: {e:?}"); + } + } +} diff --git a/vey-dcgen/src/serve/dummy.rs b/vey-dcgen/src/serve/dummy.rs new file mode 100644 index 000000000..2d24e0a0d --- /dev/null +++ b/vey-dcgen/src/serve/dummy.rs @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::Arc; + +use anyhow::anyhow; +use async_trait::async_trait; +use tokio::sync::broadcast; + +use vey_daemon::listen::{ListenStats, UdpResponder}; +use vey_daemon::server::{BaseServer, ServerReloadCommand}; +use vey_types::metrics::NodeName; + +use super::stats::ServerStats; +use super::{ArcServer, ArcServerInternal, Server, ServerInternal}; +use crate::config::server::{AnyServerConfig, DummyServerConfig, ServerConfig}; + +const SERVER_TYPE: &str = "DUMMY"; + +pub(super) struct DummyServer { + config: DummyServerConfig, + reload_sender: broadcast::Sender>, + listen_stats: Arc, + server_stats: Arc, + reload_version: usize, +} + +impl DummyServer { + fn new( + config: DummyServerConfig, + reload_version: usize, + listen_stats: Arc, + server_stats: Arc, + ) -> Self { + DummyServer { + config, + reload_sender: ServerReloadCommand::new_sender(), + listen_stats, + server_stats, + reload_version, + } + } + + pub(super) fn prepare_default(name: &NodeName) -> ArcServerInternal { + let config = DummyServerConfig::new(name); + let listen_stats = Arc::new(ListenStats::new(name)); + let server_stats = Arc::new(ServerStats::new(name)); + Arc::new(DummyServer::new(config, 0, listen_stats, server_stats)) + } + + pub(super) fn prepare_initial(config: DummyServerConfig) -> anyhow::Result { + let listen_stats = Arc::new(ListenStats::new(config.name())); + let server_stats = Arc::new(ServerStats::new(config.name())); + Ok(Arc::new(DummyServer::new( + config, + 1, + listen_stats, + server_stats, + ))) + } + + fn prepare_reload(&self, config: DummyServerConfig) -> DummyServer { + DummyServer::new( + config, + self.reload_version + 1, + Arc::clone(&self.listen_stats), + Arc::clone(&self.server_stats), + ) + } +} + +impl ServerInternal for DummyServer { + fn _clone_config(&self) -> AnyServerConfig { + AnyServerConfig::Dummy(self.config.clone()) + } + + fn _reload_with_new_notifier( + &self, + config: AnyServerConfig, + ) -> anyhow::Result { + let AnyServerConfig::Dummy(config) = config else { + return Err(anyhow!("config type mismatch: expect {SERVER_TYPE}")); + }; + Ok(Arc::new(self.prepare_reload(config))) + } + + fn _start_runtime(&self, _server: ArcServer) -> anyhow::Result<()> { + Ok(()) + } + + fn _abort_runtime(&self) { + let _ = self.reload_sender.send(ServerReloadCommand::QuitRuntime); + } +} + +impl BaseServer for DummyServer { + fn name(&self) -> &NodeName { + self.config.name() + } + + fn r#type(&self) -> &'static str { + SERVER_TYPE + } + + fn version(&self) -> usize { + self.reload_version + } +} + +#[async_trait] +impl Server for DummyServer { + fn listen_stats(&self) -> Arc { + Arc::clone(&self.listen_stats) + } + + fn server_stats(&self) -> Arc { + Arc::clone(&self.server_stats) + } + + async fn handle_udp_request( + &self, + _packet: &[u8], + _responder: UdpResponder, + _worker_id: Option, + ) { + } + + #[cfg(unix)] + async fn handle_unix_request( + &self, + _packet: &[u8], + _responder: &vey_daemon::listen::UnixDatagramResponder, + ) { + } +} diff --git a/vey-dcgen/src/serve/mod.rs b/vey-dcgen/src/serve/mod.rs new file mode 100644 index 000000000..cf97f4113 --- /dev/null +++ b/vey-dcgen/src/serve/mod.rs @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::Arc; + +use async_trait::async_trait; + +use vey_daemon::listen::{ListenStats, RespondUdpServer, UdpResponder}; +#[cfg(unix)] +use vey_daemon::listen::{RespondUnixDatagramServer, UnixDatagramResponder}; +use vey_daemon::server::{BaseServer, ReloadServer}; +use vey_types::metrics::NodeName; + +use crate::config::server::AnyServerConfig; + +mod registry; +pub(crate) use registry::foreach_online; +use registry::get_or_insert_default; + +mod ops; +pub use ops::{spawn_all, spawn_offline_clean}; +pub(crate) use ops::{stop_all, wait_all_tasks}; + +mod stats; +pub(crate) use stats::ServerStats; + +mod answer; + +mod dummy; +mod udp; + +#[cfg(unix)] +mod unix; + +#[async_trait] +pub(crate) trait Server: BaseServer { + fn listen_stats(&self) -> Arc; + fn server_stats(&self) -> Arc; + fn alive_count(&self) -> i32 { + self.server_stats().alive_count() + } + + async fn handle_udp_request( + &self, + packet: &[u8], + responder: UdpResponder, + worker_id: Option, + ); + + #[cfg(unix)] + async fn handle_unix_request(&self, packet: &[u8], responder: &UnixDatagramResponder); +} + +trait ServerInternal: Server { + fn _clone_config(&self) -> AnyServerConfig; + + fn _reload_with_new_notifier( + &self, + config: AnyServerConfig, + ) -> anyhow::Result; + + fn _start_runtime(&self, server: ArcServer) -> anyhow::Result<()>; + fn _abort_runtime(&self); +} + +pub(crate) type ArcServer = Arc; +type ArcServerInternal = Arc; + +#[derive(Clone)] +struct WrapArcServer(ArcServer); + +impl BaseServer for WrapArcServer { + fn name(&self) -> &NodeName { + self.0.name() + } + + fn r#type(&self) -> &'static str { + self.0.r#type() + } + + fn version(&self) -> usize { + self.0.version() + } +} + +impl ReloadServer for WrapArcServer { + fn reload(&self) -> Self { + WrapArcServer(get_or_insert_default(self.name())) + } +} + +impl RespondUdpServer for WrapArcServer { + async fn handle_udp_request( + &self, + packet: &[u8], + responder: UdpResponder, + worker_id: Option, + ) { + self.0 + .handle_udp_request(packet, responder, worker_id) + .await + } +} + +#[cfg(unix)] +impl RespondUnixDatagramServer for WrapArcServer { + async fn handle_unix_request(&self, packet: &[u8], responder: &UnixDatagramResponder) { + self.0.handle_unix_request(packet, responder).await + } +} + +pub fn spawn_issuer() -> anyhow::Result<()> { + crate::issuer::spawn_workers() +} diff --git a/vey-dcgen/src/serve/ops.rs b/vey-dcgen/src/serve/ops.rs new file mode 100644 index 000000000..d536ca0ef --- /dev/null +++ b/vey-dcgen/src/serve/ops.rs @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; + +use log::debug; +use tokio::sync::Mutex; + +use vey_types::metrics::NodeName; + +use super::registry; +use crate::config::server::{AnyServerConfig, ServerConfigDiffAction}; + +static SERVER_OPS_LOCK: Mutex<()> = Mutex::const_new(()); + +/// Shutdown checks this often. A datagram listener stops within the reuseport drain window. +const QUIT_POLL_INTERVAL: Duration = Duration::from_millis(200); + +pub fn spawn_offline_clean() { + tokio::spawn(async { + let mut interval = tokio::time::interval(Duration::from_secs(60)); + interval.tick().await; + loop { + registry::retain_offline(); + interval.tick().await; + } + }); +} + +pub async fn spawn_all() -> anyhow::Result<()> { + let _guard = SERVER_OPS_LOCK.lock().await; + + let mut new_names = HashSet::new(); + for config in crate::config::server::get_all() { + let name = config.name().clone(); + new_names.insert(name.clone()); + match registry::get_config(&name) { + Some(old) => { + debug!("reloading server {name}"); + reload_old_unlocked(old, config)?; + debug!("server {name} reload OK"); + } + None => { + debug!("creating server {name}"); + spawn_new_unlocked(config)?; + debug!("server {name} create OK"); + } + } + } + + for name in registry::get_names() { + if !new_names.contains(&name) { + debug!("deleting server {name}"); + registry::del(&name); + debug!("server {name} deleted"); + } + } + Ok(()) +} + +fn reload_old_unlocked(old: AnyServerConfig, new: AnyServerConfig) -> anyhow::Result<()> { + let name = old.name().clone(); + match old.diff_action(&new) { + ServerConfigDiffAction::NoAction => { + debug!("server {name} reload: no action is needed"); + Ok(()) + } + ServerConfigDiffAction::SpawnNew => { + debug!("server {name} reload: will create a new server"); + spawn_new_unlocked(new) + } + ServerConfigDiffAction::ReloadAndRespawn => { + debug!("server {name} reload: will respawn with old stats"); + registry::reload_and_respawn(&name, new) + } + } +} + +fn spawn_new_unlocked(config: AnyServerConfig) -> anyhow::Result<()> { + let server = match config { + AnyServerConfig::Dummy(config) => super::dummy::DummyServer::prepare_initial(config)?, + AnyServerConfig::Udp(config) => super::udp::UdpServer::prepare_initial(config)?, + #[cfg(unix)] + AnyServerConfig::Unix(config) => super::unix::UnixServer::prepare_initial(config)?, + }; + registry::add(server) +} + +pub(crate) async fn stop_all() { + let _guard = SERVER_OPS_LOCK.lock().await; + + registry::foreach_online_internal(|server| { + server._abort_runtime(); + registry::add_offline(Arc::clone(server)); + }); +} + +pub(crate) async fn wait_all_tasks(wait_timeout: Duration, quit_timeout: Duration, on_timeout: F) +where + F: Fn(&NodeName, i32), +{ + let loop_wait = async { + loop { + let mut has_pending = false; + + registry::foreach_offline(|server| { + if server.alive_count() > 0 || server.listen_stats().is_running() { + has_pending = true; + } + }); + + if !has_pending { + if let Some(stat_config) = vey_daemon::stat::config::get_global_stat_config() { + tokio::time::sleep(stat_config.emit_interval * 2).await; + } + break; + } + + tokio::time::sleep(QUIT_POLL_INTERVAL).await; + } + }; + + tokio::pin!(loop_wait); + + debug!("will wait {wait_timeout:?} for all tasks to be finished"); + if tokio::time::timeout(wait_timeout, &mut loop_wait) + .await + .is_ok() + { + return; + } + + debug!("will wait {quit_timeout:?} for remaining tasks"); + if tokio::time::timeout(quit_timeout, &mut loop_wait) + .await + .is_err() + { + registry::foreach_offline(|server| { + let mut left = server.alive_count(); + if left == 0 && server.listen_stats().is_running() { + left = 1; + } + if left > 0 { + on_timeout(server.name(), left); + } + }); + } +} diff --git a/vey-dcgen/src/serve/registry.rs b/vey-dcgen/src/serve/registry.rs new file mode 100644 index 000000000..26e5474a9 --- /dev/null +++ b/vey-dcgen/src/serve/registry.rs @@ -0,0 +1,143 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex}; + +use anyhow::anyhow; +use foldhash::fast::FixedState; + +use vey_types::metrics::NodeName; + +use super::{ArcServer, ArcServerInternal}; +use crate::config::server::AnyServerConfig; + +static RUNTIME_SERVER_REGISTRY: Mutex = Mutex::new(ServerRegistry::new()); +static OFFLINE_SERVER_SET: Mutex> = Mutex::new(Vec::new()); + +struct ServerRegistry { + inner: HashMap, +} + +impl ServerRegistry { + const fn new() -> Self { + ServerRegistry { + inner: HashMap::with_hasher(FixedState::with_seed(0)), + } + } + + fn add(&mut self, name: NodeName, server: ArcServerInternal) -> anyhow::Result<()> { + server._start_runtime(server.clone())?; + if let Some(old_server) = self.inner.insert(name, server) { + old_server._abort_runtime(); + add_offline(old_server); + } + Ok(()) + } + + fn del(&mut self, name: &NodeName) { + if let Some(old_server) = self.inner.remove(name) { + old_server._abort_runtime(); + add_offline(old_server); + } + } + + fn get_or_insert_default(&mut self, name: &NodeName) -> ArcServer { + let server = self + .inner + .entry(name.clone()) + .or_insert_with(|| super::dummy::DummyServer::prepare_default(name)); + let server: ArcServer = server.clone(); + server + } + + fn reload_and_respawn( + &mut self, + name: &NodeName, + config: AnyServerConfig, + ) -> anyhow::Result<()> { + let Some(old_server) = self.inner.get(name).cloned() else { + return Err(anyhow!("no server with name {name} found")); + }; + let server = old_server._reload_with_new_notifier(config)?; + self.add(name.clone(), server) + } +} + +pub(super) fn add_offline(old_server: ArcServerInternal) { + let mut set = OFFLINE_SERVER_SET.lock().unwrap(); + set.push(old_server); +} + +pub(super) fn retain_offline() { + let mut set = OFFLINE_SERVER_SET.lock().unwrap(); + set.retain(|server| server.alive_count() > 0 || Arc::strong_count(server) > 1); +} + +pub(super) fn foreach_offline(mut f: F) +where + F: FnMut(&ArcServerInternal), +{ + let set = OFFLINE_SERVER_SET.lock().unwrap(); + for server in set.iter() { + f(server) + } +} + +pub(super) fn add(server: ArcServerInternal) -> anyhow::Result<()> { + let name = server.name().clone(); + let mut sr = RUNTIME_SERVER_REGISTRY + .lock() + .map_err(|e| anyhow!("failed to lock server registry: {e}"))?; + sr.add(name, server) +} + +pub(super) fn del(name: &NodeName) { + let mut sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + sr.del(name); +} + +pub(super) fn get_names() -> HashSet { + let sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + sr.inner.keys().cloned().collect() +} + +pub(super) fn get_config(name: &NodeName) -> Option { + let sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + sr.inner.get(name).map(|server| server._clone_config()) +} + +pub(super) fn get_or_insert_default(name: &NodeName) -> ArcServer { + let mut sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + sr.get_or_insert_default(name) +} + +pub(super) fn reload_and_respawn(name: &NodeName, config: AnyServerConfig) -> anyhow::Result<()> { + let mut sr = RUNTIME_SERVER_REGISTRY + .lock() + .map_err(|e| anyhow!("failed to lock server registry: {e}"))?; + sr.reload_and_respawn(name, config) +} + +pub(crate) fn foreach_online(mut f: F) +where + F: FnMut(&ArcServer), +{ + let sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + for server in sr.inner.values() { + let server: ArcServer = server.clone(); + f(&server); + } +} + +pub(super) fn foreach_online_internal(mut f: F) +where + F: FnMut(&ArcServerInternal), +{ + let sr = RUNTIME_SERVER_REGISTRY.lock().unwrap(); + for server in sr.inner.values() { + f(server); + } +} diff --git a/vey-dcgen/src/serve/stats.rs b/vey-dcgen/src/serve/stats.rs new file mode 100644 index 000000000..89a2ec0cd --- /dev/null +++ b/vey-dcgen/src/serve/stats.rs @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::atomic::{AtomicI32, AtomicU64, Ordering}; + +use vey_types::metrics::NodeName; +use vey_types::stats::StatId; + +pub(crate) struct ServerStats { + name: NodeName, + id: StatId, + alive: AtomicI32, + request_total: AtomicU64, + request_invalid: AtomicU64, + response_total: AtomicU64, + response_fail: AtomicU64, +} + +impl ServerStats { + pub(crate) fn new(name: &NodeName) -> Self { + ServerStats { + name: name.clone(), + id: StatId::new_unique(), + alive: AtomicI32::new(0), + request_total: AtomicU64::new(0), + request_invalid: AtomicU64::new(0), + response_total: AtomicU64::new(0), + response_fail: AtomicU64::new(0), + } + } + + pub(crate) fn name(&self) -> &NodeName { + &self.name + } + + pub(crate) fn stat_id(&self) -> StatId { + self.id + } + + pub(super) fn enter_alive(&self) -> AliveGuard<'_> { + self.alive.fetch_add(1, Ordering::Relaxed); + AliveGuard(self) + } + + pub(crate) fn alive_count(&self) -> i32 { + self.alive.load(Ordering::Relaxed) + } + + pub(crate) fn add_request_total(&self) { + self.request_total.fetch_add(1, Ordering::Relaxed); + } + + pub(crate) fn take_request_total(&self) -> u64 { + self.request_total.swap(0, Ordering::Relaxed) + } + + pub(crate) fn add_request_invalid(&self) { + self.request_invalid.fetch_add(1, Ordering::Relaxed); + } + + pub(crate) fn take_request_invalid(&self) -> u64 { + self.request_invalid.swap(0, Ordering::Relaxed) + } + + pub(crate) fn add_response_total(&self) { + self.response_total.fetch_add(1, Ordering::Relaxed); + } + + pub(crate) fn take_response_total(&self) -> u64 { + self.response_total.swap(0, Ordering::Relaxed) + } + + pub(crate) fn add_response_fail(&self) { + self.response_fail.fetch_add(1, Ordering::Relaxed); + } + + pub(crate) fn take_response_fail(&self) -> u64 { + self.response_fail.swap(0, Ordering::Relaxed) + } +} + +pub(super) struct AliveGuard<'a>(&'a ServerStats); + +impl Drop for AliveGuard<'_> { + fn drop(&mut self) { + self.0.alive.fetch_sub(1, Ordering::Relaxed); + } +} diff --git a/vey-dcgen/src/serve/udp.rs b/vey-dcgen/src/serve/udp.rs new file mode 100644 index 000000000..baebda8e7 --- /dev/null +++ b/vey-dcgen/src/serve/udp.rs @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::Arc; + +use anyhow::anyhow; +use async_trait::async_trait; +use tokio::sync::broadcast; + +use vey_daemon::listen::{ListenStats, RespondUdpRuntime, UdpResponder}; +use vey_daemon::server::{BaseServer, ServerReloadCommand}; +use vey_types::metrics::NodeName; + +use super::answer; +use super::stats::ServerStats; +use super::{ArcServer, ArcServerInternal, Server, ServerInternal, WrapArcServer}; +use crate::config::server::{AnyServerConfig, ServerConfig, UdpServerConfig}; + +const SERVER_TYPE: &str = "UDP"; +const UDP_PACKET_MAX_SIZE: u16 = 16384; + +pub(super) struct UdpServer { + config: UdpServerConfig, + reload_sender: broadcast::Sender>, + listen_stats: Arc, + server_stats: Arc, + reload_version: usize, +} + +impl UdpServer { + fn new( + config: UdpServerConfig, + reload_version: usize, + listen_stats: Arc, + server_stats: Arc, + ) -> Self { + UdpServer { + config, + reload_sender: ServerReloadCommand::new_sender(), + listen_stats, + server_stats, + reload_version, + } + } + + pub(super) fn prepare_initial(config: UdpServerConfig) -> anyhow::Result { + let listen_stats = Arc::new(ListenStats::new(config.name())); + let server_stats = Arc::new(ServerStats::new(config.name())); + Ok(Arc::new(UdpServer::new( + config, + 1, + listen_stats, + server_stats, + ))) + } + + fn prepare_reload(&self, config: UdpServerConfig) -> UdpServer { + UdpServer::new( + config, + self.reload_version + 1, + Arc::clone(&self.listen_stats), + Arc::clone(&self.server_stats), + ) + } +} + +impl ServerInternal for UdpServer { + fn _clone_config(&self) -> AnyServerConfig { + AnyServerConfig::Udp(self.config.clone()) + } + + fn _reload_with_new_notifier( + &self, + config: AnyServerConfig, + ) -> anyhow::Result { + let AnyServerConfig::Udp(config) = config else { + return Err(anyhow!("config type mismatch: expect {SERVER_TYPE}")); + }; + Ok(Arc::new(self.prepare_reload(config))) + } + + fn _start_runtime(&self, server: ArcServer) -> anyhow::Result<()> { + let mut runtime = RespondUdpRuntime::new( + WrapArcServer(server), + self.config.listen.clone(), + Arc::clone(&self.listen_stats), + UDP_PACKET_MAX_SIZE, + ); + if let Err(e) = runtime.run_all_instances(true, &self.reload_sender) { + let _ = self.reload_sender.send(ServerReloadCommand::QuitRuntime); + return Err(e); + } + Ok(()) + } + + fn _abort_runtime(&self) { + let _ = self.reload_sender.send(ServerReloadCommand::QuitRuntime); + } +} + +impl BaseServer for UdpServer { + fn name(&self) -> &NodeName { + self.config.name() + } + + fn r#type(&self) -> &'static str { + SERVER_TYPE + } + + fn version(&self) -> usize { + self.reload_version + } +} + +#[async_trait] +impl Server for UdpServer { + fn listen_stats(&self) -> Arc { + Arc::clone(&self.listen_stats) + } + + fn server_stats(&self) -> Arc { + Arc::clone(&self.server_stats) + } + + async fn handle_udp_request( + &self, + packet: &[u8], + responder: UdpResponder, + _worker_id: Option, + ) { + let peer = responder.client_addr().to_string(); + answer::answer(&self.server_stats, packet, &peer, move |buf| { + let responder = responder.clone(); + async move { responder.send(&buf).await } + }) + .await; + } + + #[cfg(unix)] + async fn handle_unix_request( + &self, + _packet: &[u8], + _responder: &vey_daemon::listen::UnixDatagramResponder, + ) { + } +} diff --git a/vey-dcgen/src/serve/unix.rs b/vey-dcgen/src/serve/unix.rs new file mode 100644 index 000000000..d232dcb87 --- /dev/null +++ b/vey-dcgen/src/serve/unix.rs @@ -0,0 +1,144 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use std::sync::Arc; + +use anyhow::anyhow; +use async_trait::async_trait; +use tokio::sync::broadcast; + +use vey_daemon::listen::{ + ListenStats, RespondUnixDatagramRuntime, UdpResponder, UnixDatagramResponder, +}; +use vey_daemon::server::{BaseServer, ServerReloadCommand}; +use vey_types::metrics::NodeName; + +use super::answer; +use super::stats::ServerStats; +use super::{ArcServer, ArcServerInternal, Server, ServerInternal, WrapArcServer}; +use crate::config::server::{AnyServerConfig, ServerConfig, UnixServerConfig}; + +const SERVER_TYPE: &str = "UNIX"; + +pub(super) struct UnixServer { + config: UnixServerConfig, + reload_sender: broadcast::Sender>, + listen_stats: Arc, + server_stats: Arc, + reload_version: usize, +} + +impl UnixServer { + fn new( + config: UnixServerConfig, + reload_version: usize, + listen_stats: Arc, + server_stats: Arc, + ) -> Self { + UnixServer { + config, + reload_sender: ServerReloadCommand::new_sender(), + listen_stats, + server_stats, + reload_version, + } + } + + pub(super) fn prepare_initial(config: UnixServerConfig) -> anyhow::Result { + let listen_stats = Arc::new(ListenStats::new(config.name())); + let server_stats = Arc::new(ServerStats::new(config.name())); + Ok(Arc::new(UnixServer::new( + config, + 1, + listen_stats, + server_stats, + ))) + } + + fn prepare_reload(&self, config: UnixServerConfig) -> UnixServer { + UnixServer::new( + config, + self.reload_version + 1, + Arc::clone(&self.listen_stats), + Arc::clone(&self.server_stats), + ) + } +} + +impl ServerInternal for UnixServer { + fn _clone_config(&self) -> AnyServerConfig { + AnyServerConfig::Unix(self.config.clone()) + } + + fn _reload_with_new_notifier( + &self, + config: AnyServerConfig, + ) -> anyhow::Result { + let AnyServerConfig::Unix(config) = config else { + return Err(anyhow!("config type mismatch: expect {SERVER_TYPE}")); + }; + Ok(Arc::new(self.prepare_reload(config))) + } + + fn _start_runtime(&self, server: ArcServer) -> anyhow::Result<()> { + let runtime = RespondUnixDatagramRuntime::new( + WrapArcServer(server), + self.config.listen.clone(), + Arc::clone(&self.listen_stats), + ); + if let Err(e) = runtime.spawn(&self.reload_sender) { + let _ = self.reload_sender.send(ServerReloadCommand::QuitRuntime); + return Err(e); + } + Ok(()) + } + + fn _abort_runtime(&self) { + let _ = self.reload_sender.send(ServerReloadCommand::QuitRuntime); + } +} + +impl BaseServer for UnixServer { + fn name(&self) -> &NodeName { + self.config.name() + } + + fn r#type(&self) -> &'static str { + SERVER_TYPE + } + + fn version(&self) -> usize { + self.reload_version + } +} + +#[async_trait] +impl Server for UnixServer { + fn listen_stats(&self) -> Arc { + Arc::clone(&self.listen_stats) + } + + fn server_stats(&self) -> Arc { + Arc::clone(&self.server_stats) + } + + async fn handle_udp_request( + &self, + _packet: &[u8], + _responder: UdpResponder, + _worker_id: Option, + ) { + } + + async fn handle_unix_request(&self, packet: &[u8], responder: &UnixDatagramResponder) { + let peer = format!("{:?}", responder.client_addr()); + let responder = responder.clone(); + answer::answer(&self.server_stats, packet, &peer, move |buf| { + let responder = responder.clone(); + async move { responder.send(&buf).await } + }) + .await; + } +} diff --git a/vey-dcgen/src/signal.rs b/vey-dcgen/src/signal.rs new file mode 100644 index 000000000..21d2bd411 --- /dev/null +++ b/vey-dcgen/src/signal.rs @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::Context; +use log::{info, warn}; +use tokio::sync::Mutex; + +use vey_daemon::signal::AsyncSignalAction; + +static RELOAD_MUTEX: Mutex<()> = Mutex::const_new(()); + +pub(crate) async fn reload_issuer() -> anyhow::Result<()> { + let _guard = RELOAD_MUTEX.lock().await; + info!("reloading issuer"); + + match crate::config::reload_issuer().await { + Ok(()) => { + info!("issuer reload finished"); + Ok(()) + } + Err(e) => { + warn!("issuer reload error: {e:?}"); + warn!("issuer reload aborted"); + Err(e) + } + } +} + +pub(crate) async fn reload() -> anyhow::Result<()> { + let _guard = RELOAD_MUTEX.lock().await; + info!("reloading config"); + + match reload_locked().await { + Ok(_) => { + info!("reload finished"); + Ok(()) + } + Err(e) => { + warn!("reload error: {e:?}"); + warn!("reload aborted"); + Err(e) + } + } +} + +async fn reload_locked() -> anyhow::Result<()> { + crate::config::reload() + .await + .context("failed to reload config")?; + crate::serve::spawn_all() + .await + .context("failed to reload all servers")?; + Ok(()) +} + +#[derive(Clone, Copy)] +struct QuitAction {} + +impl AsyncSignalAction for QuitAction { + async fn run(&self) { + vey_daemon::control::quit::trigger_force_shutdown(); + } +} + +#[cfg(unix)] +mod unix { + use vey_daemon::signal::AsyncSignalAction; + + #[derive(Clone, Copy)] + struct OfflineAction {} + + impl AsyncSignalAction for OfflineAction { + async fn run(&self) { + vey_daemon::control::quit::start_graceful_shutdown().await; + } + } + + #[derive(Clone, Copy)] + struct ReloadAction {} + + impl AsyncSignalAction for ReloadAction { + async fn run(&self) { + let _ = super::reload().await; + } + } + + pub(super) fn register() -> anyhow::Result<()> { + vey_daemon::signal::register_reload(ReloadAction {})?; + vey_daemon::signal::register_offline(OfflineAction {})?; + Ok(()) + } +} + +pub fn register() -> anyhow::Result<()> { + #[cfg(unix)] + unix::register()?; + vey_daemon::signal::register_quit(QuitAction {}) +} diff --git a/vey-dcgen/src/stat/metrics/frontend.rs b/vey-dcgen/src/stat/metrics/frontend.rs deleted file mode 100644 index b6d7a2e64..000000000 --- a/vey-dcgen/src/stat/metrics/frontend.rs +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. - */ - -use vey_statsd_client::StatsdClient; - -use crate::FrontendStats; - -pub(crate) fn emit_stats(client: &mut StatsdClient, s: &FrontendStats) { - macro_rules! emit_count { - ($take:ident, $name:literal) => { - let v = s.$take(); - client.count(concat!("frontend.", $name), v).send(); - }; - } - - emit_count!(take_request_total, "request_total"); - emit_count!(take_request_invalid, "request_invalid"); - emit_count!(take_response_total, "response_total"); - emit_count!(take_response_fail, "response_fail"); -} diff --git a/vey-dcgen/src/stat/metrics/backend.rs b/vey-dcgen/src/stat/metrics/issuer.rs similarity index 77% rename from vey-dcgen/src/stat/metrics/backend.rs rename to vey-dcgen/src/stat/metrics/issuer.rs index 98f97231a..b5796c7f8 100644 --- a/vey-dcgen/src/stat/metrics/backend.rs +++ b/vey-dcgen/src/stat/metrics/issuer.rs @@ -7,13 +7,13 @@ use vey_daemon::metrics::TAG_KEY_QUANTILE; use vey_histogram::HistogramStats; use vey_statsd_client::StatsdClient; -use crate::BackendStats; +use crate::issuer::IssuerStats; -pub(crate) fn emit_stats(client: &mut StatsdClient, s: &BackendStats) { +pub(crate) fn emit_stats(client: &mut StatsdClient, s: &IssuerStats) { macro_rules! emit_count { ($take:ident, $name:literal) => { let v = s.$take(); - client.count(concat!("backend.", $name), v).send(); + client.count(concat!("issuer.", $name), v).send(); }; } @@ -26,7 +26,7 @@ pub(crate) fn emit_stats(client: &mut StatsdClient, s: &BackendStats) { pub(crate) fn emit_duration_stats(client: &mut StatsdClient, s: &HistogramStats) { s.foreach_stat(|_, qs, v| { client - .gauge_float("backend.request_duration", v) + .gauge_float("issuer.request_duration", v) .with_tag(TAG_KEY_QUANTILE, qs) .send(); }); diff --git a/vey-dcgen/src/stat/metrics/mod.rs b/vey-dcgen/src/stat/metrics/mod.rs index 8dcb2410e..940b9944d 100644 --- a/vey-dcgen/src/stat/metrics/mod.rs +++ b/vey-dcgen/src/stat/metrics/mod.rs @@ -1,7 +1,81 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -pub(super) mod backend; -pub(super) mod frontend; +use std::sync::{Arc, Mutex}; + +use vey_daemon::listen::{ListenSnapshot, ListenStats}; +use vey_daemon::metrics::{TAG_KEY_SERVER, emit_listen_stats}; +use vey_statsd_client::{StatsdClient, StatsdTagGroup}; +use vey_types::stats::GlobalStatsMap; + +use crate::serve::ServerStats; + +mod issuer; + +static SERVER_STATS_MAP: Mutex>> = + Mutex::new(GlobalStatsMap::new()); +static LISTEN_STATS_MAP: Mutex, ListenSnapshot)>> = + Mutex::new(GlobalStatsMap::new()); + +pub(super) fn emit_stats(client: &mut StatsdClient) { + if let Some(stats) = crate::issuer::stats() { + issuer::emit_stats(client, &stats); + } + if let Some(stats) = crate::issuer::duration_stats() { + issuer::emit_duration_stats(client, &stats); + } + + let mut server_stats = Vec::new(); + let mut listen_stats = Vec::new(); + crate::serve::foreach_online(|server| { + server_stats.push(server.server_stats()); + listen_stats.push(server.listen_stats()); + }); + + let mut server_stats_map = SERVER_STATS_MAP.lock().unwrap(); + for stats in server_stats { + let stat_id = stats.stat_id(); + server_stats_map.get_or_insert_with(stat_id, || stats); + } + server_stats_map.retain(|stats| { + emit_server_stats(client, stats); + Arc::strong_count(stats) > 1 + }); + drop(server_stats_map); + + let mut listen_stats_map = LISTEN_STATS_MAP.lock().unwrap(); + for stats in listen_stats { + let stat_id = stats.stat_id(); + listen_stats_map.get_or_insert_with(stat_id, || (stats, ListenSnapshot::default())); + } + listen_stats_map.retain(|(stats, snap)| { + emit_listen_stats(client, stats, snap); + Arc::strong_count(stats) > 1 + }); +} + +fn emit_server_stats(client: &mut StatsdClient, stats: &Arc) { + let mut tags = StatsdTagGroup::default(); + tags.add_tag(TAG_KEY_SERVER, stats.name()); + + macro_rules! emit_count { + ($take:ident, $name:literal) => { + let v = stats.$take(); + client + .count_with_tags(concat!("server.", $name), v, &tags) + .send(); + }; + } + + emit_count!(take_request_total, "request_total"); + emit_count!(take_request_invalid, "request_invalid"); + emit_count!(take_response_total, "response_total"); + emit_count!(take_response_fail, "response_fail"); + + client + .gauge_with_tags("server.request_alive", stats.alive_count(), &tags) + .send(); +} diff --git a/vey-dcgen/src/stat/mod.rs b/vey-dcgen/src/stat/mod.rs index d4f2efb04..91e0bdb3f 100644 --- a/vey-dcgen/src/stat/mod.rs +++ b/vey-dcgen/src/stat/mod.rs @@ -4,45 +4,62 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::thread::JoinHandle; use std::time::Instant; -use anyhow::anyhow; +use anyhow::{Context, anyhow}; -use vey_histogram::HistogramStats; -use vey_statsd_client::StatsdClientConfig; +use vey_statsd_client::{StatsdClient, StatsdClientConfig}; mod metrics; -use super::{BackendStats, FrontendStats}; +static QUIT_STAT_THREAD: AtomicBool = AtomicBool::new(false); -pub(crate) fn spawn_working_thread( - config: StatsdClientConfig, - backend_stats: Arc, - backend_duration_stats: Arc, - frontend_stats: Arc, -) -> anyhow::Result> { - let mut client = config +fn build_statsd_client(config: &StatsdClientConfig) -> anyhow::Result { + let client = config .build() .map_err(|e| anyhow!("failed to build statsd client: {e}"))?; + Ok(client.with_tag( + vey_daemon::metrics::TAG_KEY_DAEMON_GROUP, + crate::opts::daemon_group(), + )) +} + +fn spawn_main_thread(config: &StatsdClientConfig) -> anyhow::Result> { + let mut client = build_statsd_client(config)?; + + let emit_interval = config.emit_interval; let handle = std::thread::Builder::new() - .name("stat-main".into()) + .name("stat".into()) .spawn(move || { loop { let instant_start = Instant::now(); - metrics::backend::emit_stats(&mut client, &backend_stats); - metrics::backend::emit_duration_stats(&mut client, &backend_duration_stats); - metrics::frontend::emit_stats(&mut client, &frontend_stats); + metrics::emit_stats(&mut client); vey_daemon::runtime::metrics::emit_stats(&mut client); client.flush_sink(); - vey_daemon::stat::emit::wait_duration(config.emit_interval, instant_start); + if QUIT_STAT_THREAD.load(Ordering::Relaxed) { + break; + } + + vey_daemon::stat::emit::wait_duration(emit_interval, instant_start); } }) .map_err(|e| anyhow!("failed to spawn thread: {e:?}"))?; Ok(handle) } + +pub fn spawn_working_threads(config: StatsdClientConfig) -> anyhow::Result>> { + let mut handlers = Vec::with_capacity(2); + let main_handle = spawn_main_thread(&config).context("failed to spawn main stats thread")?; + handlers.push(main_handle); + Ok(handlers) +} + +pub fn stop_working_threads() { + QUIT_STAT_THREAD.store(true, Ordering::Relaxed); +} diff --git a/vey-dcgen/utils/ctl/Cargo.toml b/vey-dcgen/utils/ctl/Cargo.toml new file mode 100644 index 000000000..e35400f0c --- /dev/null +++ b/vey-dcgen/utils/ctl/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "vey-dcgen-ctl" +version = "0.1.0" +license.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +capnp.workspace = true +clap.workspace = true +tokio = { workspace = true, features = ["rt", "macros", "io-util", "fs"] } +vey-ctl.workspace = true +vey-dcgen-proto = { path = "../../proto" } diff --git a/vey-dcgen/utils/ctl/src/common.rs b/vey-dcgen/utils/ctl/src/common.rs new file mode 100644 index 000000000..a7f24daf1 --- /dev/null +++ b/vey-dcgen/utils/ctl/src/common.rs @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::anyhow; + +use vey_ctl::{CommandError, CommandResult}; + +use vey_dcgen_proto::types_capnp::operation_result; + +pub(crate) fn parse_operation_result(r: operation_result::Reader<'_>) -> CommandResult<()> { + match r + .which() + .map_err(|e| anyhow!("invalid operation result: {e}"))? + { + operation_result::Which::Ok(ok) => vey_ctl::print_ok_notice(ok?), + operation_result::Which::Err(err) => { + let e = err?; + Err(CommandError::api_error(e.get_code(), e.get_reason()?)) + } + } +} diff --git a/vey-dcgen/utils/ctl/src/main.rs b/vey-dcgen/utils/ctl/src/main.rs new file mode 100644 index 000000000..9e3ca1714 --- /dev/null +++ b/vey-dcgen/utils/ctl/src/main.rs @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use anyhow::anyhow; +use clap::Command; + +use vey_ctl::{CommandError, DaemonCtlArgs, DaemonCtlArgsExt}; + +use vey_dcgen_proto::proc_capnp::proc_control; + +mod common; +mod proc; + +fn build_cli_args() -> Command { + Command::new(env!("CARGO_PKG_NAME")) + .append_daemon_ctl_args() + .subcommand(proc::commands::version()) + .subcommand(proc::commands::offline()) + .subcommand(proc::commands::reload()) + .subcommand(proc::commands::reload_issuer()) + .subcommand(proc::commands::cancel_shutdown()) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> anyhow::Result<()> { + let mut args_parser = build_cli_args(); + let args = args_parser.get_matches_mut(); + + let mut ctl_opts = DaemonCtlArgs::parse_clap(&args); + if ctl_opts.generate_shell_completion(build_cli_args) { + return Ok(()); + } + + let bin_name = args_parser.get_bin_name().unwrap_or(args_parser.get_name()); + let daemon_name = bin_name.strip_suffix("-ctl").unwrap_or("vey-dcgen"); + let (rpc_system, proc_control) = ctl_opts + .connect_rpc::(daemon_name) + .await?; + + tokio::task::LocalSet::new() + .run_until(async move { + tokio::task::spawn_local(async move { + rpc_system + .await + .map_err(|e| eprintln!("rpc system error: {e:?}")) + }); + + let (subcommand, _sub_args) = args.subcommand().unwrap(); + match subcommand { + proc::COMMAND_VERSION => proc::version(&proc_control).await, + proc::COMMAND_OFFLINE => proc::offline(&proc_control).await, + proc::COMMAND_RELOAD => proc::reload(&proc_control).await, + proc::COMMAND_RELOAD_ISSUER => proc::reload_issuer(&proc_control).await, + proc::COMMAND_CANCEL_SHUTDOWN => proc::cancel_shutdown(&proc_control).await, + _ => Err(CommandError::Cli(anyhow!( + "unsupported command {subcommand}" + ))), + } + }) + .await + .map_err(anyhow::Error::new) +} diff --git a/vey-dcgen/utils/ctl/src/proc.rs b/vey-dcgen/utils/ctl/src/proc.rs new file mode 100644 index 000000000..7f23ac040 --- /dev/null +++ b/vey-dcgen/utils/ctl/src/proc.rs @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. + */ + +use clap::Command; + +use vey_ctl::CommandResult; + +use vey_dcgen_proto::proc_capnp::proc_control; + +use crate::common::parse_operation_result; + +pub const COMMAND_VERSION: &str = "version"; +pub const COMMAND_OFFLINE: &str = "offline"; +pub const COMMAND_RELOAD: &str = "reload"; +pub const COMMAND_RELOAD_ISSUER: &str = "reload-issuer"; +pub const COMMAND_CANCEL_SHUTDOWN: &str = "cancel-shutdown"; + +pub mod commands { + use super::*; + + pub fn version() -> Command { + Command::new(COMMAND_VERSION) + } + + pub fn offline() -> Command { + Command::new(COMMAND_OFFLINE).about("Put this daemon into offline mode") + } + + pub fn reload() -> Command { + Command::new(COMMAND_RELOAD).about("Reload servers and the issuer from the config file") + } + + pub fn reload_issuer() -> Command { + Command::new(COMMAND_RELOAD_ISSUER).about("Reload the issuer from the config file") + } + + pub fn cancel_shutdown() -> Command { + Command::new(COMMAND_CANCEL_SHUTDOWN) + .about("Cancel the shutdown progress if the daemon is still in shutdown wait state") + } +} + +pub async fn version(client: &proc_control::Client) -> CommandResult<()> { + let req = client.version_request(); + let rsp = req.send().promise.await?; + vey_ctl::print_version(rsp.get()?.get_version()?) +} + +pub async fn offline(client: &proc_control::Client) -> CommandResult<()> { + let req = client.offline_request(); + let rsp = req.send().promise.await?; + parse_operation_result(rsp.get()?.get_result()?) +} + +pub async fn reload(client: &proc_control::Client) -> CommandResult<()> { + let req = client.reload_request(); + let rsp = req.send().promise.await?; + parse_operation_result(rsp.get()?.get_result()?) +} + +pub async fn reload_issuer(client: &proc_control::Client) -> CommandResult<()> { + let req = client.reload_issuer_request(); + let rsp = req.send().promise.await?; + parse_operation_result(rsp.get()?.get_result()?) +} + +pub async fn cancel_shutdown(client: &proc_control::Client) -> CommandResult<()> { + let req = client.cancel_shutdown_request(); + let rsp = req.send().promise.await?; + parse_operation_result(rsp.get()?.get_result()?) +} diff --git a/vey-dcgen/vey-dcgen.spec b/vey-dcgen/vey-dcgen.spec index 58bd115e1..7ace0bd58 100644 --- a/vey-dcgen/vey-dcgen.spec +++ b/vey-dcgen/vey-dcgen.spec @@ -3,7 +3,7 @@ %define build_profile release-lto Name: vey-dcgen -Version: 0.9.0 +Version: 0.10.0 Release: 1%{?dist} Summary: Dynamic certificate generator for TLS interception @@ -11,7 +11,7 @@ License: Apache-2.0 URL: https://github.com/VEY-OSS/vey Source0: %{name}-%{version}.tar.xz -BuildRequires: gcc, gcc-c++, make, pkgconf, cmake +BuildRequires: gcc, gcc-c++, make, pkgconf, cmake, capnproto, clang, libbpf-devel BuildRequires: openssl-devel BuildRequires: systemd-rpm-macros %{?systemd_ordering} @@ -29,12 +29,14 @@ certificate authority. VEY_PACKAGE_VERSION="%{version}-%{release}" export VEY_PACKAGE_VERSION %{?vey_pgo_profile:export VEY_PGO_PROFILE="%{vey_pgo_profile}"} -scripts/package/cargo_with_pgo.sh build --frozen --offline --profile %{build_profile} --no-default-features --features secure-snmalloc --package vey-dcgen +EBPF_FEATURE=$(sh scripts/package/detect_ebpf_feature.sh) +scripts/package/cargo_with_pgo.sh build --frozen --offline --profile %{build_profile} --no-default-features --features secure-snmalloc,$EBPF_FEATURE --package vey-dcgen --package vey-dcgen-ctl %install rm -rf $RPM_BUILD_ROOT install -m 755 -D target/%{build_profile}/vey-dcgen %{buildroot}%{_bindir}/vey-dcgen +install -m 755 -D target/%{build_profile}/vey-dcgen-ctl %{buildroot}%{_bindir}/vey-dcgen-ctl install -m 644 -D %{name}/debian/vey-dcgen@.service %{buildroot}%{_unitdir}/vey-dcgen@.service @@ -51,6 +53,7 @@ fi %files %{_bindir}/vey-dcgen +%{_bindir}/vey-dcgen-ctl %{_unitdir}/vey-dcgen@.service %license LICENSE %license LICENSE-BUNDLED @@ -58,5 +61,7 @@ fi %changelog +* Sun Nov 01 2026 VEY-OSS Developers - 0.10.0-1 +- New upstream release * Sun Jul 26 2026 VEY-OSS Developers - 0.9.0-1 - New upstream release