mirror of
https://github.com/telemt/telemt.git
synced 2026-05-24 04:31:43 +03:00
Compare commits
26 Commits
4096a85db4
...
toolchains
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de8b2f0bf | ||
|
|
4e5b67bae8 | ||
|
|
73f218b62a | ||
|
|
13ff3af1db | ||
|
|
77f717e3d1 | ||
|
|
db3e246390 | ||
|
|
b74ba38d40 | ||
|
|
269fce839f | ||
|
|
5a4072c964 | ||
|
|
a95678988a | ||
|
|
b17482ede3 | ||
|
|
e7a1d26e6e | ||
|
|
b91c6cb339 | ||
|
|
c4e7f54cbe | ||
|
|
f85205d48d | ||
|
|
d767ec02ee | ||
|
|
88a4c652b6 | ||
|
|
ea2d964502 | ||
|
|
3055637571 | ||
|
|
19b84b9d73 | ||
|
|
6ead8b1922 | ||
|
|
63aa1038c0 | ||
|
|
24594e648e | ||
|
|
e8b38ea860 | ||
|
|
f3598cf309 | ||
|
|
777b15b1da |
252
.github/workflows/release.yml
vendored
252
.github/workflows/release.yml
vendored
@@ -6,36 +6,34 @@ on:
|
|||||||
- '[0-9]+.[0-9]+.[0-9]+'
|
- '[0-9]+.[0-9]+.[0-9]+'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
BINARY_NAME: telemt
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
# ==========================
|
||||||
name: Build ${{ matrix.target }}
|
# GNU / glibc
|
||||||
|
# ==========================
|
||||||
|
build-gnu:
|
||||||
|
name: GNU ${{ matrix.target }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
artifact_name: telemt
|
asset: telemt-x86_64-linux-gnu
|
||||||
asset_name: telemt-x86_64-linux-gnu
|
|
||||||
- target: aarch64-unknown-linux-gnu
|
- target: aarch64-unknown-linux-gnu
|
||||||
artifact_name: telemt
|
asset: telemt-aarch64-linux-gnu
|
||||||
asset_name: telemt-aarch64-linux-gnu
|
|
||||||
- target: x86_64-unknown-linux-musl
|
|
||||||
artifact_name: telemt
|
|
||||||
asset_name: telemt-x86_64-linux-musl
|
|
||||||
- target: aarch64-unknown-linux-musl
|
|
||||||
artifact_name: telemt
|
|
||||||
asset_name: telemt-aarch64-linux-musl
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -43,12 +41,20 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ matrix.target }}
|
targets: |
|
||||||
|
x86_64-unknown-linux-gnu
|
||||||
|
aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
- name: Install cross-compilation tools
|
- name: Install deps
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
sudo apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
clang \
|
||||||
|
lld \
|
||||||
|
pkg-config \
|
||||||
|
gcc-aarch64-linux-gnu \
|
||||||
|
g++-aarch64-linux-gnu
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -56,41 +62,183 @@ jobs:
|
|||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
target
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
key: gnu-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
|
||||||
|
|
||||||
- name: Install cross
|
- name: Build
|
||||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
|
||||||
|
|
||||||
- name: Build Release
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: ${{ contains(matrix.target, 'musl') && '-C target-feature=+crt-static' || '' }}
|
|
||||||
run: cross build --release --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Package binary
|
|
||||||
run: |
|
run: |
|
||||||
cd target/${{ matrix.target }}/release
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
|
||||||
tar -czvf ${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }}
|
export CC=aarch64-linux-gnu-gcc
|
||||||
sha256sum ${{ matrix.asset_name }}.tar.gz > ${{ matrix.asset_name }}.sha256
|
export CXX=aarch64-linux-gnu-g++
|
||||||
|
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
|
||||||
|
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
||||||
|
export RUSTFLAGS="-C linker=aarch64-linux-gnu-gcc"
|
||||||
|
else
|
||||||
|
export CC=clang
|
||||||
|
export CXX=clang++
|
||||||
|
export CC_x86_64_unknown_linux_gnu=clang
|
||||||
|
export CXX_x86_64_unknown_linux_gnu=clang++
|
||||||
|
export RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=lld"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
BIN=target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||||
|
|
||||||
|
cp "$BIN" dist/${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
tar -czf ${{ matrix.asset }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.asset_name }}
|
name: ${{ matrix.asset }}
|
||||||
path: |
|
path: |
|
||||||
target/${{ matrix.target }}/release/${{ matrix.asset_name }}.tar.gz
|
dist/${{ matrix.asset }}.tar.gz
|
||||||
target/${{ matrix.target }}/release/${{ matrix.asset_name }}.sha256
|
dist/${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
build-docker-image:
|
# ==========================
|
||||||
needs: build
|
# MUSL
|
||||||
|
# ==========================
|
||||||
|
build-musl:
|
||||||
|
name: MUSL ${{ matrix.target }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
container:
|
||||||
packages: write
|
image: rust:slim-bookworm
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: x86_64-unknown-linux-musl
|
||||||
|
asset: telemt-x86_64-linux-musl
|
||||||
|
- target: aarch64-unknown-linux-musl
|
||||||
|
asset: telemt-aarch64-linux-musl
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
musl-tools \
|
||||||
|
pkg-config \
|
||||||
|
curl
|
||||||
|
|
||||||
|
# 💾 cache toolchain
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
with:
|
||||||
|
path: ~/.musl-aarch64
|
||||||
|
key: musl-toolchain-aarch64-v1
|
||||||
|
|
||||||
|
# 🔥 надёжная установка
|
||||||
|
- name: Install aarch64 musl toolchain
|
||||||
|
if: matrix.target == 'aarch64-unknown-linux-musl'
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
TOOLCHAIN_DIR="$HOME/.musl-aarch64"
|
||||||
|
ARCHIVE="aarch64-linux-musl-cross.tgz"
|
||||||
|
|
||||||
|
if [ -x "$TOOLCHAIN_DIR/bin/aarch64-linux-musl-gcc" ]; then
|
||||||
|
echo "✅ musl toolchain already installed"
|
||||||
|
else
|
||||||
|
echo "⬇️ downloading musl toolchain..."
|
||||||
|
|
||||||
|
download() {
|
||||||
|
url="$1"
|
||||||
|
echo "→ trying $url"
|
||||||
|
curl -fL \
|
||||||
|
--retry 5 \
|
||||||
|
--retry-delay 3 \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 120 \
|
||||||
|
-o "$ARCHIVE" "$url" && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
download "https://musl.cc/$ARCHIVE" || \
|
||||||
|
download "https://more.musl.cc/$ARCHIVE" || \
|
||||||
|
{ echo "❌ failed to download musl toolchain"; exit 1; }
|
||||||
|
|
||||||
|
mkdir -p "$TOOLCHAIN_DIR"
|
||||||
|
tar -xzf "$ARCHIVE" --strip-components=1 -C "$TOOLCHAIN_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$TOOLCHAIN_DIR/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Add rust target
|
||||||
|
run: rustup target add ${{ matrix.target }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
/usr/local/cargo/registry
|
||||||
|
/usr/local/cargo/git
|
||||||
|
target
|
||||||
|
key: musl-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
|
||||||
|
export CC=aarch64-linux-musl-gcc
|
||||||
|
export CC_aarch64_unknown_linux_musl=aarch64-linux-musl-gcc
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static -C linker=aarch64-linux-musl-gcc"
|
||||||
|
else
|
||||||
|
export CC=musl-gcc
|
||||||
|
export CC_x86_64_unknown_linux_musl=musl-gcc
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build --release --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
BIN=target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}
|
||||||
|
|
||||||
|
cp "$BIN" dist/${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
|
||||||
|
cd dist
|
||||||
|
tar -czf ${{ matrix.asset }}.tar.gz ${{ env.BINARY_NAME }}-${{ matrix.target }}
|
||||||
|
sha256sum ${{ matrix.asset }}.tar.gz > ${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.asset }}
|
||||||
|
path: |
|
||||||
|
dist/${{ matrix.asset }}.tar.gz
|
||||||
|
dist/${{ matrix.asset }}.sha256
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Docker
|
||||||
|
# ==========================
|
||||||
|
docker:
|
||||||
|
name: Docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-gnu, build-musl]
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Extract binaries
|
||||||
|
run: |
|
||||||
|
mkdir dist
|
||||||
|
find artifacts -name "*.tar.gz" -exec tar -xzf {} -C dist \;
|
||||||
|
|
||||||
|
cp dist/telemt-x86_64-unknown-linux-musl dist/telemt || true
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
- uses: docker/setup-qemu-action@v3
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@@ -105,35 +253,43 @@ jobs:
|
|||||||
id: vars
|
id: vars
|
||||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build & Push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
|
ghcr.io/${{ github.repository }}:${{ steps.vars.outputs.VERSION }}
|
||||||
ghcr.io/${{ github.repository }}:latest
|
ghcr.io/${{ github.repository }}:latest
|
||||||
|
build-args: |
|
||||||
|
BINARY=dist/telemt
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Release
|
||||||
|
# ==========================
|
||||||
release:
|
release:
|
||||||
name: Create Release
|
name: Release
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-gnu, build-musl]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Flatten artifacts
|
||||||
|
run: |
|
||||||
|
mkdir dist
|
||||||
|
find artifacts -type f -exec cp {} dist/ \;
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: artifacts/**/*
|
files: dist/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
|
prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
|
||||||
|
|||||||
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -2672,7 +2672,6 @@ dependencies = [
|
|||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"url",
|
"url",
|
||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
@@ -3000,18 +2999,6 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-appender"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"time",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.31"
|
version = "0.1.31"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ zeroize = { version = "1.8", features = ["derive"] }
|
|||||||
|
|
||||||
# Network
|
# Network
|
||||||
socket2 = { version = "0.5", features = ["all"] }
|
socket2 = { version = "0.5", features = ["all"] }
|
||||||
nix = { version = "0.28", default-features = false, features = ["net", "user", "process", "fs", "signal"] }
|
nix = { version = "0.28", default-features = false, features = ["net"] }
|
||||||
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] }
|
||||||
|
|
||||||
# Serialization
|
# Serialization
|
||||||
@@ -39,7 +39,6 @@ bytes = "1.9"
|
|||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-appender = "0.2"
|
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
dashmap = "5.5"
|
dashmap = "5.5"
|
||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
|
|||||||
65
Dockerfile
65
Dockerfile
@@ -1,3 +1,5 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
# ==========================
|
# ==========================
|
||||||
@@ -5,36 +7,87 @@ FROM rust:1.88-slim-bookworm AS builder
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Depcache
|
||||||
COPY Cargo.toml Cargo.lock* ./
|
COPY Cargo.toml Cargo.lock* ./
|
||||||
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
|
RUN mkdir src && echo 'fn main() {}' > src/main.rs && \
|
||||||
cargo build --release 2>/dev/null || true && \
|
cargo build --release 2>/dev/null || true && \
|
||||||
rm -rf src
|
rm -rf src
|
||||||
|
|
||||||
|
# Build
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo build --release && strip target/release/telemt
|
RUN cargo build --release && strip target/release/telemt
|
||||||
|
|
||||||
# ==========================
|
# ==========================
|
||||||
# Stage 2: Runtime
|
# Stage 2: Compress (strip + UPX)
|
||||||
# ==========================
|
# ==========================
|
||||||
FROM debian:bookworm-slim
|
FROM debian:12-slim AS minimal
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
upx \
|
||||||
|
binutils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=builder /build/target/release/telemt /telemt
|
||||||
|
|
||||||
|
RUN strip /telemt || true
|
||||||
|
RUN upx --best --lzma /telemt || true
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Stage 3: Debug base
|
||||||
|
# ==========================
|
||||||
|
FROM debian:12-slim AS debug-base
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
curl \
|
||||||
|
iproute2 \
|
||||||
|
busybox \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN useradd -r -s /usr/sbin/nologin telemt
|
# ==========================
|
||||||
|
# Stage 4: Debug image
|
||||||
|
# ==========================
|
||||||
|
FROM debug-base AS debug
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/telemt /app/telemt
|
COPY --from=minimal /telemt /app/telemt
|
||||||
COPY config.toml /app/config.toml
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
RUN chown -R telemt:telemt /app
|
USER root
|
||||||
USER telemt
|
|
||||||
|
EXPOSE 443
|
||||||
|
EXPOSE 9090
|
||||||
|
EXPOSE 9091
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/telemt"]
|
||||||
|
CMD ["config.toml"]
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Stage 5: Production (distroless)
|
||||||
|
# ==========================
|
||||||
|
FROM gcr.io/distroless/base-debian12 AS prod
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=minimal /telemt /app/telemt
|
||||||
|
COPY config.toml /app/config.toml
|
||||||
|
|
||||||
|
# TLS + timezone + shell
|
||||||
|
COPY --from=debug-base /etc/ssl/certs /etc/ssl/certs
|
||||||
|
COPY --from=debug-base /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=debug-base /bin/busybox /bin/busybox
|
||||||
|
|
||||||
|
RUN ["/bin/busybox", "--install", "-s", "/bin"]
|
||||||
|
|
||||||
|
# distroless user
|
||||||
|
USER nonroot:nonroot
|
||||||
|
|
||||||
EXPOSE 443
|
EXPOSE 443
|
||||||
EXPOSE 9090
|
EXPOSE 9090
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ docker compose down
|
|||||||
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
|
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
|
||||||
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`
|
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`
|
||||||
|
|
||||||
**Запуск в Docker Compose**
|
**Запуск без Docker Compose**
|
||||||
```bash
|
```bash
|
||||||
docker build -t telemt:local .
|
docker build -t telemt:local .
|
||||||
docker run --name telemt --restart unless-stopped \
|
docker run --name telemt --restart unless-stopped \
|
||||||
|
|||||||
514
src/cli.rs
514
src/cli.rs
@@ -1,270 +1,11 @@
|
|||||||
//! CLI commands: --init (fire-and-forget setup), daemon options, subcommands
|
//! CLI commands: --init (fire-and-forget setup)
|
||||||
//!
|
|
||||||
//! Subcommands:
|
|
||||||
//! - `start [OPTIONS] [config.toml]` - Start the daemon
|
|
||||||
//! - `stop [--pid-file PATH]` - Stop a running daemon
|
|
||||||
//! - `reload [--pid-file PATH]` - Reload configuration (SIGHUP)
|
|
||||||
//! - `status [--pid-file PATH]` - Check daemon status
|
|
||||||
//! - `run [OPTIONS] [config.toml]` - Run in foreground (default behavior)
|
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::daemon::{self, DaemonOptions, DEFAULT_PID_FILE};
|
|
||||||
|
|
||||||
/// CLI subcommand to execute.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Subcommand {
|
|
||||||
/// Run the proxy (default, or explicit `run` subcommand).
|
|
||||||
Run,
|
|
||||||
/// Start as daemon (`start` subcommand).
|
|
||||||
Start,
|
|
||||||
/// Stop a running daemon (`stop` subcommand).
|
|
||||||
Stop,
|
|
||||||
/// Reload configuration (`reload` subcommand).
|
|
||||||
Reload,
|
|
||||||
/// Check daemon status (`status` subcommand).
|
|
||||||
Status,
|
|
||||||
/// Fire-and-forget setup (`--init`).
|
|
||||||
Init,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parsed subcommand with its options.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ParsedCommand {
|
|
||||||
pub subcommand: Subcommand,
|
|
||||||
pub pid_file: PathBuf,
|
|
||||||
pub config_path: String,
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub daemon_opts: DaemonOptions,
|
|
||||||
pub init_opts: Option<InitOptions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ParsedCommand {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
subcommand: Subcommand::Run,
|
|
||||||
#[cfg(unix)]
|
|
||||||
pid_file: PathBuf::from(DEFAULT_PID_FILE),
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pid_file: PathBuf::from("/var/run/telemt.pid"),
|
|
||||||
config_path: "config.toml".to_string(),
|
|
||||||
#[cfg(unix)]
|
|
||||||
daemon_opts: DaemonOptions::default(),
|
|
||||||
init_opts: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse CLI arguments into a command structure.
|
|
||||||
pub fn parse_command(args: &[String]) -> ParsedCommand {
|
|
||||||
let mut cmd = ParsedCommand::default();
|
|
||||||
|
|
||||||
// Check for --init first (legacy form)
|
|
||||||
if args.iter().any(|a| a == "--init") {
|
|
||||||
cmd.subcommand = Subcommand::Init;
|
|
||||||
cmd.init_opts = parse_init_args(args);
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for subcommand as first argument
|
|
||||||
if let Some(first) = args.first() {
|
|
||||||
match first.as_str() {
|
|
||||||
"start" => {
|
|
||||||
cmd.subcommand = Subcommand::Start;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
// Force daemonize for start command
|
|
||||||
cmd.daemon_opts.daemonize = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"stop" => {
|
|
||||||
cmd.subcommand = Subcommand::Stop;
|
|
||||||
}
|
|
||||||
"reload" => {
|
|
||||||
cmd.subcommand = Subcommand::Reload;
|
|
||||||
}
|
|
||||||
"status" => {
|
|
||||||
cmd.subcommand = Subcommand::Status;
|
|
||||||
}
|
|
||||||
"run" => {
|
|
||||||
cmd.subcommand = Subcommand::Run;
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// No subcommand, default to Run
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts = parse_daemon_args(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse remaining options
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
// Skip subcommand names
|
|
||||||
"start" | "stop" | "reload" | "status" | "run" => {}
|
|
||||||
// PID file option (for stop/reload/status)
|
|
||||||
"--pid-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
cmd.pid_file = PathBuf::from(&args[i]);
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--pid-file=") => {
|
|
||||||
cmd.pid_file = PathBuf::from(s.trim_start_matches("--pid-file="));
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
cmd.daemon_opts.pid_file = Some(cmd.pid_file.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Config path (positional, non-flag argument)
|
|
||||||
s if !s.starts_with('-') => {
|
|
||||||
cmd.config_path = s.to_string();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a subcommand that doesn't require starting the server.
|
|
||||||
/// Returns `Some(exit_code)` if the command was handled, `None` if server should start.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
|
||||||
match cmd.subcommand {
|
|
||||||
Subcommand::Stop => Some(cmd_stop(&cmd.pid_file)),
|
|
||||||
Subcommand::Reload => Some(cmd_reload(&cmd.pid_file)),
|
|
||||||
Subcommand::Status => Some(cmd_status(&cmd.pid_file)),
|
|
||||||
Subcommand::Init => {
|
|
||||||
if let Some(opts) = cmd.init_opts.clone() {
|
|
||||||
match run_init(opts) {
|
|
||||||
Ok(()) => Some(0),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Init failed: {}", e);
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Run and Start need the server
|
|
||||||
Subcommand::Run | Subcommand::Start => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
|
||||||
match cmd.subcommand {
|
|
||||||
Subcommand::Stop | Subcommand::Reload | Subcommand::Status => {
|
|
||||||
eprintln!("[telemt] Subcommand not supported on this platform");
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
Subcommand::Init => {
|
|
||||||
if let Some(opts) = cmd.init_opts.clone() {
|
|
||||||
match run_init(opts) {
|
|
||||||
Ok(()) => Some(0),
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Init failed: {}", e);
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Some(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Subcommand::Run | Subcommand::Start => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop command: send SIGTERM to the running daemon.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_stop(pid_file: &Path) -> i32 {
|
|
||||||
use nix::sys::signal::Signal;
|
|
||||||
|
|
||||||
println!("Stopping telemt daemon...");
|
|
||||||
|
|
||||||
match daemon::signal_pid_file(pid_file, Signal::SIGTERM) {
|
|
||||||
Ok(()) => {
|
|
||||||
println!("Stop signal sent successfully");
|
|
||||||
|
|
||||||
// Wait for process to exit (up to 10 seconds)
|
|
||||||
for _ in 0..20 {
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
|
||||||
if let daemon::DaemonStatus::NotRunning = daemon::check_status(pid_file) {
|
|
||||||
println!("Daemon stopped");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!("Daemon may still be shutting down");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to stop daemon: {}", e);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reload command: send SIGHUP to trigger config reload.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_reload(pid_file: &Path) -> i32 {
|
|
||||||
use nix::sys::signal::Signal;
|
|
||||||
|
|
||||||
println!("Reloading telemt configuration...");
|
|
||||||
|
|
||||||
match daemon::signal_pid_file(pid_file, Signal::SIGHUP) {
|
|
||||||
Ok(()) => {
|
|
||||||
println!("Reload signal sent successfully");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to reload daemon: {}", e);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Status command: check if daemon is running.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn cmd_status(pid_file: &Path) -> i32 {
|
|
||||||
match daemon::check_status(pid_file) {
|
|
||||||
daemon::DaemonStatus::Running(pid) => {
|
|
||||||
println!("telemt is running (pid {})", pid);
|
|
||||||
0
|
|
||||||
}
|
|
||||||
daemon::DaemonStatus::Stale(pid) => {
|
|
||||||
println!("telemt is not running (stale pid file, was pid {})", pid);
|
|
||||||
// Clean up stale PID file
|
|
||||||
let _ = std::fs::remove_file(pid_file);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
daemon::DaemonStatus::NotRunning => {
|
|
||||||
println!("telemt is not running");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for the init command
|
/// Options for the init command
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct InitOptions {
|
pub struct InitOptions {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
@@ -274,64 +15,6 @@ pub struct InitOptions {
|
|||||||
pub no_start: bool,
|
pub no_start: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse daemon-related options from CLI args.
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub fn parse_daemon_args(args: &[String]) -> DaemonOptions {
|
|
||||||
let mut opts = DaemonOptions::default();
|
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
"--daemon" | "-d" => {
|
|
||||||
opts.daemonize = true;
|
|
||||||
}
|
|
||||||
"--foreground" | "-f" => {
|
|
||||||
opts.foreground = true;
|
|
||||||
}
|
|
||||||
"--pid-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.pid_file = Some(PathBuf::from(&args[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--pid-file=") => {
|
|
||||||
opts.pid_file = Some(PathBuf::from(s.trim_start_matches("--pid-file=")));
|
|
||||||
}
|
|
||||||
"--run-as-user" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.user = Some(args[i].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-user=") => {
|
|
||||||
opts.user = Some(s.trim_start_matches("--run-as-user=").to_string());
|
|
||||||
}
|
|
||||||
"--run-as-group" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.group = Some(args[i].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-group=") => {
|
|
||||||
opts.group = Some(s.trim_start_matches("--run-as-group=").to_string());
|
|
||||||
}
|
|
||||||
"--working-dir" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
opts.working_dir = Some(PathBuf::from(&args[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--working-dir=") => {
|
|
||||||
opts.working_dir = Some(PathBuf::from(s.trim_start_matches("--working-dir=")));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
opts
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for InitOptions {
|
impl Default for InitOptions {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -401,16 +84,10 @@ pub fn parse_init_args(args: &[String]) -> Option<InitOptions> {
|
|||||||
|
|
||||||
/// Run the fire-and-forget setup.
|
/// Run the fire-and-forget setup.
|
||||||
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use crate::service::{self, InitSystem, ServiceOptions};
|
|
||||||
|
|
||||||
eprintln!("[telemt] Fire-and-forget setup");
|
eprintln!("[telemt] Fire-and-forget setup");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 1. Detect init system
|
// 1. Generate or validate secret
|
||||||
let init_system = service::detect_init_system();
|
|
||||||
eprintln!("[+] Detected init system: {}", init_system);
|
|
||||||
|
|
||||||
// 2. Generate or validate secret
|
|
||||||
let secret = match opts.secret {
|
let secret = match opts.secret {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
if s.len() != 32 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
@@ -421,134 +98,80 @@ pub fn run_init(opts: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
None => generate_secret(),
|
None => generate_secret(),
|
||||||
};
|
};
|
||||||
|
|
||||||
eprintln!("[+] Secret: {}", secret);
|
eprintln!("[+] Secret: {}", secret);
|
||||||
eprintln!("[+] User: {}", opts.username);
|
eprintln!("[+] User: {}", opts.username);
|
||||||
eprintln!("[+] Port: {}", opts.port);
|
eprintln!("[+] Port: {}", opts.port);
|
||||||
eprintln!("[+] Domain: {}", opts.domain);
|
eprintln!("[+] Domain: {}", opts.domain);
|
||||||
|
|
||||||
// 3. Create config directory
|
// 2. Create config directory
|
||||||
fs::create_dir_all(&opts.config_dir)?;
|
fs::create_dir_all(&opts.config_dir)?;
|
||||||
let config_path = opts.config_dir.join("config.toml");
|
let config_path = opts.config_dir.join("config.toml");
|
||||||
|
|
||||||
// 4. Write config
|
// 3. Write config
|
||||||
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
let config_content = generate_config(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
fs::write(&config_path, &config_content)?;
|
fs::write(&config_path, &config_content)?;
|
||||||
eprintln!("[+] Config written to {}", config_path.display());
|
eprintln!("[+] Config written to {}", config_path.display());
|
||||||
|
|
||||||
// 5. Generate and write service file
|
// 4. Write systemd unit
|
||||||
let exe_path = std::env::current_exe()
|
let exe_path = std::env::current_exe()
|
||||||
.unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
.unwrap_or_else(|_| PathBuf::from("/usr/local/bin/telemt"));
|
||||||
|
|
||||||
let service_opts = ServiceOptions {
|
let unit_path = Path::new("/etc/systemd/system/telemt.service");
|
||||||
exe_path: &exe_path,
|
let unit_content = generate_systemd_unit(&exe_path, &config_path);
|
||||||
config_path: &config_path,
|
|
||||||
user: None, // Let systemd/init handle user
|
match fs::write(unit_path, &unit_content) {
|
||||||
group: None,
|
|
||||||
pid_file: "/var/run/telemt.pid",
|
|
||||||
working_dir: Some("/var/lib/telemt"),
|
|
||||||
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
|
||||||
};
|
|
||||||
|
|
||||||
let service_path = service::service_file_path(init_system);
|
|
||||||
let service_content = service::generate_service_file(init_system, &service_opts);
|
|
||||||
|
|
||||||
// Ensure parent directory exists
|
|
||||||
if let Some(parent) = Path::new(service_path).parent() {
|
|
||||||
let _ = fs::create_dir_all(parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
match fs::write(service_path, &service_content) {
|
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
eprintln!("[+] Service file written to {}", service_path);
|
eprintln!("[+] Systemd unit written to {}", unit_path.display());
|
||||||
|
|
||||||
// Make script executable for OpenRC/FreeBSD
|
|
||||||
#[cfg(unix)]
|
|
||||||
if init_system == InitSystem::OpenRC || init_system == InitSystem::FreeBSDRc {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
let mut perms = fs::metadata(service_path)?.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
fs::set_permissions(service_path, perms)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[!] Cannot write service file (run as root?): {}", e);
|
eprintln!("[!] Cannot write systemd unit (run as root?): {}", e);
|
||||||
eprintln!("[!] Manual service file content:");
|
eprintln!("[!] Manual unit file content:");
|
||||||
eprintln!("{}", service_content);
|
eprintln!("{}", unit_content);
|
||||||
|
|
||||||
// Still print links and installation instructions
|
// Still print links and config
|
||||||
eprintln!();
|
|
||||||
eprintln!("{}", service::installation_instructions(init_system));
|
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Install and enable service based on init system
|
// 5. Reload systemd
|
||||||
match init_system {
|
run_cmd("systemctl", &["daemon-reload"]);
|
||||||
InitSystem::Systemd => {
|
|
||||||
run_cmd("systemctl", &["daemon-reload"]);
|
// 6. Enable service
|
||||||
run_cmd("systemctl", &["enable", "telemt.service"]);
|
run_cmd("systemctl", &["enable", "telemt.service"]);
|
||||||
eprintln!("[+] Service enabled");
|
eprintln!("[+] Service enabled");
|
||||||
|
|
||||||
if !opts.no_start {
|
// 7. Start service (unless --no-start)
|
||||||
run_cmd("systemctl", &["start", "telemt.service"]);
|
if !opts.no_start {
|
||||||
eprintln!("[+] Service started");
|
run_cmd("systemctl", &["start", "telemt.service"]);
|
||||||
|
eprintln!("[+] Service started");
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
|
||||||
let status = Command::new("systemctl")
|
// Brief delay then check status
|
||||||
.args(["is-active", "telemt.service"])
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
.output();
|
let status = Command::new("systemctl")
|
||||||
|
.args(["is-active", "telemt.service"])
|
||||||
match status {
|
.output();
|
||||||
Ok(out) if out.status.success() => {
|
|
||||||
eprintln!("[+] Service is running");
|
match status {
|
||||||
}
|
Ok(out) if out.status.success() => {
|
||||||
_ => {
|
eprintln!("[+] Service is running");
|
||||||
eprintln!("[!] Service may not have started correctly");
|
}
|
||||||
eprintln!("[!] Check: journalctl -u telemt.service -n 20");
|
_ => {
|
||||||
}
|
eprintln!("[!] Service may not have started correctly");
|
||||||
}
|
eprintln!("[!] Check: journalctl -u telemt.service -n 20");
|
||||||
} else {
|
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
|
||||||
eprintln!("[+] Start manually: systemctl start telemt.service");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InitSystem::OpenRC => {
|
} else {
|
||||||
run_cmd("rc-update", &["add", "telemt", "default"]);
|
eprintln!("[+] Service not started (--no-start)");
|
||||||
eprintln!("[+] Service enabled");
|
eprintln!("[+] Start manually: systemctl start telemt.service");
|
||||||
|
|
||||||
if !opts.no_start {
|
|
||||||
run_cmd("rc-service", &["telemt", "start"]);
|
|
||||||
eprintln!("[+] Service started");
|
|
||||||
} else {
|
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
|
||||||
eprintln!("[+] Start manually: rc-service telemt start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InitSystem::FreeBSDRc => {
|
|
||||||
run_cmd("sysrc", &["telemt_enable=YES"]);
|
|
||||||
eprintln!("[+] Service enabled");
|
|
||||||
|
|
||||||
if !opts.no_start {
|
|
||||||
run_cmd("service", &["telemt", "start"]);
|
|
||||||
eprintln!("[+] Service started");
|
|
||||||
} else {
|
|
||||||
eprintln!("[+] Service not started (--no-start)");
|
|
||||||
eprintln!("[+] Start manually: service telemt start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InitSystem::Unknown => {
|
|
||||||
eprintln!("[!] Unknown init system - service file written but not installed");
|
|
||||||
eprintln!("[!] You may need to install it manually");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
// 7. Print links
|
// 8. Print links
|
||||||
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
print_links(&opts.username, &secret, opts.port, &opts.domain);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,6 +264,35 @@ weight = 10
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_systemd_unit(exe_path: &Path, config_path: &Path) -> String {
|
||||||
|
format!(
|
||||||
|
r#"[Unit]
|
||||||
|
Description=Telemt MTProxy
|
||||||
|
Documentation=https://github.com/telemt/telemt
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart={exe} {config}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
LimitNOFILE=65535
|
||||||
|
# Security hardening
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
ReadWritePaths=/etc/telemt
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
"#,
|
||||||
|
exe = exe_path.display(),
|
||||||
|
config = config_path.display(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn run_cmd(cmd: &str, args: &[&str]) {
|
fn run_cmd(cmd: &str, args: &[&str]) {
|
||||||
match Command::new(cmd).args(args).output() {
|
match Command::new(cmd).args(args).output() {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
|||||||
@@ -1,523 +0,0 @@
|
|||||||
//! Unix daemon support for telemt.
|
|
||||||
//!
|
|
||||||
//! Provides classic Unix daemonization (double-fork), PID file management,
|
|
||||||
//! and privilege dropping for running telemt as a background service.
|
|
||||||
|
|
||||||
use std::fs::{self, File, OpenOptions};
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use nix::fcntl::{Flock, FlockArg};
|
|
||||||
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, dup2, fork, getpid, setsid};
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
/// Default PID file location.
|
|
||||||
pub const DEFAULT_PID_FILE: &str = "/var/run/telemt.pid";
|
|
||||||
|
|
||||||
/// Daemon configuration options parsed from CLI.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct DaemonOptions {
|
|
||||||
/// Run as daemon (fork to background).
|
|
||||||
pub daemonize: bool,
|
|
||||||
/// Path to PID file.
|
|
||||||
pub pid_file: Option<PathBuf>,
|
|
||||||
/// User to run as after binding sockets.
|
|
||||||
pub user: Option<String>,
|
|
||||||
/// Group to run as after binding sockets.
|
|
||||||
pub group: Option<String>,
|
|
||||||
/// Working directory for the daemon.
|
|
||||||
pub working_dir: Option<PathBuf>,
|
|
||||||
/// Explicit foreground mode (for systemd Type=simple).
|
|
||||||
pub foreground: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DaemonOptions {
|
|
||||||
/// Returns the effective PID file path.
|
|
||||||
pub fn pid_file_path(&self) -> &Path {
|
|
||||||
self.pid_file
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or(Path::new(DEFAULT_PID_FILE))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if we should actually daemonize.
|
|
||||||
/// Foreground flag takes precedence.
|
|
||||||
pub fn should_daemonize(&self) -> bool {
|
|
||||||
self.daemonize && !self.foreground
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error types for daemon operations.
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum DaemonError {
|
|
||||||
#[error("fork failed: {0}")]
|
|
||||||
ForkFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("setsid failed: {0}")]
|
|
||||||
SetsidFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("chdir failed: {0}")]
|
|
||||||
ChdirFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("failed to open /dev/null: {0}")]
|
|
||||||
DevNullFailed(#[source] io::Error),
|
|
||||||
|
|
||||||
#[error("failed to redirect stdio: {0}")]
|
|
||||||
RedirectFailed(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("PID file error: {0}")]
|
|
||||||
PidFile(String),
|
|
||||||
|
|
||||||
#[error("another instance is already running (pid {0})")]
|
|
||||||
AlreadyRunning(i32),
|
|
||||||
|
|
||||||
#[error("user '{0}' not found")]
|
|
||||||
UserNotFound(String),
|
|
||||||
|
|
||||||
#[error("group '{0}' not found")]
|
|
||||||
GroupNotFound(String),
|
|
||||||
|
|
||||||
#[error("failed to set uid/gid: {0}")]
|
|
||||||
PrivilegeDrop(#[source] nix::Error),
|
|
||||||
|
|
||||||
#[error("io error: {0}")]
|
|
||||||
Io(#[from] io::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Result of a successful daemonize() call.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum DaemonizeResult {
|
|
||||||
/// We are the parent process and should exit.
|
|
||||||
Parent,
|
|
||||||
/// We are the daemon child process and should continue.
|
|
||||||
Child,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs classic Unix double-fork daemonization.
|
|
||||||
///
|
|
||||||
/// This detaches the process from the controlling terminal:
|
|
||||||
/// 1. First fork - parent exits, child continues
|
|
||||||
/// 2. setsid() - become session leader
|
|
||||||
/// 3. Second fork - ensure we can never acquire a controlling terminal
|
|
||||||
/// 4. chdir("/") - don't hold any directory open
|
|
||||||
/// 5. Redirect stdin/stdout/stderr to /dev/null
|
|
||||||
///
|
|
||||||
/// Returns `DaemonizeResult::Parent` in the original parent (which should exit),
|
|
||||||
/// or `DaemonizeResult::Child` in the final daemon child.
|
|
||||||
pub fn daemonize(working_dir: Option<&Path>) -> Result<DaemonizeResult, DaemonError> {
|
|
||||||
// First fork
|
|
||||||
match unsafe { fork() } {
|
|
||||||
Ok(ForkResult::Parent { .. }) => {
|
|
||||||
// Parent exits
|
|
||||||
return Ok(DaemonizeResult::Parent);
|
|
||||||
}
|
|
||||||
Ok(ForkResult::Child) => {
|
|
||||||
// Child continues
|
|
||||||
}
|
|
||||||
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new session, become session leader
|
|
||||||
setsid().map_err(DaemonError::SetsidFailed)?;
|
|
||||||
|
|
||||||
// Second fork to ensure we can never acquire a controlling terminal
|
|
||||||
match unsafe { fork() } {
|
|
||||||
Ok(ForkResult::Parent { .. }) => {
|
|
||||||
// Intermediate parent exits
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Ok(ForkResult::Child) => {
|
|
||||||
// Final daemon child continues
|
|
||||||
}
|
|
||||||
Err(e) => return Err(DaemonError::ForkFailed(e)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change working directory
|
|
||||||
let target_dir = working_dir.unwrap_or(Path::new("/"));
|
|
||||||
chdir(target_dir).map_err(DaemonError::ChdirFailed)?;
|
|
||||||
|
|
||||||
// Redirect stdin, stdout, stderr to /dev/null
|
|
||||||
redirect_stdio_to_devnull()?;
|
|
||||||
|
|
||||||
Ok(DaemonizeResult::Child)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redirects stdin, stdout, and stderr to /dev/null.
|
|
||||||
fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
|
|
||||||
let devnull = File::options()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.open("/dev/null")
|
|
||||||
.map_err(DaemonError::DevNullFailed)?;
|
|
||||||
|
|
||||||
let devnull_fd = std::os::unix::io::AsRawFd::as_raw_fd(&devnull);
|
|
||||||
|
|
||||||
// Redirect stdin (fd 0)
|
|
||||||
dup2(devnull_fd, 0).map_err(DaemonError::RedirectFailed)?;
|
|
||||||
// Redirect stdout (fd 1)
|
|
||||||
dup2(devnull_fd, 1).map_err(DaemonError::RedirectFailed)?;
|
|
||||||
// Redirect stderr (fd 2)
|
|
||||||
dup2(devnull_fd, 2).map_err(DaemonError::RedirectFailed)?;
|
|
||||||
|
|
||||||
// Close original devnull fd if it's not one of the standard fds
|
|
||||||
if devnull_fd > 2 {
|
|
||||||
let _ = close(devnull_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PID file manager with flock-based locking.
|
|
||||||
pub struct PidFile {
|
|
||||||
path: PathBuf,
|
|
||||||
file: Option<File>,
|
|
||||||
locked: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PidFile {
|
|
||||||
/// Creates a new PID file manager for the given path.
|
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
|
||||||
Self {
|
|
||||||
path: path.as_ref().to_path_buf(),
|
|
||||||
file: None,
|
|
||||||
locked: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if another instance is already running.
|
|
||||||
///
|
|
||||||
/// Returns the PID of the running instance if one exists.
|
|
||||||
pub fn check_running(&self) -> Result<Option<i32>, DaemonError> {
|
|
||||||
if !self.path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to read existing PID
|
|
||||||
let mut contents = String::new();
|
|
||||||
File::open(&self.path)
|
|
||||||
.and_then(|mut f| f.read_to_string(&mut contents))
|
|
||||||
.map_err(|e| DaemonError::PidFile(format!("cannot read {}: {}", self.path.display(), e)))?;
|
|
||||||
|
|
||||||
let pid: i32 = contents
|
|
||||||
.trim()
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", self.path.display())))?;
|
|
||||||
|
|
||||||
// Check if process is still running
|
|
||||||
if is_process_running(pid) {
|
|
||||||
Ok(Some(pid))
|
|
||||||
} else {
|
|
||||||
// Stale PID file
|
|
||||||
debug!(pid, path = %self.path.display(), "Removing stale PID file");
|
|
||||||
let _ = fs::remove_file(&self.path);
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acquires the PID file lock and writes the current PID.
|
|
||||||
///
|
|
||||||
/// Fails if another instance is already running.
|
|
||||||
pub fn acquire(&mut self) -> Result<(), DaemonError> {
|
|
||||||
// Check for running instance first
|
|
||||||
if let Some(pid) = self.check_running()? {
|
|
||||||
return Err(DaemonError::AlreadyRunning(pid));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure parent directory exists
|
|
||||||
if let Some(parent) = self.path.parent() {
|
|
||||||
if !parent.exists() {
|
|
||||||
fs::create_dir_all(parent).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!(
|
|
||||||
"cannot create directory {}: {}",
|
|
||||||
parent.display(),
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open/create PID file with exclusive lock
|
|
||||||
let file = OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.mode(0o644)
|
|
||||||
.open(&self.path)
|
|
||||||
.map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot open {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Try to acquire exclusive lock (non-blocking)
|
|
||||||
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
|
||||||
// Check if another instance grabbed the lock
|
|
||||||
if let Some(pid) = self.check_running().ok().flatten() {
|
|
||||||
DaemonError::AlreadyRunning(pid)
|
|
||||||
} else {
|
|
||||||
DaemonError::PidFile(format!("cannot lock {}: {}", self.path.display(), errno))
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Write our PID
|
|
||||||
let pid = getpid();
|
|
||||||
let mut file = flock.unlock().map_err(|(_, errno)| {
|
|
||||||
DaemonError::PidFile(format!("unlock failed: {}", errno))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
writeln!(file, "{}", pid).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot write PID to {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Re-acquire lock and keep it
|
|
||||||
let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock).map_err(|(_, errno)| {
|
|
||||||
DaemonError::PidFile(format!("cannot re-lock {}: {}", self.path.display(), errno))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
self.file = Some(flock.unlock().map_err(|(_, errno)| {
|
|
||||||
DaemonError::PidFile(format!("unlock for storage failed: {}", errno))
|
|
||||||
})?);
|
|
||||||
self.locked = true;
|
|
||||||
|
|
||||||
info!(pid = pid.as_raw(), path = %self.path.display(), "PID file created");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Releases the PID file lock and removes the file.
|
|
||||||
pub fn release(&mut self) -> Result<(), DaemonError> {
|
|
||||||
if let Some(file) = self.file.take() {
|
|
||||||
drop(file);
|
|
||||||
}
|
|
||||||
self.locked = false;
|
|
||||||
|
|
||||||
if self.path.exists() {
|
|
||||||
fs::remove_file(&self.path).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot remove {}: {}", self.path.display(), e))
|
|
||||||
})?;
|
|
||||||
debug!(path = %self.path.display(), "PID file removed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path to this PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn path(&self) -> &Path {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PidFile {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.locked {
|
|
||||||
if let Err(e) = self.release() {
|
|
||||||
warn!(error = %e, "Failed to clean up PID file on drop");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks if a process with the given PID is running.
|
|
||||||
fn is_process_running(pid: i32) -> bool {
|
|
||||||
// kill(pid, 0) checks if process exists without sending a signal
|
|
||||||
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drops privileges to the specified user and group.
|
|
||||||
///
|
|
||||||
/// This should be called after binding privileged ports but before
|
|
||||||
/// entering the main event loop.
|
|
||||||
pub fn drop_privileges(user: Option<&str>, group: Option<&str>) -> Result<(), DaemonError> {
|
|
||||||
// Look up group first (need to do this while still root)
|
|
||||||
let target_gid = if let Some(group_name) = group {
|
|
||||||
Some(lookup_group(group_name)?)
|
|
||||||
} else if let Some(user_name) = user {
|
|
||||||
// If no group specified but user is, use user's primary group
|
|
||||||
Some(lookup_user_primary_gid(user_name)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Look up user
|
|
||||||
let target_uid = if let Some(user_name) = user {
|
|
||||||
Some(lookup_user(user_name)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Drop privileges: set GID first, then UID
|
|
||||||
// (Setting UID first would prevent us from setting GID)
|
|
||||||
if let Some(gid) = target_gid {
|
|
||||||
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
// Also set supplementary groups to just this one
|
|
||||||
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
info!(gid = gid.as_raw(), "Dropped group privileges");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(uid) = target_uid {
|
|
||||||
unistd::setuid(uid).map_err(DaemonError::PrivilegeDrop)?;
|
|
||||||
info!(uid = uid.as_raw(), "Dropped user privileges");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a user by name and returns their UID.
|
|
||||||
fn lookup_user(name: &str) -> Result<Uid, DaemonError> {
|
|
||||||
// Use libc getpwnam
|
|
||||||
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let pwd = libc::getpwnam(c_name.as_ptr());
|
|
||||||
if pwd.is_null() {
|
|
||||||
Err(DaemonError::UserNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Uid::from_raw((*pwd).pw_uid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a user's primary GID by username.
|
|
||||||
fn lookup_user_primary_gid(name: &str) -> Result<Gid, DaemonError> {
|
|
||||||
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::UserNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let pwd = libc::getpwnam(c_name.as_ptr());
|
|
||||||
if pwd.is_null() {
|
|
||||||
Err(DaemonError::UserNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Gid::from_raw((*pwd).pw_gid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up a group by name and returns its GID.
|
|
||||||
fn lookup_group(name: &str) -> Result<Gid, DaemonError> {
|
|
||||||
let c_name = std::ffi::CString::new(name).map_err(|_| DaemonError::GroupNotFound(name.to_string()))?;
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let grp = libc::getgrnam(c_name.as_ptr());
|
|
||||||
if grp.is_null() {
|
|
||||||
Err(DaemonError::GroupNotFound(name.to_string()))
|
|
||||||
} else {
|
|
||||||
Ok(Gid::from_raw((*grp).gr_gid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reads PID from a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn read_pid_file<P: AsRef<Path>>(path: P) -> Result<i32, DaemonError> {
|
|
||||||
let path = path.as_ref();
|
|
||||||
let mut contents = String::new();
|
|
||||||
File::open(path)
|
|
||||||
.and_then(|mut f| f.read_to_string(&mut contents))
|
|
||||||
.map_err(|e| DaemonError::PidFile(format!("cannot read {}: {}", path.display(), e)))?;
|
|
||||||
|
|
||||||
contents
|
|
||||||
.trim()
|
|
||||||
.parse()
|
|
||||||
.map_err(|_| DaemonError::PidFile(format!("invalid PID in {}", path.display())))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a signal to the process specified in a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn signal_pid_file<P: AsRef<Path>>(
|
|
||||||
path: P,
|
|
||||||
signal: nix::sys::signal::Signal,
|
|
||||||
) -> Result<(), DaemonError> {
|
|
||||||
let pid = read_pid_file(&path)?;
|
|
||||||
|
|
||||||
if !is_process_running(pid) {
|
|
||||||
return Err(DaemonError::PidFile(format!(
|
|
||||||
"process {} from {} is not running",
|
|
||||||
pid,
|
|
||||||
path.as_ref().display()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
nix::sys::signal::kill(Pid::from_raw(pid), signal).map_err(|e| {
|
|
||||||
DaemonError::PidFile(format!("cannot signal process {}: {}", pid, e))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the status of the daemon based on PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum DaemonStatus {
|
|
||||||
/// Daemon is running with the given PID.
|
|
||||||
Running(i32),
|
|
||||||
/// PID file exists but process is not running.
|
|
||||||
Stale(i32),
|
|
||||||
/// No PID file exists.
|
|
||||||
NotRunning,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks the daemon status from a PID file.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn check_status<P: AsRef<Path>>(path: P) -> DaemonStatus {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
return DaemonStatus::NotRunning;
|
|
||||||
}
|
|
||||||
|
|
||||||
match read_pid_file(path) {
|
|
||||||
Ok(pid) => {
|
|
||||||
if is_process_running(pid) {
|
|
||||||
DaemonStatus::Running(pid)
|
|
||||||
} else {
|
|
||||||
DaemonStatus::Stale(pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => DaemonStatus::NotRunning,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_daemon_options_default() {
|
|
||||||
let opts = DaemonOptions::default();
|
|
||||||
assert!(!opts.daemonize);
|
|
||||||
assert!(!opts.should_daemonize());
|
|
||||||
assert_eq!(opts.pid_file_path(), Path::new(DEFAULT_PID_FILE));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_daemon_options_foreground_overrides() {
|
|
||||||
let opts = DaemonOptions {
|
|
||||||
daemonize: true,
|
|
||||||
foreground: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
assert!(!opts.should_daemonize());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_check_status_not_running() {
|
|
||||||
let path = "/tmp/telemt_test_nonexistent.pid";
|
|
||||||
assert_eq!(check_status(path), DaemonStatus::NotRunning);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pid_file_basic() {
|
|
||||||
let path = "/tmp/telemt_test_pidfile.pid";
|
|
||||||
let _ = fs::remove_file(path);
|
|
||||||
|
|
||||||
let mut pf = PidFile::new(path);
|
|
||||||
assert!(pf.check_running().unwrap().is_none());
|
|
||||||
|
|
||||||
pf.acquire().unwrap();
|
|
||||||
assert!(Path::new(path).exists());
|
|
||||||
|
|
||||||
// Read it back
|
|
||||||
let pid = read_pid_file(path).unwrap();
|
|
||||||
assert_eq!(pid, std::process::id() as i32);
|
|
||||||
|
|
||||||
pf.release().unwrap();
|
|
||||||
assert!(!Path::new(path).exists());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
291
src/logging.rs
291
src/logging.rs
@@ -1,291 +0,0 @@
|
|||||||
//! Logging configuration for telemt.
|
|
||||||
//!
|
|
||||||
//! Supports multiple log destinations:
|
|
||||||
//! - stderr (default, works with systemd journald)
|
|
||||||
//! - syslog (Unix only, for traditional init systems)
|
|
||||||
//! - file (with optional rotation)
|
|
||||||
|
|
||||||
#![allow(dead_code)] // Infrastructure module - used via CLI flags
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
|
||||||
use tracing_subscriber::util::SubscriberInitExt;
|
|
||||||
use tracing_subscriber::{EnvFilter, fmt, reload};
|
|
||||||
|
|
||||||
/// Log destination configuration.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub enum LogDestination {
|
|
||||||
/// Log to stderr (default, captured by systemd journald).
|
|
||||||
#[default]
|
|
||||||
Stderr,
|
|
||||||
/// Log to syslog (Unix only).
|
|
||||||
#[cfg(unix)]
|
|
||||||
Syslog,
|
|
||||||
/// Log to a file with optional rotation.
|
|
||||||
File {
|
|
||||||
path: String,
|
|
||||||
/// Rotate daily if true.
|
|
||||||
rotate_daily: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Logging options parsed from CLI/config.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct LoggingOptions {
|
|
||||||
/// Where to send logs.
|
|
||||||
pub destination: LogDestination,
|
|
||||||
/// Disable ANSI colors.
|
|
||||||
pub disable_colors: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Guard that must be held to keep file logging active.
|
|
||||||
/// When dropped, flushes and closes log files.
|
|
||||||
pub struct LoggingGuard {
|
|
||||||
_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoggingGuard {
|
|
||||||
fn new(guard: Option<tracing_appender::non_blocking::WorkerGuard>) -> Self {
|
|
||||||
Self { _guard: guard }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a no-op guard for stderr/syslog logging.
|
|
||||||
pub fn noop() -> Self {
|
|
||||||
Self { _guard: None }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize the tracing subscriber with the specified options.
|
|
||||||
///
|
|
||||||
/// Returns a reload handle for dynamic log level changes and a guard
|
|
||||||
/// that must be kept alive for file logging.
|
|
||||||
pub fn init_logging(
|
|
||||||
opts: &LoggingOptions,
|
|
||||||
initial_filter: &str,
|
|
||||||
) -> (reload::Handle<EnvFilter, impl tracing::Subscriber + Send + Sync>, LoggingGuard) {
|
|
||||||
let (filter_layer, filter_handle) = reload::Layer::new(EnvFilter::new(initial_filter));
|
|
||||||
|
|
||||||
match &opts.destination {
|
|
||||||
LogDestination::Stderr => {
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(!opts.disable_colors)
|
|
||||||
.with_target(true);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::noop())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
LogDestination::Syslog => {
|
|
||||||
// Use a custom fmt layer that writes to syslog
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_target(true)
|
|
||||||
.with_writer(SyslogWriter::new);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::noop())
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
let (non_blocking, guard) = if *rotate_daily {
|
|
||||||
// Extract directory and filename prefix
|
|
||||||
let path = Path::new(path);
|
|
||||||
let dir = path.parent().unwrap_or(Path::new("/var/log"));
|
|
||||||
let prefix = path.file_name()
|
|
||||||
.and_then(|s| s.to_str())
|
|
||||||
.unwrap_or("telemt");
|
|
||||||
|
|
||||||
let file_appender = tracing_appender::rolling::daily(dir, prefix);
|
|
||||||
tracing_appender::non_blocking(file_appender)
|
|
||||||
} else {
|
|
||||||
let file = std::fs::OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(path)
|
|
||||||
.expect("Failed to open log file");
|
|
||||||
tracing_appender::non_blocking(file)
|
|
||||||
};
|
|
||||||
|
|
||||||
let fmt_layer = fmt::Layer::default()
|
|
||||||
.with_ansi(false)
|
|
||||||
.with_target(true)
|
|
||||||
.with_writer(non_blocking);
|
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
(filter_handle, LoggingGuard::new(Some(guard)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Syslog writer for tracing.
|
|
||||||
#[cfg(unix)]
|
|
||||||
struct SyslogWriter {
|
|
||||||
_private: (),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl SyslogWriter {
|
|
||||||
fn new() -> Self {
|
|
||||||
// Open syslog connection on first use
|
|
||||||
static INIT: std::sync::Once = std::sync::Once::new();
|
|
||||||
INIT.call_once(|| {
|
|
||||||
unsafe {
|
|
||||||
// Open syslog with ident "telemt", LOG_PID, LOG_DAEMON facility
|
|
||||||
let ident = b"telemt\0".as_ptr() as *const libc::c_char;
|
|
||||||
libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Self { _private: () }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl std::io::Write for SyslogWriter {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
||||||
// Convert to C string, stripping newlines
|
|
||||||
let msg = String::from_utf8_lossy(buf);
|
|
||||||
let msg = msg.trim_end();
|
|
||||||
|
|
||||||
if msg.is_empty() {
|
|
||||||
return Ok(buf.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine priority based on log level in the message
|
|
||||||
let priority = if msg.contains(" ERROR ") || msg.contains(" error ") {
|
|
||||||
libc::LOG_ERR
|
|
||||||
} else if msg.contains(" WARN ") || msg.contains(" warn ") {
|
|
||||||
libc::LOG_WARNING
|
|
||||||
} else if msg.contains(" INFO ") || msg.contains(" info ") {
|
|
||||||
libc::LOG_INFO
|
|
||||||
} else if msg.contains(" DEBUG ") || msg.contains(" debug ") {
|
|
||||||
libc::LOG_DEBUG
|
|
||||||
} else {
|
|
||||||
libc::LOG_INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write to syslog
|
|
||||||
let c_msg = std::ffi::CString::new(msg.as_bytes())
|
|
||||||
.unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap());
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
libc::syslog(priority, b"%s\0".as_ptr() as *const libc::c_char, c_msg.as_ptr());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> std::io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogWriter {
|
|
||||||
type Writer = SyslogWriter;
|
|
||||||
|
|
||||||
fn make_writer(&'a self) -> Self::Writer {
|
|
||||||
SyslogWriter::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse log destination from CLI arguments.
|
|
||||||
pub fn parse_log_destination(args: &[String]) -> LogDestination {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < args.len() {
|
|
||||||
match args[i].as_str() {
|
|
||||||
#[cfg(unix)]
|
|
||||||
"--syslog" => {
|
|
||||||
return LogDestination::Syslog;
|
|
||||||
}
|
|
||||||
"--log-file" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: args[i].clone(),
|
|
||||||
rotate_daily: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--log-file=") => {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: s.trim_start_matches("--log-file=").to_string(),
|
|
||||||
rotate_daily: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
"--log-file-daily" => {
|
|
||||||
i += 1;
|
|
||||||
if i < args.len() {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: args[i].clone(),
|
|
||||||
rotate_daily: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--log-file-daily=") => {
|
|
||||||
return LogDestination::File {
|
|
||||||
path: s.trim_start_matches("--log-file-daily=").to_string(),
|
|
||||||
rotate_daily: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
LogDestination::Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_default() {
|
|
||||||
let args: Vec<String> = vec![];
|
|
||||||
assert!(matches!(parse_log_destination(&args), LogDestination::Stderr));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_file() {
|
|
||||||
let args = vec!["--log-file".to_string(), "/var/log/telemt.log".to_string()];
|
|
||||||
match parse_log_destination(&args) {
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
assert_eq!(path, "/var/log/telemt.log");
|
|
||||||
assert!(!rotate_daily);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected File destination"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_file_daily() {
|
|
||||||
let args = vec!["--log-file-daily=/var/log/telemt".to_string()];
|
|
||||||
match parse_log_destination(&args) {
|
|
||||||
LogDestination::File { path, rotate_daily } => {
|
|
||||||
assert_eq!(path, "/var/log/telemt");
|
|
||||||
assert!(rotate_daily);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected File destination"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[test]
|
|
||||||
fn test_parse_log_destination_syslog() {
|
|
||||||
let args = vec!["--syslog".to_string()];
|
|
||||||
assert!(matches!(parse_log_destination(&args), LogDestination::Syslog));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,21 +6,11 @@ use tracing::{debug, error, info, warn};
|
|||||||
|
|
||||||
use crate::cli;
|
use crate::cli;
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use crate::logging::LogDestination;
|
|
||||||
use crate::transport::middle_proxy::{
|
use crate::transport::middle_proxy::{
|
||||||
ProxyConfigData, fetch_proxy_config_with_raw, load_proxy_config_cache, save_proxy_config_cache,
|
ProxyConfigData, fetch_proxy_config_with_raw, load_proxy_config_cache, save_proxy_config_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parsed CLI arguments.
|
pub(crate) fn parse_cli() -> (String, Option<PathBuf>, bool, Option<String>) {
|
||||||
pub(crate) struct CliArgs {
|
|
||||||
pub config_path: String,
|
|
||||||
pub data_path: Option<PathBuf>,
|
|
||||||
pub silent: bool,
|
|
||||||
pub log_level: Option<String>,
|
|
||||||
pub log_destination: LogDestination,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_cli() -> CliArgs {
|
|
||||||
let mut config_path = "config.toml".to_string();
|
let mut config_path = "config.toml".to_string();
|
||||||
let mut data_path: Option<PathBuf> = None;
|
let mut data_path: Option<PathBuf> = None;
|
||||||
let mut silent = false;
|
let mut silent = false;
|
||||||
@@ -28,9 +18,6 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
|
|
||||||
// Parse log destination
|
|
||||||
let log_destination = crate::logging::parse_log_destination(&args);
|
|
||||||
|
|
||||||
// Check for --init first (handled before tokio)
|
// Check for --init first (handled before tokio)
|
||||||
if let Some(init_opts) = cli::parse_init_args(&args) {
|
if let Some(init_opts) = cli::parse_init_args(&args) {
|
||||||
if let Err(e) = cli::run_init(init_opts) {
|
if let Err(e) = cli::run_init(init_opts) {
|
||||||
@@ -68,35 +55,34 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
log_level = Some(s.trim_start_matches("--log-level=").to_string());
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
print_help();
|
eprintln!("Usage: telemt [config.toml] [OPTIONS]");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" --data-path <DIR> Set data directory (absolute path; overrides config value)");
|
||||||
|
eprintln!(" --silent, -s Suppress info logs");
|
||||||
|
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
||||||
|
eprintln!(" --help, -h Show this help");
|
||||||
|
eprintln!();
|
||||||
|
eprintln!("Setup (fire-and-forget):");
|
||||||
|
eprintln!(
|
||||||
|
" --init Generate config, install systemd service, start"
|
||||||
|
);
|
||||||
|
eprintln!(" --port <PORT> Listen port (default: 443)");
|
||||||
|
eprintln!(
|
||||||
|
" --domain <DOMAIN> TLS domain for masking (default: www.google.com)"
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
" --secret <HEX> 32-char hex secret (auto-generated if omitted)"
|
||||||
|
);
|
||||||
|
eprintln!(" --user <NAME> Username (default: user)");
|
||||||
|
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
||||||
|
eprintln!(" --no-start Don't start the service after install");
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
println!("telemt {}", env!("CARGO_PKG_VERSION"));
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
// Skip daemon-related flags (already parsed)
|
|
||||||
"--daemon" | "-d" | "--foreground" | "-f" => {}
|
|
||||||
s if s.starts_with("--pid-file") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1; // skip value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-user") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--run-as-group") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if s.starts_with("--working-dir") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if !s.starts_with('-') => {
|
s if !s.starts_with('-') => {
|
||||||
config_path = s.to_string();
|
config_path = s.to_string();
|
||||||
}
|
}
|
||||||
@@ -107,77 +93,7 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
CliArgs {
|
(config_path, data_path, silent, log_level)
|
||||||
config_path,
|
|
||||||
data_path,
|
|
||||||
silent,
|
|
||||||
log_level,
|
|
||||||
log_destination,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
eprintln!("Usage: telemt [COMMAND] [OPTIONS] [config.toml]");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Commands:");
|
|
||||||
eprintln!(" run Run in foreground (default if no command given)");
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!(" start Start as background daemon");
|
|
||||||
eprintln!(" stop Stop a running daemon");
|
|
||||||
eprintln!(" reload Reload configuration (send SIGHUP)");
|
|
||||||
eprintln!(" status Check if daemon is running");
|
|
||||||
}
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Options:");
|
|
||||||
eprintln!(" --data-path <DIR> Set data directory (absolute path; overrides config value)");
|
|
||||||
eprintln!(" --silent, -s Suppress info logs");
|
|
||||||
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
|
||||||
eprintln!(" --help, -h Show this help");
|
|
||||||
eprintln!(" --version, -V Show version");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Logging options:");
|
|
||||||
eprintln!(" --log-file <PATH> Log to file (default: stderr)");
|
|
||||||
eprintln!(" --log-file-daily <PATH> Log to file with daily rotation");
|
|
||||||
#[cfg(unix)]
|
|
||||||
eprintln!(" --syslog Log to syslog (Unix only)");
|
|
||||||
eprintln!();
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!("Daemon options (Unix only):");
|
|
||||||
eprintln!(" --daemon, -d Fork to background (daemonize)");
|
|
||||||
eprintln!(" --foreground, -f Explicit foreground mode (for systemd)");
|
|
||||||
eprintln!(" --pid-file <PATH> PID file path (default: /var/run/telemt.pid)");
|
|
||||||
eprintln!(" --run-as-user <USER> Drop privileges to this user after binding");
|
|
||||||
eprintln!(" --run-as-group <GROUP> Drop privileges to this group after binding");
|
|
||||||
eprintln!(" --working-dir <DIR> Working directory for daemon mode");
|
|
||||||
eprintln!();
|
|
||||||
}
|
|
||||||
eprintln!("Setup (fire-and-forget):");
|
|
||||||
eprintln!(
|
|
||||||
" --init Generate config, install systemd service, start"
|
|
||||||
);
|
|
||||||
eprintln!(" --port <PORT> Listen port (default: 443)");
|
|
||||||
eprintln!(
|
|
||||||
" --domain <DOMAIN> TLS domain for masking (default: www.google.com)"
|
|
||||||
);
|
|
||||||
eprintln!(
|
|
||||||
" --secret <HEX> 32-char hex secret (auto-generated if omitted)"
|
|
||||||
);
|
|
||||||
eprintln!(" --user <NAME> Username (default: user)");
|
|
||||||
eprintln!(" --config-dir <DIR> Config directory (default: /etc/telemt)");
|
|
||||||
eprintln!(" --no-start Don't start the service after install");
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
eprintln!();
|
|
||||||
eprintln!("Examples:");
|
|
||||||
eprintln!(" telemt config.toml Run in foreground");
|
|
||||||
eprintln!(" telemt start config.toml Start as daemon");
|
|
||||||
eprintln!(" telemt start --pid-file /tmp/t.pid Start with custom PID file");
|
|
||||||
eprintln!(" telemt stop Stop daemon");
|
|
||||||
eprintln!(" telemt reload Reload configuration");
|
|
||||||
eprintln!(" telemt status Check daemon status");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
||||||
|
|||||||
@@ -47,56 +47,8 @@ use crate::transport::middle_proxy::MePool;
|
|||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
use helpers::parse_cli;
|
use helpers::parse_cli;
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::daemon::{DaemonOptions, PidFile, drop_privileges};
|
|
||||||
|
|
||||||
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
||||||
///
|
|
||||||
/// On Unix, daemon options should be handled before calling this function
|
|
||||||
/// (daemonization must happen before tokio runtime starts).
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub async fn run_with_daemon(
|
|
||||||
daemon_opts: DaemonOptions,
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
run_inner(daemon_opts).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs the full telemt runtime startup pipeline and blocks until shutdown.
|
|
||||||
///
|
|
||||||
/// This is the main entry point for non-daemon mode or when called as a library.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
// Parse CLI to get daemon options even in simple run() path
|
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
||||||
let daemon_opts = crate::cli::parse_daemon_args(&args);
|
|
||||||
run_inner(daemon_opts).await
|
|
||||||
}
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
run_inner().await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn run_inner(
|
|
||||||
daemon_opts: DaemonOptions,
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
|
|
||||||
// Acquire PID file if daemonizing or if explicitly requested
|
|
||||||
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
|
||||||
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
|
||||||
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
|
||||||
if let Err(e) = pf.acquire() {
|
|
||||||
eprintln!("[telemt] {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
Some(pf)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_started_at = Instant::now();
|
let process_started_at = Instant::now();
|
||||||
let process_started_at_epoch_secs = SystemTime::now()
|
let process_started_at_epoch_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -106,12 +58,7 @@ async fn run_inner(
|
|||||||
startup_tracker
|
startup_tracker
|
||||||
.start_component(COMPONENT_CONFIG_LOAD, Some("load and validate config".to_string()))
|
.start_component(COMPONENT_CONFIG_LOAD, Some("load and validate config".to_string()))
|
||||||
.await;
|
.await;
|
||||||
let cli_args = parse_cli();
|
let (config_path, data_path, cli_silent, cli_log_level) = parse_cli();
|
||||||
let config_path = cli_args.config_path;
|
|
||||||
let data_path = cli_args.data_path;
|
|
||||||
let cli_silent = cli_args.silent;
|
|
||||||
let cli_log_level = cli_args.log_level;
|
|
||||||
let log_destination = cli_args.log_destination;
|
|
||||||
|
|
||||||
let mut config = match ProxyConfig::load(&config_path) {
|
let mut config = match ProxyConfig::load(&config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -183,43 +130,17 @@ async fn run_inner(
|
|||||||
.start_component(COMPONENT_TRACING_INIT, Some("initialize tracing subscriber".to_string()))
|
.start_component(COMPONENT_TRACING_INIT, Some("initialize tracing subscriber".to_string()))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Initialize logging based on destination
|
// Configure color output based on config
|
||||||
let _logging_guard: Option<crate::logging::LoggingGuard>;
|
let fmt_layer = if config.general.disable_colors {
|
||||||
match log_destination {
|
fmt::Layer::default().with_ansi(false)
|
||||||
crate::logging::LogDestination::Stderr => {
|
} else {
|
||||||
// Default: log to stderr (works with systemd journald)
|
fmt::Layer::default().with_ansi(true)
|
||||||
let fmt_layer = if config.general.disable_colors {
|
};
|
||||||
fmt::Layer::default().with_ansi(false)
|
|
||||||
} else {
|
|
||||||
fmt::Layer::default().with_ansi(true)
|
|
||||||
};
|
|
||||||
tracing_subscriber::registry()
|
|
||||||
.with(filter_layer)
|
|
||||||
.with(fmt_layer)
|
|
||||||
.init();
|
|
||||||
_logging_guard = None;
|
|
||||||
}
|
|
||||||
#[cfg(unix)]
|
|
||||||
crate::logging::LogDestination::Syslog => {
|
|
||||||
// Syslog: for OpenRC/FreeBSD
|
|
||||||
let logging_opts = crate::logging::LoggingOptions {
|
|
||||||
destination: log_destination,
|
|
||||||
disable_colors: true,
|
|
||||||
};
|
|
||||||
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
|
||||||
_logging_guard = Some(guard);
|
|
||||||
}
|
|
||||||
crate::logging::LogDestination::File { .. } => {
|
|
||||||
// File logging with optional rotation
|
|
||||||
let logging_opts = crate::logging::LoggingOptions {
|
|
||||||
destination: log_destination,
|
|
||||||
disable_colors: true,
|
|
||||||
};
|
|
||||||
let (_, guard) = crate::logging::init_logging(&logging_opts, "info");
|
|
||||||
_logging_guard = Some(guard);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(filter_layer)
|
||||||
|
.with(fmt_layer)
|
||||||
|
.init();
|
||||||
startup_tracker
|
startup_tracker
|
||||||
.complete_component(COMPONENT_TRACING_INIT, Some("tracing initialized".to_string()))
|
.complete_component(COMPONENT_TRACING_INIT, Some("tracing initialized".to_string()))
|
||||||
.await;
|
.await;
|
||||||
@@ -634,17 +555,6 @@ async fn run_inner(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop privileges after binding sockets (which may require root for port < 1024)
|
|
||||||
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
|
|
||||||
if let Err(e) = drop_privileges(
|
|
||||||
daemon_opts.user.as_deref(),
|
|
||||||
daemon_opts.group.as_deref(),
|
|
||||||
) {
|
|
||||||
error!(error = %e, "Failed to drop privileges");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime_tasks::apply_runtime_log_filter(
|
runtime_tasks::apply_runtime_log_filter(
|
||||||
has_rust_log,
|
has_rust_log,
|
||||||
&effective_log_level,
|
&effective_log_level,
|
||||||
@@ -665,9 +575,6 @@ async fn run_inner(
|
|||||||
|
|
||||||
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
runtime_tasks::mark_runtime_ready(&startup_tracker).await;
|
||||||
|
|
||||||
// Spawn signal handlers for SIGUSR1/SIGUSR2 (non-shutdown signals)
|
|
||||||
shutdown::spawn_signal_handlers(stats.clone(), process_started_at);
|
|
||||||
|
|
||||||
listeners::spawn_tcp_accept_loops(
|
listeners::spawn_tcp_accept_loops(
|
||||||
listeners,
|
listeners,
|
||||||
config_rx.clone(),
|
config_rx.clone(),
|
||||||
@@ -685,7 +592,7 @@ async fn run_inner(
|
|||||||
max_connections.clone(),
|
max_connections.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
shutdown::wait_for_shutdown(process_started_at, me_pool, stats).await;
|
shutdown::wait_for_shutdown(process_started_at, me_pool).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,211 +1,42 @@
|
|||||||
//! Shutdown and signal handling for telemt.
|
|
||||||
//!
|
|
||||||
//! Handles graceful shutdown on various signals:
|
|
||||||
//! - SIGINT (Ctrl+C) / SIGTERM: Graceful shutdown
|
|
||||||
//! - SIGQUIT: Graceful shutdown with stats dump
|
|
||||||
//! - SIGUSR1: Reserved for log rotation (logs acknowledgment)
|
|
||||||
//! - SIGUSR2: Dump runtime status to log
|
|
||||||
//!
|
|
||||||
//! SIGHUP is handled separately in config/hot_reload.rs for config reload.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
use tokio::signal;
|
use tokio::signal;
|
||||||
use tracing::{info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::stats::Stats;
|
|
||||||
use crate::transport::middle_proxy::MePool;
|
use crate::transport::middle_proxy::MePool;
|
||||||
|
|
||||||
use super::helpers::{format_uptime, unit_label};
|
use super::helpers::{format_uptime, unit_label};
|
||||||
|
|
||||||
/// Signal that triggered shutdown.
|
pub(crate) async fn wait_for_shutdown(process_started_at: Instant, me_pool: Option<Arc<MePool>>) {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
match signal::ctrl_c().await {
|
||||||
pub enum ShutdownSignal {
|
Ok(()) => {
|
||||||
/// SIGINT (Ctrl+C)
|
let shutdown_started_at = Instant::now();
|
||||||
Interrupt,
|
info!("Shutting down...");
|
||||||
/// SIGTERM
|
let uptime_secs = process_started_at.elapsed().as_secs();
|
||||||
Terminate,
|
info!("Uptime: {}", format_uptime(uptime_secs));
|
||||||
/// SIGQUIT (with stats dump)
|
if let Some(pool) = &me_pool {
|
||||||
Quit,
|
match tokio::time::timeout(Duration::from_secs(2), pool.shutdown_send_close_conn_all())
|
||||||
}
|
.await
|
||||||
|
{
|
||||||
impl std::fmt::Display for ShutdownSignal {
|
Ok(total) => {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
info!(
|
||||||
match self {
|
close_conn_sent = total,
|
||||||
ShutdownSignal::Interrupt => write!(f, "SIGINT"),
|
"ME shutdown: RPC_CLOSE_CONN broadcast completed"
|
||||||
ShutdownSignal::Terminate => write!(f, "SIGTERM"),
|
);
|
||||||
ShutdownSignal::Quit => write!(f, "SIGQUIT"),
|
}
|
||||||
}
|
Err(_) => {
|
||||||
}
|
warn!("ME shutdown: RPC_CLOSE_CONN broadcast timed out");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits for a shutdown signal and performs graceful shutdown.
|
|
||||||
pub(crate) async fn wait_for_shutdown(
|
|
||||||
process_started_at: Instant,
|
|
||||||
me_pool: Option<Arc<MePool>>,
|
|
||||||
stats: Arc<Stats>,
|
|
||||||
) {
|
|
||||||
let signal = wait_for_shutdown_signal().await;
|
|
||||||
perform_shutdown(signal, process_started_at, me_pool, &stats).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits for any shutdown signal (SIGINT, SIGTERM, SIGQUIT).
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
|
||||||
let mut sigint = signal(SignalKind::interrupt()).expect("Failed to register SIGINT handler");
|
|
||||||
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to register SIGTERM handler");
|
|
||||||
let mut sigquit = signal(SignalKind::quit()).expect("Failed to register SIGQUIT handler");
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = sigint.recv() => ShutdownSignal::Interrupt,
|
|
||||||
_ = sigterm.recv() => ShutdownSignal::Terminate,
|
|
||||||
_ = sigquit.recv() => ShutdownSignal::Quit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
async fn wait_for_shutdown_signal() -> ShutdownSignal {
|
|
||||||
signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
|
|
||||||
ShutdownSignal::Interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs graceful shutdown sequence.
|
|
||||||
async fn perform_shutdown(
|
|
||||||
signal: ShutdownSignal,
|
|
||||||
process_started_at: Instant,
|
|
||||||
me_pool: Option<Arc<MePool>>,
|
|
||||||
stats: &Stats,
|
|
||||||
) {
|
|
||||||
let shutdown_started_at = Instant::now();
|
|
||||||
info!(signal = %signal, "Received shutdown signal");
|
|
||||||
|
|
||||||
// Dump stats if SIGQUIT
|
|
||||||
if signal == ShutdownSignal::Quit {
|
|
||||||
dump_stats(stats, process_started_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Shutting down...");
|
|
||||||
let uptime_secs = process_started_at.elapsed().as_secs();
|
|
||||||
info!("Uptime: {}", format_uptime(uptime_secs));
|
|
||||||
|
|
||||||
// Graceful ME pool shutdown
|
|
||||||
if let Some(pool) = &me_pool {
|
|
||||||
match tokio::time::timeout(Duration::from_secs(2), pool.shutdown_send_close_conn_all()).await
|
|
||||||
{
|
|
||||||
Ok(total) => {
|
|
||||||
info!(
|
|
||||||
close_conn_sent = total,
|
|
||||||
"ME shutdown: RPC_CLOSE_CONN broadcast completed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("ME shutdown: RPC_CLOSE_CONN broadcast timed out");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
|
||||||
info!(
|
|
||||||
"Shutdown completed successfully in {} {}.",
|
|
||||||
shutdown_secs,
|
|
||||||
unit_label(shutdown_secs, "second", "seconds")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dumps runtime statistics to the log.
|
|
||||||
fn dump_stats(stats: &Stats, process_started_at: Instant) {
|
|
||||||
let uptime_secs = process_started_at.elapsed().as_secs();
|
|
||||||
|
|
||||||
info!("=== Runtime Statistics Dump ===");
|
|
||||||
info!("Uptime: {}", format_uptime(uptime_secs));
|
|
||||||
|
|
||||||
// Connection stats
|
|
||||||
info!(
|
|
||||||
"Connections: total={}, current={} (direct={}, me={}), bad={}",
|
|
||||||
stats.get_connects_all(),
|
|
||||||
stats.get_current_connections_total(),
|
|
||||||
stats.get_current_connections_direct(),
|
|
||||||
stats.get_current_connections_me(),
|
|
||||||
stats.get_connects_bad(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ME pool stats
|
|
||||||
info!(
|
|
||||||
"ME keepalive: sent={}, pong={}, failed={}, timeout={}",
|
|
||||||
stats.get_me_keepalive_sent(),
|
|
||||||
stats.get_me_keepalive_pong(),
|
|
||||||
stats.get_me_keepalive_failed(),
|
|
||||||
stats.get_me_keepalive_timeout(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Relay stats
|
|
||||||
info!(
|
|
||||||
"Relay adaptive: promotions={}, demotions={}, hard_promotions={}",
|
|
||||||
stats.get_relay_adaptive_promotions_total(),
|
|
||||||
stats.get_relay_adaptive_demotions_total(),
|
|
||||||
stats.get_relay_adaptive_hard_promotions_total(),
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("=== End Statistics Dump ===");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawns a background task to handle operational signals (SIGUSR1, SIGUSR2).
|
|
||||||
///
|
|
||||||
/// These signals don't trigger shutdown but perform specific actions:
|
|
||||||
/// - SIGUSR1: Log rotation acknowledgment (for external log rotation tools)
|
|
||||||
/// - SIGUSR2: Dump runtime status to log
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub(crate) fn spawn_signal_handlers(
|
|
||||||
stats: Arc<Stats>,
|
|
||||||
process_started_at: Instant,
|
|
||||||
) {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut sigusr1 = signal(SignalKind::user_defined1())
|
|
||||||
.expect("Failed to register SIGUSR1 handler");
|
|
||||||
let mut sigusr2 = signal(SignalKind::user_defined2())
|
|
||||||
.expect("Failed to register SIGUSR2 handler");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = sigusr1.recv() => {
|
|
||||||
handle_sigusr1();
|
|
||||||
}
|
|
||||||
_ = sigusr2.recv() => {
|
|
||||||
handle_sigusr2(&stats, process_started_at);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let shutdown_secs = shutdown_started_at.elapsed().as_secs();
|
||||||
|
info!(
|
||||||
|
"Shutdown completed successfully in {} {}.",
|
||||||
|
shutdown_secs,
|
||||||
|
unit_label(shutdown_secs, "second", "seconds")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
Err(e) => error!("Signal error: {}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No-op on non-Unix platforms.
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub(crate) fn spawn_signal_handlers(
|
|
||||||
_stats: Arc<Stats>,
|
|
||||||
_process_started_at: Instant,
|
|
||||||
) {
|
|
||||||
// No SIGUSR1/SIGUSR2 on non-Unix
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles SIGUSR1 - log rotation signal.
|
|
||||||
///
|
|
||||||
/// This signal is typically sent by logrotate or similar tools after
|
|
||||||
/// rotating log files. Since tracing-subscriber doesn't natively support
|
|
||||||
/// reopening files, we just acknowledge the signal. If file logging is
|
|
||||||
/// added in the future, this would reopen log file handles.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn handle_sigusr1() {
|
|
||||||
info!("SIGUSR1 received - log rotation acknowledged");
|
|
||||||
// Future: If using file-based logging, reopen file handles here
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles SIGUSR2 - dump runtime status.
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn handle_sigusr2(stats: &Stats, process_started_at: Instant) {
|
|
||||||
info!("SIGUSR2 received - dumping runtime status");
|
|
||||||
dump_stats(stats, process_started_at);
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/main.rs
52
src/main.rs
@@ -4,12 +4,8 @@ mod api;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
#[cfg(unix)]
|
|
||||||
mod daemon;
|
|
||||||
mod error;
|
mod error;
|
||||||
mod ip_tracker;
|
mod ip_tracker;
|
||||||
mod logging;
|
|
||||||
mod service;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod ip_tracker_regression_tests;
|
mod ip_tracker_regression_tests;
|
||||||
mod maestro;
|
mod maestro;
|
||||||
@@ -24,49 +20,7 @@ mod tls_front;
|
|||||||
mod transport;
|
mod transport;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
#[tokio::main]
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
let cmd = cli::parse_command(&args);
|
maestro::run().await
|
||||||
|
|
||||||
// Handle subcommands that don't need the server (stop, reload, status, init)
|
|
||||||
if let Some(exit_code) = cli::execute_subcommand(&cmd) {
|
|
||||||
std::process::exit(exit_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Unix, handle daemonization before starting tokio runtime
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
let daemon_opts = cmd.daemon_opts;
|
|
||||||
|
|
||||||
// Daemonize if requested (must happen before tokio runtime starts)
|
|
||||||
if daemon_opts.should_daemonize() {
|
|
||||||
match daemon::daemonize(daemon_opts.working_dir.as_deref()) {
|
|
||||||
Ok(daemon::DaemonizeResult::Parent) => {
|
|
||||||
// Parent process exits successfully
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
Ok(daemon::DaemonizeResult::Child) => {
|
|
||||||
// Continue as daemon child
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[telemt] Daemonization failed: {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now start tokio runtime and run the server
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(maestro::run_with_daemon(daemon_opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
{
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()?
|
|
||||||
.block_on(maestro::run())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
//! Service manager integration for telemt.
|
|
||||||
//!
|
|
||||||
//! Supports generating service files for:
|
|
||||||
//! - systemd (Linux)
|
|
||||||
//! - OpenRC (Alpine, Gentoo)
|
|
||||||
//! - rc.d (FreeBSD)
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
/// Detected init/service system.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum InitSystem {
|
|
||||||
/// systemd (most modern Linux distributions)
|
|
||||||
Systemd,
|
|
||||||
/// OpenRC (Alpine, Gentoo, some BSDs)
|
|
||||||
OpenRC,
|
|
||||||
/// FreeBSD rc.d
|
|
||||||
FreeBSDRc,
|
|
||||||
/// No known init system detected
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for InitSystem {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
InitSystem::Systemd => write!(f, "systemd"),
|
|
||||||
InitSystem::OpenRC => write!(f, "OpenRC"),
|
|
||||||
InitSystem::FreeBSDRc => write!(f, "FreeBSD rc.d"),
|
|
||||||
InitSystem::Unknown => write!(f, "unknown"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detects the init system in use on the current host.
|
|
||||||
pub fn detect_init_system() -> InitSystem {
|
|
||||||
// Check for systemd first (most common on Linux)
|
|
||||||
if Path::new("/run/systemd/system").exists() {
|
|
||||||
return InitSystem::Systemd;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for OpenRC
|
|
||||||
if Path::new("/sbin/openrc-run").exists() || Path::new("/sbin/openrc").exists() {
|
|
||||||
return InitSystem::OpenRC;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for FreeBSD rc.d
|
|
||||||
if Path::new("/etc/rc.subr").exists() && Path::new("/etc/rc.d").exists() {
|
|
||||||
return InitSystem::FreeBSDRc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: check if systemctl exists even without /run/systemd
|
|
||||||
if Path::new("/usr/bin/systemctl").exists() || Path::new("/bin/systemctl").exists() {
|
|
||||||
return InitSystem::Systemd;
|
|
||||||
}
|
|
||||||
|
|
||||||
InitSystem::Unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the default service file path for the given init system.
|
|
||||||
pub fn service_file_path(init_system: InitSystem) -> &'static str {
|
|
||||||
match init_system {
|
|
||||||
InitSystem::Systemd => "/etc/systemd/system/telemt.service",
|
|
||||||
InitSystem::OpenRC => "/etc/init.d/telemt",
|
|
||||||
InitSystem::FreeBSDRc => "/usr/local/etc/rc.d/telemt",
|
|
||||||
InitSystem::Unknown => "/etc/init.d/telemt",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for generating service files.
|
|
||||||
pub struct ServiceOptions<'a> {
|
|
||||||
/// Path to the telemt executable
|
|
||||||
pub exe_path: &'a Path,
|
|
||||||
/// Path to the configuration file
|
|
||||||
pub config_path: &'a Path,
|
|
||||||
/// User to run as (optional)
|
|
||||||
pub user: Option<&'a str>,
|
|
||||||
/// Group to run as (optional)
|
|
||||||
pub group: Option<&'a str>,
|
|
||||||
/// PID file path
|
|
||||||
pub pid_file: &'a str,
|
|
||||||
/// Working directory
|
|
||||||
pub working_dir: Option<&'a str>,
|
|
||||||
/// Description
|
|
||||||
pub description: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Default for ServiceOptions<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
exe_path: Path::new("/usr/local/bin/telemt"),
|
|
||||||
config_path: Path::new("/etc/telemt/config.toml"),
|
|
||||||
user: Some("telemt"),
|
|
||||||
group: Some("telemt"),
|
|
||||||
pid_file: "/var/run/telemt.pid",
|
|
||||||
working_dir: Some("/var/lib/telemt"),
|
|
||||||
description: "Telemt MTProxy - Telegram MTProto Proxy",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a service file for the given init system.
|
|
||||||
pub fn generate_service_file(init_system: InitSystem, opts: &ServiceOptions) -> String {
|
|
||||||
match init_system {
|
|
||||||
InitSystem::Systemd => generate_systemd_unit(opts),
|
|
||||||
InitSystem::OpenRC => generate_openrc_script(opts),
|
|
||||||
InitSystem::FreeBSDRc => generate_freebsd_rc_script(opts),
|
|
||||||
InitSystem::Unknown => generate_systemd_unit(opts), // Default to systemd format
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates an enhanced systemd unit file.
|
|
||||||
fn generate_systemd_unit(opts: &ServiceOptions) -> String {
|
|
||||||
let user_line = opts.user.map(|u| format!("User={}", u)).unwrap_or_default();
|
|
||||||
let group_line = opts.group.map(|g| format!("Group={}", g)).unwrap_or_default();
|
|
||||||
let working_dir = opts.working_dir.map(|d| format!("WorkingDirectory={}", d)).unwrap_or_default();
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"[Unit]
|
|
||||||
Description={description}
|
|
||||||
Documentation=https://github.com/telemt/telemt
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart={exe} --foreground --pid-file {pid_file} {config}
|
|
||||||
ExecReload=/bin/kill -HUP $MAINPID
|
|
||||||
PIDFile={pid_file}
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
{user}
|
|
||||||
{group}
|
|
||||||
{working_dir}
|
|
||||||
|
|
||||||
# Resource limits
|
|
||||||
LimitNOFILE=65535
|
|
||||||
LimitNPROC=4096
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
PrivateTmp=true
|
|
||||||
PrivateDevices=true
|
|
||||||
ProtectKernelTunables=true
|
|
||||||
ProtectKernelModules=true
|
|
||||||
ProtectControlGroups=true
|
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
|
||||||
RestrictNamespaces=true
|
|
||||||
RestrictRealtime=true
|
|
||||||
RestrictSUIDSGID=true
|
|
||||||
MemoryDenyWriteExecute=true
|
|
||||||
LockPersonality=true
|
|
||||||
|
|
||||||
# Allow binding to privileged ports and writing to specific paths
|
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
||||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
||||||
ReadWritePaths=/etc/telemt /var/run /var/lib/telemt
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
"#,
|
|
||||||
description = opts.description,
|
|
||||||
exe = opts.exe_path.display(),
|
|
||||||
config = opts.config_path.display(),
|
|
||||||
pid_file = opts.pid_file,
|
|
||||||
user = user_line,
|
|
||||||
group = group_line,
|
|
||||||
working_dir = working_dir,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates an OpenRC init script.
|
|
||||||
fn generate_openrc_script(opts: &ServiceOptions) -> String {
|
|
||||||
let user = opts.user.unwrap_or("root");
|
|
||||||
let group = opts.group.unwrap_or("root");
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"#!/sbin/openrc-run
|
|
||||||
# OpenRC init script for telemt
|
|
||||||
|
|
||||||
description="{description}"
|
|
||||||
command="{exe}"
|
|
||||||
command_args="--daemon --syslog --pid-file {pid_file} {config}"
|
|
||||||
command_user="{user}:{group}"
|
|
||||||
pidfile="{pid_file}"
|
|
||||||
|
|
||||||
depend() {{
|
|
||||||
need net
|
|
||||||
use logger
|
|
||||||
after firewall
|
|
||||||
}}
|
|
||||||
|
|
||||||
start_pre() {{
|
|
||||||
checkpath --directory --owner {user}:{group} --mode 0755 /var/run
|
|
||||||
checkpath --directory --owner {user}:{group} --mode 0755 /var/lib/telemt
|
|
||||||
checkpath --directory --owner {user}:{group} --mode 0755 /var/log/telemt
|
|
||||||
}}
|
|
||||||
|
|
||||||
reload() {{
|
|
||||||
ebegin "Reloading ${{RC_SVCNAME}}"
|
|
||||||
start-stop-daemon --signal HUP --pidfile "${{pidfile}}"
|
|
||||||
eend $?
|
|
||||||
}}
|
|
||||||
"#,
|
|
||||||
description = opts.description,
|
|
||||||
exe = opts.exe_path.display(),
|
|
||||||
config = opts.config_path.display(),
|
|
||||||
pid_file = opts.pid_file,
|
|
||||||
user = user,
|
|
||||||
group = group,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a FreeBSD rc.d script.
|
|
||||||
fn generate_freebsd_rc_script(opts: &ServiceOptions) -> String {
|
|
||||||
let user = opts.user.unwrap_or("root");
|
|
||||||
let group = opts.group.unwrap_or("wheel");
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"#!/bin/sh
|
|
||||||
#
|
|
||||||
# PROVIDE: telemt
|
|
||||||
# REQUIRE: LOGIN NETWORKING
|
|
||||||
# KEYWORD: shutdown
|
|
||||||
#
|
|
||||||
# Add the following lines to /etc/rc.conf to enable telemt:
|
|
||||||
#
|
|
||||||
# telemt_enable="YES"
|
|
||||||
# telemt_config="/etc/telemt/config.toml" # optional
|
|
||||||
# telemt_user="telemt" # optional
|
|
||||||
# telemt_group="telemt" # optional
|
|
||||||
#
|
|
||||||
|
|
||||||
. /etc/rc.subr
|
|
||||||
|
|
||||||
name="telemt"
|
|
||||||
rcvar="telemt_enable"
|
|
||||||
desc="{description}"
|
|
||||||
|
|
||||||
load_rc_config $name
|
|
||||||
|
|
||||||
: ${{telemt_enable:="NO"}}
|
|
||||||
: ${{telemt_config:="{config}"}}
|
|
||||||
: ${{telemt_user:="{user}"}}
|
|
||||||
: ${{telemt_group:="{group}"}}
|
|
||||||
: ${{telemt_pidfile:="{pid_file}"}}
|
|
||||||
|
|
||||||
pidfile="${{telemt_pidfile}}"
|
|
||||||
command="{exe}"
|
|
||||||
command_args="--daemon --syslog --pid-file ${{telemt_pidfile}} ${{telemt_config}}"
|
|
||||||
|
|
||||||
start_precmd="telemt_prestart"
|
|
||||||
reload_cmd="telemt_reload"
|
|
||||||
extra_commands="reload"
|
|
||||||
|
|
||||||
telemt_prestart() {{
|
|
||||||
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/run
|
|
||||||
install -d -o ${{telemt_user}} -g ${{telemt_group}} -m 755 /var/lib/telemt
|
|
||||||
}}
|
|
||||||
|
|
||||||
telemt_reload() {{
|
|
||||||
if [ -f "${{pidfile}}" ]; then
|
|
||||||
echo "Reloading ${{name}} configuration."
|
|
||||||
kill -HUP $(cat ${{pidfile}})
|
|
||||||
else
|
|
||||||
echo "${{name}} is not running."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}}
|
|
||||||
|
|
||||||
run_rc_command "$1"
|
|
||||||
"#,
|
|
||||||
description = opts.description,
|
|
||||||
exe = opts.exe_path.display(),
|
|
||||||
config = opts.config_path.display(),
|
|
||||||
pid_file = opts.pid_file,
|
|
||||||
user = user,
|
|
||||||
group = group,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Installation instructions for each init system.
|
|
||||||
pub fn installation_instructions(init_system: InitSystem) -> &'static str {
|
|
||||||
match init_system {
|
|
||||||
InitSystem::Systemd => {
|
|
||||||
r#"To install and enable the service:
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable telemt
|
|
||||||
sudo systemctl start telemt
|
|
||||||
|
|
||||||
To check status:
|
|
||||||
sudo systemctl status telemt
|
|
||||||
|
|
||||||
To view logs:
|
|
||||||
journalctl -u telemt -f
|
|
||||||
|
|
||||||
To reload configuration:
|
|
||||||
sudo systemctl reload telemt
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
InitSystem::OpenRC => {
|
|
||||||
r#"To install and enable the service:
|
|
||||||
sudo chmod +x /etc/init.d/telemt
|
|
||||||
sudo rc-update add telemt default
|
|
||||||
sudo rc-service telemt start
|
|
||||||
|
|
||||||
To check status:
|
|
||||||
sudo rc-service telemt status
|
|
||||||
|
|
||||||
To reload configuration:
|
|
||||||
sudo rc-service telemt reload
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
InitSystem::FreeBSDRc => {
|
|
||||||
r#"To install and enable the service:
|
|
||||||
sudo chmod +x /usr/local/etc/rc.d/telemt
|
|
||||||
sudo sysrc telemt_enable="YES"
|
|
||||||
sudo service telemt start
|
|
||||||
|
|
||||||
To check status:
|
|
||||||
sudo service telemt status
|
|
||||||
|
|
||||||
To reload configuration:
|
|
||||||
sudo service telemt reload
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
InitSystem::Unknown => {
|
|
||||||
r#"No supported init system detected.
|
|
||||||
You may need to create a service file manually or run telemt directly:
|
|
||||||
telemt start /etc/telemt/config.toml
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_systemd_unit_generation() {
|
|
||||||
let opts = ServiceOptions::default();
|
|
||||||
let unit = generate_systemd_unit(&opts);
|
|
||||||
assert!(unit.contains("[Unit]"));
|
|
||||||
assert!(unit.contains("[Service]"));
|
|
||||||
assert!(unit.contains("[Install]"));
|
|
||||||
assert!(unit.contains("ExecReload="));
|
|
||||||
assert!(unit.contains("PIDFile="));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_openrc_script_generation() {
|
|
||||||
let opts = ServiceOptions::default();
|
|
||||||
let script = generate_openrc_script(&opts);
|
|
||||||
assert!(script.contains("#!/sbin/openrc-run"));
|
|
||||||
assert!(script.contains("depend()"));
|
|
||||||
assert!(script.contains("reload()"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_freebsd_rc_script_generation() {
|
|
||||||
let opts = ServiceOptions::default();
|
|
||||||
let script = generate_freebsd_rc_script(&opts);
|
|
||||||
assert!(script.contains("#!/bin/sh"));
|
|
||||||
assert!(script.contains("PROVIDE: telemt"));
|
|
||||||
assert!(script.contains("run_rc_command"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_service_file_paths() {
|
|
||||||
assert_eq!(service_file_path(InitSystem::Systemd), "/etc/systemd/system/telemt.service");
|
|
||||||
assert_eq!(service_file_path(InitSystem::OpenRC), "/etc/init.d/telemt");
|
|
||||||
assert_eq!(service_file_path(InitSystem::FreeBSDRc), "/usr/local/etc/rc.d/telemt");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user