Merge 036e49faf0 into 57c519436e
This commit is contained in:
commit
f8a6dfc1ce
6 changed files with 692 additions and 86 deletions
141
build/Dockerfile
141
build/Dockerfile
|
|
@ -1,33 +1,138 @@
|
||||||
# The build image could be golang, but it currently does not support riscv64. Only debian:sid does, at the time of writing.
|
# syntax=docker/dockerfile:1
|
||||||
FROM debian:sid-slim AS build
|
### The Deb install is just a repack of the official ProtonMail Bridge deb package with less dependencies.
|
||||||
|
### I recommend you don't use this. It's here for legacy reasons.
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BUILD STAGE
|
||||||
|
# =============================================================================
|
||||||
|
FROM golang:1.26-trixie AS build
|
||||||
|
|
||||||
ARG version
|
ARG version
|
||||||
|
ENV version=${version}
|
||||||
|
|
||||||
# Install dependencies
|
# ---------------------------------------------------------------------------
|
||||||
RUN apt-get update && apt-get install -y golang build-essential libsecret-1-dev
|
# LTO — why ENV CGO_LDFLAGS didn't work:
|
||||||
|
#
|
||||||
|
# The Makefile's go-build-finalize on Linux expands to:
|
||||||
|
# CGO_LDFLAGS="${LIBFIDO2_LDFLAGS}" go build ...
|
||||||
|
# That inline shell assignment *overwrites* any CGO_LDFLAGS inherited from
|
||||||
|
# the Docker environment entirely.
|
||||||
|
#
|
||||||
|
# Fix: pass LIBFIDO2_LDFLAGS on the make command line (see RUN step below).
|
||||||
|
# make CLI variables override Makefile ?= and := definitions, so our flags
|
||||||
|
# reach the CGO_LDFLAGS inline assignment verbatim.
|
||||||
|
#
|
||||||
|
# To disable LTO at build time:
|
||||||
|
# docker buildx build --build-arg CGO_LTO_FLAGS="" ...
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ARG CGO_LTO_FLAGS="-flto=auto -O2"
|
||||||
|
|
||||||
# Build
|
# -trimpath: the Makefile never sets this flag, so GOFLAGS is safe here.
|
||||||
|
# Strips host machine paths from the binary → reproducible builds, no path leaks.
|
||||||
|
ENV GOFLAGS="-trimpath"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# System dependencies
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
libsecret-1-dev \
|
||||||
|
libfido2-dev \
|
||||||
|
libcbor-dev
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fetch source via BuildKit's git ADD (shallow clone at the requested ref).
|
||||||
|
# Because the entire tree arrives in one shot we cannot pre-copy go.mod,
|
||||||
|
# but the module cache mount below still avoids re-downloading dependencies.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
ADD https://github.com/ProtonMail/proton-bridge.git#${version} /build/
|
ADD https://github.com/ProtonMail/proton-bridge.git#${version} /build/
|
||||||
WORKDIR /build/
|
WORKDIR /build/
|
||||||
RUN make build-nogui vault-editor
|
|
||||||
|
|
||||||
FROM debian:sid-slim
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pre-seed the Go module cache.
|
||||||
|
# This layer is invalidated only when go.mod / go.sum change (dep upgrades).
|
||||||
|
# On pure source changes the cache is warm and this step is near-instant.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||||
|
go mod download -x
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Compile.
|
||||||
|
#
|
||||||
|
# Cache mounts:
|
||||||
|
# /go/pkg/mod — downloaded module zips (read, shared)
|
||||||
|
# /root/.cache/go-build — incremental .a object cache (read-write, locked)
|
||||||
|
#
|
||||||
|
# make variable overrides:
|
||||||
|
# LIBFIDO2_LDFLAGS — appends LTO flags to the CGO_LDFLAGS inline assignment
|
||||||
|
# in go-build-finalize without touching any other flags.
|
||||||
|
# Default Makefile value: -lfido2 -lcbor -lssl -lcrypto
|
||||||
|
#
|
||||||
|
# Why not -s -w here:
|
||||||
|
# The Makefile builds its own -ldflags chain:
|
||||||
|
# BUILD_FLAGS += -ldflags '${GO_LDFLAGS}'
|
||||||
|
# GO_LDFLAGS carries the -X version/revision constants computed at build
|
||||||
|
# time. Overriding GO_LDFLAGS on the CLI loses those constants.
|
||||||
|
# GOFLAGS=-ldflags="-s -w" is silently ignored when the Makefile already
|
||||||
|
# passes -ldflags on the go build command line (last -ldflags wins in go).
|
||||||
|
# → We strip the binaries in the next step instead.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
|
||||||
|
make build-nogui vault-editor \
|
||||||
|
BUILD_ENV=prod \
|
||||||
|
LIBFIDO2_LDFLAGS="-lfido2 -lcbor -lssl -lcrypto ${CGO_LTO_FLAGS}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Strip debug symbols — equivalent to go -ldflags="-s -w".
|
||||||
|
# Typically saves 25-35 % on binary size.
|
||||||
|
# Remove this RUN if you need delve / runtime stack traces.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
RUN strip --strip-all \
|
||||||
|
/build/proton-bridge \
|
||||||
|
/build/bridge \
|
||||||
|
/build/vault-editor
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# RUNTIME STAGE — minimal Debian image, no build toolchain
|
||||||
|
# =============================================================================
|
||||||
|
FROM debian:trixie-slim
|
||||||
LABEL maintainer="Simon Felding <sife@adm.ku.dk>"
|
LABEL maintainer="Simon Felding <sife@adm.ku.dk>"
|
||||||
|
|
||||||
|
# Select PTY tool for manage/attach commands: dtach (default), abduco, reptyr
|
||||||
|
ARG PTY_TOOL=dtach
|
||||||
|
ENV PTY_TOOL=${PTY_TOOL}
|
||||||
|
|
||||||
EXPOSE 25/tcp
|
EXPOSE 25/tcp
|
||||||
EXPOSE 143/tcp
|
EXPOSE 143/tcp
|
||||||
|
|
||||||
# Install dependencies and protonmail bridge
|
WORKDIR /protonmail
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends socat pass libsecret-1-0 ca-certificates \
|
COPY gpgparams entrypoint.sh healthcheck.sh /protonmail/
|
||||||
|
|
||||||
|
COPY --from=build /build/bridge /protonmail/
|
||||||
|
COPY --from=build /build/proton-bridge /protonmail/
|
||||||
|
COPY --from=build /build/vault-editor /protonmail/
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
socat pass libsecret-1-0 libfido2-1 ca-certificates procps \
|
||||||
|
&& case "${PTY_TOOL}" in \
|
||||||
|
dtach) apt-get install -y --no-install-recommends dtach ;; \
|
||||||
|
abduco) apt-get install -y --no-install-recommends abduco ;; \
|
||||||
|
reptyr) apt-get install -y --no-install-recommends reptyr ;; \
|
||||||
|
*) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
&& chmod +x /protonmail/entrypoint.sh /protonmail/healthcheck.sh \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy bash scripts
|
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=120s \
|
||||||
COPY gpgparams entrypoint.sh /protonmail/
|
CMD /protonmail/healthcheck.sh
|
||||||
|
|
||||||
# Copy protonmail
|
ENTRYPOINT ["/protonmail/entrypoint.sh"]
|
||||||
COPY --from=build /build/bridge /protonmail/
|
CMD ["run"]
|
||||||
COPY --from=build /build/proton-bridge /protonmail/
|
|
||||||
COPY --from=build /build/vault-editor /protonmail/
|
|
||||||
|
|
||||||
ENTRYPOINT ["bash", "/protonmail/entrypoint.sh"]
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,247 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -ex
|
set -euo pipefail
|
||||||
|
|
||||||
# Initialize
|
PTY_TOOL="${PTY_TOOL:-dtach}"
|
||||||
if [[ $1 == init ]]; then
|
BRIDGE_SOCK=/protonmail/bridge.sock
|
||||||
|
BRIDGE_PID_FILE=/protonmail/bridge.pid
|
||||||
|
|
||||||
# Initialize pass
|
# Validate PTY_TOOL early and ensure the selected binary is present.
|
||||||
gpg --generate-key --batch /protonmail/gpgparams
|
case "${PTY_TOOL}" in
|
||||||
pass init pass-key
|
dtach|abduco|reptyr)
|
||||||
|
if ! command -v "${PTY_TOOL}" &>/dev/null; then
|
||||||
|
echo "ERROR: PTY_TOOL=${PTY_TOOL} but '${PTY_TOOL}' was not found in PATH." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: PTY_TOOL=${PTY_TOOL} is not supported. Valid values: dtach, abduco, reptyr." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
# Kill the other instance as only one can be running at a time.
|
# Clean stale gpg-agent sockets left from a previous run
|
||||||
# This allows users to run entrypoint init inside a running conainter
|
rm -f /root/.gnupg/S.gpg-agent* 2>/dev/null || true
|
||||||
# which is useful in a k8s environment.
|
|
||||||
# || true to make sure this would not fail in case there is no running instance.
|
|
||||||
pkill protonmail-bridge || true
|
|
||||||
|
|
||||||
# Login
|
# --- PTY helpers (only used by: init, manage, attach) ---
|
||||||
/protonmail/proton-bridge --cli $@
|
|
||||||
|
|
||||||
else
|
pty_start() {
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) dtach -n "${BRIDGE_SOCK}" "$@" ;;
|
||||||
|
abduco) abduco -n bridge "$@" ;;
|
||||||
|
# reptyr re-attaches existing PIDs; use nohup+setsid to launch headlessly instead
|
||||||
|
reptyr) setsid "$@" </dev/null &>/dev/null & echo $! > "${BRIDGE_PID_FILE}" ;;
|
||||||
|
*) echo "ERROR: pty_start: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# socat will make the conn appear to come from 127.0.0.1
|
pty_attach() {
|
||||||
# ProtonMail Bridge currently expects that.
|
case "${PTY_TOOL}" in
|
||||||
# It also allows us to bind to the real ports :)
|
dtach) exec dtach -a "${BRIDGE_SOCK}" -e '^\' ;;
|
||||||
socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
|
abduco) exec abduco -a bridge ;;
|
||||||
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &
|
reptyr) exec reptyr "$(cat "${BRIDGE_PID_FILE}")" ;;
|
||||||
|
*) echo "ERROR: pty_attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Start protonmail
|
detach_hint() {
|
||||||
# Fake a terminal, so it does not quit because of EOF...
|
case "${PTY_TOOL}" in
|
||||||
rm -f faketty
|
dtach|abduco) echo "Ctrl+\\" ;;
|
||||||
mkfifo faketty
|
reptyr) echo "Ctrl+C" ;;
|
||||||
cat faketty | /protonmail/proton-bridge --cli $@
|
*) echo "(unknown)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
fi
|
# True if the abduco session named 'bridge' is listed as running.
|
||||||
|
abduco_session_alive() {
|
||||||
|
abduco -l 2>/dev/null | grep -qw 'bridge'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait up to $1 seconds for the bridge session (socket or PID file) to appear.
|
||||||
|
wait_for_session() {
|
||||||
|
local timeout="${1:-10}"
|
||||||
|
local elapsed=0
|
||||||
|
while [[ "${elapsed}" -lt "${timeout}" ]]; do
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;;
|
||||||
|
abduco) abduco_session_alive && return 0 ;;
|
||||||
|
reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;;
|
||||||
|
*) echo "ERROR: wait_for_session: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
sleep 1
|
||||||
|
(( elapsed++ )) || true
|
||||||
|
done
|
||||||
|
echo "ERROR: bridge session did not start within ${timeout}s." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Commands ---
|
||||||
|
|
||||||
|
CMD="${1:-run}"
|
||||||
|
|
||||||
|
case "${CMD}" in
|
||||||
|
|
||||||
|
init)
|
||||||
|
# One-time setup: generate GPG key, init password store, interactive login.
|
||||||
|
# Run as: docker run -it <image> init
|
||||||
|
gpg --generate-key --batch /protonmail/gpgparams
|
||||||
|
pass init pass-key
|
||||||
|
exec /protonmail/proton-bridge --cli
|
||||||
|
;;
|
||||||
|
|
||||||
|
manage)
|
||||||
|
# Open an interactive --cli session for account management (add/remove accounts etc).
|
||||||
|
# Run as: docker run -it --rm -v <data-volume> <image> manage
|
||||||
|
# NOTE: Stop the running daemon container first to avoid port/lock conflicts.
|
||||||
|
CONTAINER_ID=$(hostname)
|
||||||
|
echo " Starting management session... [PTY_TOOL=${PTY_TOOL}]"
|
||||||
|
pty_start /protonmail/proton-bridge --cli
|
||||||
|
|
||||||
|
# Wait for the session socket/pid to appear before printing attach instructions
|
||||||
|
wait_for_session 10
|
||||||
|
|
||||||
|
echo " Management session ready."
|
||||||
|
echo " Attach: docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach"
|
||||||
|
echo " Detach: $(detach_hint)"
|
||||||
|
|
||||||
|
# Block so the container stays alive for `docker exec attach`.
|
||||||
|
# If stdin is a tty (docker run -it), jump straight into the session.
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
pty_attach
|
||||||
|
else
|
||||||
|
# No tty: wait until the bridge session disappears then exit cleanly.
|
||||||
|
while true; do
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) [[ -S "${BRIDGE_SOCK}" ]] || break ;;
|
||||||
|
abduco) abduco_session_alive || break ;;
|
||||||
|
reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;;
|
||||||
|
*) echo "ERROR: manage loop: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
attach)
|
||||||
|
# Reattach to a running manage session.
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach)
|
||||||
|
if [[ ! -S "${BRIDGE_SOCK}" ]]; then
|
||||||
|
echo "ERROR: No active dtach session found (${BRIDGE_SOCK} does not exist)." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
abduco)
|
||||||
|
if ! abduco_session_alive; then
|
||||||
|
echo "ERROR: No active abduco session 'bridge' found." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
reptyr)
|
||||||
|
if [[ ! -f "${BRIDGE_PID_FILE}" ]]; then
|
||||||
|
echo "ERROR: No active session found (${BRIDGE_PID_FILE} does not exist)." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
pty_attach
|
||||||
|
;;
|
||||||
|
|
||||||
|
run)
|
||||||
|
# Daemon mode: --noninteractive runs headless, output goes directly to docker logs.
|
||||||
|
CONTAINER_ID=$(hostname)
|
||||||
|
|
||||||
|
# Cleanup handler: forward SIGTERM/SIGINT and reap child processes cleanly.
|
||||||
|
# As PID 1, without this Docker's SIGTERM would leave children running until
|
||||||
|
# the kill timeout expires.
|
||||||
|
BRIDGE_PID=
|
||||||
|
SOCAT_SMTP_PID=
|
||||||
|
SOCAT_IMAP_PID=
|
||||||
|
_cleanup_done=0
|
||||||
|
cleanup() {
|
||||||
|
[[ "${_cleanup_done}" -eq 1 ]] && return
|
||||||
|
_cleanup_done=1
|
||||||
|
echo " Shutting down bridge and port-forwards..." >&2
|
||||||
|
[[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true
|
||||||
|
[[ -n "${SOCAT_SMTP_PID:-}" ]] && kill "${SOCAT_SMTP_PID}" 2>/dev/null || true
|
||||||
|
[[ -n "${SOCAT_IMAP_PID:-}" ]] && kill "${SOCAT_IMAP_PID}" 2>/dev/null || true
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT SIGTERM SIGINT
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " ProtonMail Bridge daemon starting..."
|
||||||
|
echo " Container: ${CONTAINER_ID}"
|
||||||
|
echo ""
|
||||||
|
echo " Available commands:"
|
||||||
|
echo " First-time setup:"
|
||||||
|
echo " docker run -it <image> init"
|
||||||
|
echo ""
|
||||||
|
echo " Manage accounts (stop daemon first):"
|
||||||
|
echo " docker run -it --rm -v <data-volume> <image> manage"
|
||||||
|
echo ""
|
||||||
|
echo " Attach to a running manage session:"
|
||||||
|
echo " docker exec -it <container> /protonmail/entrypoint.sh attach"
|
||||||
|
echo ""
|
||||||
|
echo " View logs:"
|
||||||
|
echo " docker logs -f ${CONTAINER_ID}"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Start bridge in background so we can wait for it to bind its ports
|
||||||
|
# before socat begins accepting connections.
|
||||||
|
/protonmail/proton-bridge --noninteractive &
|
||||||
|
BRIDGE_PID=$!
|
||||||
|
|
||||||
|
# Wait for bridge to open its local SMTP and IMAP ports (up to 60s).
|
||||||
|
# Abort immediately if the bridge process exits before the port is ready.
|
||||||
|
echo " Waiting for bridge ports 1025/1143..."
|
||||||
|
for port in 1025 1143; do
|
||||||
|
elapsed=0
|
||||||
|
until socat -u OPEN:/dev/null TCP:127.0.0.1:${port} 2>/dev/null; do
|
||||||
|
if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then
|
||||||
|
echo "ERROR: bridge process (pid ${BRIDGE_PID}) exited before port ${port} became ready." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
(( elapsed++ )) || true
|
||||||
|
if [[ "${elapsed}" -ge 60 ]]; then
|
||||||
|
echo "ERROR: bridge port ${port} did not open within 60s." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo " Port ${port} ready."
|
||||||
|
done
|
||||||
|
|
||||||
|
# socat forwards standard ports to bridge's localhost-only listener ports.
|
||||||
|
# retry=30,interval=2 handles transient bridge restarts without dropping connections.
|
||||||
|
socat TCP-LISTEN:25,fork,reuseaddr TCP:127.0.0.1:1025,nodelay,retry=30,interval=2 &
|
||||||
|
SOCAT_SMTP_PID=$!
|
||||||
|
socat TCP-LISTEN:143,fork,reuseaddr TCP:127.0.0.1:1143,nodelay,retry=30,interval=2 &
|
||||||
|
SOCAT_IMAP_PID=$!
|
||||||
|
|
||||||
|
# Verify both socat processes started
|
||||||
|
sleep 1
|
||||||
|
for pid in "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}"; do
|
||||||
|
if ! kill -0 "${pid}" 2>/dev/null; then
|
||||||
|
echo "ERROR: socat port-forward (pid ${pid}) failed to start." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait on bridge; EXIT trap will bring down socat when bridge exits.
|
||||||
|
wait "${BRIDGE_PID}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage: entrypoint.sh [init|manage|attach|run]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
|
|
||||||
38
build/healthcheck.sh
Normal file
38
build/healthcheck.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Proton Bridge healthcheck — probes all 4 ports in parallel.
|
||||||
|
# Exit 0 = healthy, 1 = unhealthy (Docker HEALTHCHECK contract).
|
||||||
|
|
||||||
|
TIMEOUT=5 # seconds per probe
|
||||||
|
|
||||||
|
check_smtp() {
|
||||||
|
local port=$1
|
||||||
|
echo 'QUIT' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \
|
||||||
|
| grep -q '^220'
|
||||||
|
}
|
||||||
|
|
||||||
|
check_imap() {
|
||||||
|
local port=$1
|
||||||
|
printf 'A1 LOGOUT\r\n' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \
|
||||||
|
| grep -q '^\* OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fire all probes in parallel, capture PIDs
|
||||||
|
check_smtp 25 & PID_SMTP_25=$!
|
||||||
|
check_imap 143 & PID_IMAP_143=$!
|
||||||
|
check_smtp 1025 & PID_SMTP_1025=$!
|
||||||
|
check_imap 1143 & PID_IMAP_1143=$!
|
||||||
|
|
||||||
|
# Collect results — || prevents set -e from exiting early on probe failure
|
||||||
|
FAIL=0
|
||||||
|
wait $PID_SMTP_25 || { echo "FAIL smtp:25"; FAIL=1; }
|
||||||
|
wait $PID_IMAP_143 || { echo "FAIL imap:143"; FAIL=1; }
|
||||||
|
wait $PID_SMTP_1025 || { echo "FAIL smtp:1025"; FAIL=1; }
|
||||||
|
wait $PID_IMAP_1143 || { echo "FAIL imap:1143"; FAIL=1; }
|
||||||
|
|
||||||
|
if [[ $FAIL -eq 0 ]]; then
|
||||||
|
echo "OK smtp:25 imap:143 smtp:1025 imap:1143"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $FAIL
|
||||||
|
|
@ -12,17 +12,32 @@ RUN bash /install.sh
|
||||||
FROM debian:sid-slim
|
FROM debian:sid-slim
|
||||||
LABEL maintainer="Simon Felding <sife@adm.ku.dk>"
|
LABEL maintainer="Simon Felding <sife@adm.ku.dk>"
|
||||||
|
|
||||||
|
# Select PTY tool for manage/attach commands: dtach (default), abduco, reptyr
|
||||||
|
ARG PTY_TOOL=dtach
|
||||||
|
ENV PTY_TOOL=${PTY_TOOL}
|
||||||
|
|
||||||
EXPOSE 25/tcp
|
EXPOSE 25/tcp
|
||||||
EXPOSE 143/tcp
|
EXPOSE 143/tcp
|
||||||
|
|
||||||
WORKDIR /protonmail
|
WORKDIR /protonmail
|
||||||
|
|
||||||
# Copy bash scripts
|
# Copy bash scripts
|
||||||
COPY gpgparams entrypoint.sh PACKAGE /protonmail/
|
COPY gpgparams entrypoint.sh healthcheck.sh PACKAGE /protonmail/
|
||||||
COPY --from=build /protonmail.deb /tmp/protonmail.deb
|
COPY --from=build /protonmail.deb /tmp/protonmail.deb
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends /tmp/protonmail.deb socat pass libsecret-1-0 ca-certificates procps \
|
&& apt-get install -y --no-install-recommends /tmp/protonmail.deb socat pass libsecret-1-0 libfido2-1 ca-certificates procps \
|
||||||
|
&& case "${PTY_TOOL}" in \
|
||||||
|
dtach) apt-get install -y --no-install-recommends dtach ;; \
|
||||||
|
abduco) apt-get install -y --no-install-recommends abduco ;; \
|
||||||
|
reptyr) apt-get install -y --no-install-recommends reptyr ;; \
|
||||||
|
*) echo "Unsupported PTY_TOOL: ${PTY_TOOL}. Supported: dtach, abduco, reptyr." >&2 ; exit 1 ;; \
|
||||||
|
esac \
|
||||||
|
&& chmod +x /protonmail/entrypoint.sh /protonmail/healthcheck.sh \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
CMD ["bash", "/protonmail/entrypoint.sh"]
|
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=120s \
|
||||||
|
CMD /protonmail/healthcheck.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/protonmail/entrypoint.sh"]
|
||||||
|
CMD ["run"]
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,247 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -ex
|
set -euo pipefail
|
||||||
|
|
||||||
# Initialize
|
PTY_TOOL="${PTY_TOOL:-dtach}"
|
||||||
if [[ $1 == init ]]; then
|
BRIDGE_SOCK=/protonmail/bridge.sock
|
||||||
|
BRIDGE_PID_FILE=/protonmail/bridge.pid
|
||||||
|
|
||||||
# # Parse parameters
|
# Validate PTY_TOOL early and ensure the selected binary is present.
|
||||||
# TFP="" # Default empty two factor passcode
|
case "${PTY_TOOL}" in
|
||||||
# shift # skip `init`
|
dtach|abduco|reptyr)
|
||||||
# while [[ $# -gt 0 ]]; do
|
if ! command -v "${PTY_TOOL}" &>/dev/null; then
|
||||||
# key="$1"
|
echo "ERROR: PTY_TOOL=${PTY_TOOL} but '${PTY_TOOL}' was not found in PATH." >&2
|
||||||
# case $key in
|
exit 1
|
||||||
# -u|--username)
|
fi
|
||||||
# USERNAME="$2"
|
;;
|
||||||
# ;;
|
*)
|
||||||
# -p|--password)
|
echo "ERROR: PTY_TOOL=${PTY_TOOL} is not supported. Valid values: dtach, abduco, reptyr." >&2
|
||||||
# PASSWORD="$2"
|
exit 1
|
||||||
# ;;
|
;;
|
||||||
# -t|--twofactor)
|
esac
|
||||||
# TWOFACTOR="$2"
|
|
||||||
# ;;
|
|
||||||
# esac
|
|
||||||
# shift
|
|
||||||
# shift
|
|
||||||
# done
|
|
||||||
|
|
||||||
# Initialize pass
|
# Clean stale gpg-agent sockets left from a previous run
|
||||||
gpg --generate-key --batch /protonmail/gpgparams
|
rm -f /root/.gnupg/S.gpg-agent* 2>/dev/null || true
|
||||||
pass init pass-key
|
|
||||||
|
|
||||||
# Login
|
# --- PTY helpers (only used by: init, manage, attach) ---
|
||||||
protonmail-bridge --cli
|
|
||||||
|
|
||||||
else
|
pty_start() {
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) dtach -n "${BRIDGE_SOCK}" "$@" ;;
|
||||||
|
abduco) abduco -n bridge "$@" ;;
|
||||||
|
# reptyr re-attaches existing PIDs; use nohup+setsid to launch headlessly instead
|
||||||
|
reptyr) setsid "$@" </dev/null &>/dev/null & echo $! > "${BRIDGE_PID_FILE}" ;;
|
||||||
|
*) echo "ERROR: pty_start: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# socat will make the conn appear to come from 127.0.0.1
|
pty_attach() {
|
||||||
# ProtonMail Bridge currently expects that.
|
case "${PTY_TOOL}" in
|
||||||
# It also allows us to bind to the real ports :)
|
dtach) exec dtach -a "${BRIDGE_SOCK}" -e '^\' ;;
|
||||||
socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
|
abduco) exec abduco -a bridge ;;
|
||||||
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &
|
reptyr) exec reptyr "$(cat "${BRIDGE_PID_FILE}")" ;;
|
||||||
|
*) echo "ERROR: pty_attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Start protonmail
|
detach_hint() {
|
||||||
# Fake a terminal, so it does not quit because of EOF...
|
case "${PTY_TOOL}" in
|
||||||
rm -f faketty
|
dtach|abduco) echo "Ctrl+\\" ;;
|
||||||
mkfifo faketty
|
reptyr) echo "Ctrl+C" ;;
|
||||||
cat faketty | protonmail-bridge --cli
|
*) echo "(unknown)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
fi
|
# True if the abduco session named 'bridge' is listed as running.
|
||||||
|
abduco_session_alive() {
|
||||||
|
abduco -l 2>/dev/null | grep -qw 'bridge'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait up to $1 seconds for the bridge session (socket or PID file) to appear.
|
||||||
|
wait_for_session() {
|
||||||
|
local timeout="${1:-10}"
|
||||||
|
local elapsed=0
|
||||||
|
while [[ "${elapsed}" -lt "${timeout}" ]]; do
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) [[ -S "${BRIDGE_SOCK}" ]] && return 0 ;;
|
||||||
|
abduco) abduco_session_alive && return 0 ;;
|
||||||
|
reptyr) [[ -f "${BRIDGE_PID_FILE}" ]] && return 0 ;;
|
||||||
|
*) echo "ERROR: wait_for_session: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
sleep 1
|
||||||
|
(( elapsed++ )) || true
|
||||||
|
done
|
||||||
|
echo "ERROR: bridge session did not start within ${timeout}s." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Commands ---
|
||||||
|
|
||||||
|
CMD="${1:-run}"
|
||||||
|
|
||||||
|
case "${CMD}" in
|
||||||
|
|
||||||
|
init)
|
||||||
|
# One-time setup: generate GPG key, init password store, interactive login.
|
||||||
|
# Run as: docker run -it <image> init
|
||||||
|
gpg --generate-key --batch /protonmail/gpgparams
|
||||||
|
pass init pass-key
|
||||||
|
exec protonmail-bridge --cli
|
||||||
|
;;
|
||||||
|
|
||||||
|
manage)
|
||||||
|
# Open an interactive --cli session for account management (add/remove accounts etc).
|
||||||
|
# Run as: docker run -it --rm -v <data-volume> <image> manage
|
||||||
|
# NOTE: Stop the running daemon container first to avoid port/lock conflicts.
|
||||||
|
CONTAINER_ID=$(hostname)
|
||||||
|
echo " Starting management session... [PTY_TOOL=${PTY_TOOL}]"
|
||||||
|
pty_start protonmail-bridge --cli
|
||||||
|
|
||||||
|
# Wait for the session socket/pid to appear before printing attach instructions
|
||||||
|
wait_for_session 10
|
||||||
|
|
||||||
|
echo " Management session ready."
|
||||||
|
echo " Attach: docker exec -it ${CONTAINER_ID} /protonmail/entrypoint.sh attach"
|
||||||
|
echo " Detach: $(detach_hint)"
|
||||||
|
|
||||||
|
# Block so the container stays alive for `docker exec attach`.
|
||||||
|
# If stdin is a tty (docker run -it), jump straight into the session.
|
||||||
|
if [[ -t 0 ]]; then
|
||||||
|
pty_attach
|
||||||
|
else
|
||||||
|
# No tty: wait until the bridge session disappears then exit cleanly.
|
||||||
|
while true; do
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach) [[ -S "${BRIDGE_SOCK}" ]] || break ;;
|
||||||
|
abduco) abduco_session_alive || break ;;
|
||||||
|
reptyr) kill -0 "$(cat "${BRIDGE_PID_FILE}" 2>/dev/null)" 2>/dev/null || break ;;
|
||||||
|
*) echo "ERROR: manage loop: unsupported PTY_TOOL=${PTY_TOOL}" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
attach)
|
||||||
|
# Reattach to a running manage session.
|
||||||
|
case "${PTY_TOOL}" in
|
||||||
|
dtach)
|
||||||
|
if [[ ! -S "${BRIDGE_SOCK}" ]]; then
|
||||||
|
echo "ERROR: No active dtach session found (${BRIDGE_SOCK} does not exist)." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
abduco)
|
||||||
|
if ! abduco_session_alive; then
|
||||||
|
echo "ERROR: No active abduco session 'bridge' found." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
reptyr)
|
||||||
|
if [[ ! -f "${BRIDGE_PID_FILE}" ]]; then
|
||||||
|
echo "ERROR: No active session found (${BRIDGE_PID_FILE} does not exist)." >&2
|
||||||
|
echo " Start one first: docker exec -it <container> /protonmail/entrypoint.sh manage" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: attach: unsupported PTY_TOOL=${PTY_TOOL}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
pty_attach
|
||||||
|
;;
|
||||||
|
|
||||||
|
run)
|
||||||
|
# Daemon mode: --noninteractive runs headless, output goes directly to docker logs.
|
||||||
|
CONTAINER_ID=$(hostname)
|
||||||
|
|
||||||
|
# Cleanup handler: forward SIGTERM/SIGINT and reap child processes cleanly.
|
||||||
|
# As PID 1, without this Docker's SIGTERM would leave children running until
|
||||||
|
# the kill timeout expires.
|
||||||
|
BRIDGE_PID=
|
||||||
|
SOCAT_SMTP_PID=
|
||||||
|
SOCAT_IMAP_PID=
|
||||||
|
_cleanup_done=0
|
||||||
|
cleanup() {
|
||||||
|
[[ "${_cleanup_done}" -eq 1 ]] && return
|
||||||
|
_cleanup_done=1
|
||||||
|
echo " Shutting down bridge and port-forwards..." >&2
|
||||||
|
[[ -n "${BRIDGE_PID:-}" ]] && kill "${BRIDGE_PID}" 2>/dev/null || true
|
||||||
|
[[ -n "${SOCAT_SMTP_PID:-}" ]] && kill "${SOCAT_SMTP_PID}" 2>/dev/null || true
|
||||||
|
[[ -n "${SOCAT_IMAP_PID:-}" ]] && kill "${SOCAT_IMAP_PID}" 2>/dev/null || true
|
||||||
|
wait 2>/dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT SIGTERM SIGINT
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo " ProtonMail Bridge daemon starting..."
|
||||||
|
echo " Container: ${CONTAINER_ID}"
|
||||||
|
echo ""
|
||||||
|
echo " Available commands:"
|
||||||
|
echo " First-time setup:"
|
||||||
|
echo " docker run -it <image> init"
|
||||||
|
echo ""
|
||||||
|
echo " Manage accounts (stop daemon first):"
|
||||||
|
echo " docker run -it --rm -v <data-volume> <image> manage"
|
||||||
|
echo ""
|
||||||
|
echo " Attach to a running manage session:"
|
||||||
|
echo " docker exec -it <container> /protonmail/entrypoint.sh attach"
|
||||||
|
echo ""
|
||||||
|
echo " View logs:"
|
||||||
|
echo " docker logs -f ${CONTAINER_ID}"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Start bridge in background so we can wait for it to bind its ports
|
||||||
|
# before socat begins accepting connections.
|
||||||
|
protonmail-bridge --noninteractive &
|
||||||
|
BRIDGE_PID=$!
|
||||||
|
|
||||||
|
# Wait for bridge to open its local SMTP and IMAP ports (up to 60s).
|
||||||
|
# Abort immediately if the bridge process exits before the port is ready.
|
||||||
|
echo " Waiting for bridge ports 1025/1143..."
|
||||||
|
for port in 1025 1143; do
|
||||||
|
elapsed=0
|
||||||
|
until socat -u OPEN:/dev/null TCP:127.0.0.1:${port} 2>/dev/null; do
|
||||||
|
if ! kill -0 "${BRIDGE_PID}" 2>/dev/null; then
|
||||||
|
echo "ERROR: bridge process (pid ${BRIDGE_PID}) exited before port ${port} became ready." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
(( elapsed++ )) || true
|
||||||
|
if [[ "${elapsed}" -ge 60 ]]; then
|
||||||
|
echo "ERROR: bridge port ${port} did not open within 60s." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo " Port ${port} ready."
|
||||||
|
done
|
||||||
|
|
||||||
|
# socat forwards standard ports to bridge's localhost-only listener ports.
|
||||||
|
# retry=30,interval=2 handles transient bridge restarts without dropping connections.
|
||||||
|
socat TCP-LISTEN:25,fork,reuseaddr TCP:127.0.0.1:1025,nodelay,retry=30,interval=2 &
|
||||||
|
SOCAT_SMTP_PID=$!
|
||||||
|
socat TCP-LISTEN:143,fork,reuseaddr TCP:127.0.0.1:1143,nodelay,retry=30,interval=2 &
|
||||||
|
SOCAT_IMAP_PID=$!
|
||||||
|
|
||||||
|
# Verify both socat processes started
|
||||||
|
sleep 1
|
||||||
|
for pid in "${SOCAT_SMTP_PID}" "${SOCAT_IMAP_PID}"; do
|
||||||
|
if ! kill -0 "${pid}" 2>/dev/null; then
|
||||||
|
echo "ERROR: socat port-forward (pid ${pid}) failed to start." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait on bridge; EXIT trap will bring down socat when bridge exits.
|
||||||
|
wait "${BRIDGE_PID}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Usage: entrypoint.sh [init|manage|attach|run]" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
|
||||||
|
esac
|
||||||
|
|
|
||||||
38
deb/healthcheck.sh
Normal file
38
deb/healthcheck.sh
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Proton Bridge healthcheck — probes all 4 ports in parallel.
|
||||||
|
# Exit 0 = healthy, 1 = unhealthy (Docker HEALTHCHECK contract).
|
||||||
|
|
||||||
|
TIMEOUT=5 # seconds per probe
|
||||||
|
|
||||||
|
check_smtp() {
|
||||||
|
local port=$1
|
||||||
|
echo 'QUIT' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \
|
||||||
|
| grep -q '^220'
|
||||||
|
}
|
||||||
|
|
||||||
|
check_imap() {
|
||||||
|
local port=$1
|
||||||
|
printf 'A1 LOGOUT\r\n' | socat -T${TIMEOUT} - TCP4:localhost:${port} 2>/dev/null \
|
||||||
|
| grep -q '^\* OK'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fire all probes in parallel, capture PIDs
|
||||||
|
check_smtp 25 & PID_SMTP_25=$!
|
||||||
|
check_imap 143 & PID_IMAP_143=$!
|
||||||
|
check_smtp 1025 & PID_SMTP_1025=$!
|
||||||
|
check_imap 1143 & PID_IMAP_1143=$!
|
||||||
|
|
||||||
|
# Collect results — || prevents set -e from exiting early on probe failure
|
||||||
|
FAIL=0
|
||||||
|
wait $PID_SMTP_25 || { echo "FAIL smtp:25"; FAIL=1; }
|
||||||
|
wait $PID_IMAP_143 || { echo "FAIL imap:143"; FAIL=1; }
|
||||||
|
wait $PID_SMTP_1025 || { echo "FAIL smtp:1025"; FAIL=1; }
|
||||||
|
wait $PID_IMAP_1143 || { echo "FAIL imap:1143"; FAIL=1; }
|
||||||
|
|
||||||
|
if [[ $FAIL -eq 0 ]]; then
|
||||||
|
echo "OK smtp:25 imap:143 smtp:1025 imap:1143"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit $FAIL
|
||||||
Loading…
Add table
Add a link
Reference in a new issue