diff --git a/scripts/Common/Software/docker/services/jellyfin/docker-compose.base.yml b/scripts/Common/Software/docker/services/jellyfin/docker-compose.base.yml index 40153bb2..b94de85e 100644 --- a/scripts/Common/Software/docker/services/jellyfin/docker-compose.base.yml +++ b/scripts/Common/Software/docker/services/jellyfin/docker-compose.base.yml @@ -94,6 +94,7 @@ services: rtorrent: build: dockerfile: ./rtorrent.Dockerfile + target: rtorrent context: . restart: unless-stopped hostname: rtorrent @@ -101,7 +102,10 @@ services: PUID: 1337 PGID: 1337 PHOME: /config + PVPN_CMD_ARGS: --p2p --random MAX_UPTIME: 43200 + sysctls: + net.ipv4.conf.all.rp_filter: 2 command: -o ratio.enable= -o ratio.min.set=200 -o ratio.max.set=10000 diff --git a/scripts/Common/Software/docker/services/jellyfin/main.fish b/scripts/Common/Software/docker/services/jellyfin/main.fish index 2e6f93cc..fafa8823 100755 --- a/scripts/Common/Software/docker/services/jellyfin/main.fish +++ b/scripts/Common/Software/docker/services/jellyfin/main.fish @@ -20,7 +20,9 @@ begin sudo cp "$dir/docker-compose.base.yml" "$root" sudo cp "$dir/.dockerignore" "$root" sudo cp "$dir/pvpn-cli.py" "$root" + sudo cp "$dir/proton-entrypoint.sh" "$root" sudo cp "$dir/rtorrent.Dockerfile" "$root" + sudo cp "$dir/rtorrent-entrypoint.sh" "$root" sudo cp "$source" "$overrides" installDockerService $argv diff --git a/scripts/Common/Software/docker/services/jellyfin/proton-entrypoint.sh b/scripts/Common/Software/docker/services/jellyfin/proton-entrypoint.sh new file mode 100644 index 00000000..5c17617a --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/proton-entrypoint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +if [ -z "$PVPN_USERNAME" ] +then + echo "Please set the \`\$PVPN_USERNAME\` variable to your ProtonVPN username." >&2 + exit 1 +fi + +export DBUS_SESSION_BUS_ADDRESS=`dbus-daemon --fork --config-file=/usr/share/dbus-1/session.conf --print-address` +mkdir -p ~/.config +ln -s /proton ~/.config/protonvpn + +pvpn-cli || true +ip link show protonwire0 >/dev/null 2>&1 || exit +exec "$@" diff --git a/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py b/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py index 9fe85edb..41f9ee48 100644 --- a/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py +++ b/scripts/Common/Software/docker/services/jellyfin/pvpn-cli.py @@ -1,100 +1,72 @@ +#!/usr/bin/env python3 from argparse import ArgumentParser -from os import chmod, environ -from os.path import dirname -from random import choice -from re import M +from os import environ import shlex import subprocess import sys -from protonvpn_cli import connection -from protonvpn_cli.constants import PASSFILE -from protonvpn_cli.utils import check_init, get_fastest_server, get_servers, set_config_value, pull_server_data +from typing import List +from protonvpn_cli.cli_wrapper import CLIWrapper +from protonvpn_nm_lib.api import FeatureEnum +from protonvpn_nm_lib.core.servers.list import LogicalServer +cli = CLIWrapper() -def run_proton(args): - exit( - subprocess.run( - ["proton"], - cwd="/app", - env=dict( - environ, - PVPN_CMD_ARGS=" ".join(args))).returncode) +if not cli.protonvpn.get_session().is_valid: + print("You are not logged in.", file=sys.stderr) -environ["PVPN_USERNAME"] = environ["PVPN_USERNAME"] + (environ["PVPN_TAGS"] or "") + if (sys.__stdin__ != None) and sys.__stdin__.isatty(): + print("Please log in to ProtonVPN", file=sys.stderr) + print("Username: " + environ["PVPN_USERNAME"], file=sys.stderr) + result = subprocess.run(["protonvpn-cli", "login", environ["PVPN_USERNAME"]]) -with open(PASSFILE, "w") as f: - f.write("{0}\n{1}".format(environ["PVPN_USERNAME"], environ["PVPN_PASSWORD"])) - chmod(PASSFILE, 0o600) + if result.returncode != 0: + exit(result.returncode) + else: + exit(0) + else: + print("Please run this container interactively in order to log in.", file=sys.stderr) + exit(1) -check_init() -set_config_value("USER", "username", environ["PVPN_USERNAME"]) -set_config_value("USER", "tier", environ["PVPN_TIER"]) -set_config_value("USER", "default_protocol", environ["PVPN_PROTOCOL"]) -set_config_value("USER", "initialized", 1) +parser = ArgumentParser() args = sys.argv[1:] if not args: args = shlex.split(environ.get("PVPN_CMD_ARGS") or "") - environ["PVPN_CMD_ARGS"] = "" -parser = ArgumentParser(exit_on_error=False) -subParsers = parser.add_subparsers(dest="command") -initParser = subParsers.add_parser("init", aliases=["i"]) -connectParser = subParsers.add_parser("connect", aliases=["c"]) - -for aliases in [ - ["-f", "--fastest"], - ["-r", "--random"], +for alias in [ ["-s", "--streaming"], ["--sc"], ["--p2p"], ["--tor"] ]: - connectParser.add_argument(*aliases, action="store_true") + parser.add_argument(*alias, action="store_true") -connectParser.add_argument("--cc") -parsedArgs = None +group = parser.add_mutually_exclusive_group() -try: - parsedArgs = parser.parse_args(args) -except: - pass +for alias in [ + ["-f", "--fastest"], + ["-r", "--random"] +]: + group.add_argument(*alias, action="store_true") -if parsedArgs is not None and ( - len( - list( - filter( - lambda item: item[1] not in [False, None], - vars(parsedArgs).items()))) > 1): +parser.add_argument("--cc") +parsedArgs = parser.parse_args() +features: List[FeatureEnum] = list() - def match(server): - features = list() +if parsedArgs.streaming: + features.append(FeatureEnum.STREAMING) +if parsedArgs.sc: + features.append(FeatureEnum.SECURE_CORE) +if parsedArgs.p2p: + features.append(FeatureEnum.P2P) +if parsedArgs.tor: + features.append(FeatureEnum.TOR) - if parsedArgs.streaming: - pass - if parsedArgs.sc: - pass - if parsedArgs.p2p: - pass - if parsedArgs.tor: - pass - - return (parsedArgs.cc is None or server.exit_country.lower() == parsedArgs.cc.lower()) and ( +def match(server: LogicalServer): + return (parsedArgs.cc is None or server.exit_country.lower() == parsedArgs.cc.lower()) and ( all(feature in server.features for feature in features)) - pull_server_data(force=True) - servers = list(filter(lambda server: match(server), get_servers())) - - if len(servers) > 0: - if parsedArgs.fastest or not parsedArgs.random: - server = get_fastest_server(servers) - else: - server = choice(servers) - - run_proton(["connect", server["Name"]]) - else: - raise Exception( - f"Unable to find a server matching the specified criteria {args[1:]}!") -else: - run_proton(args) +matches = cli.protonvpn.get_session().servers.filter(match) +server = matches.get_fastest_server() if parsedArgs.fastest else matches.get_random_server() +subprocess.run(["protonwire", "connect", server.data["Domain"]]) diff --git a/scripts/Common/Software/docker/services/jellyfin/rtorrent-entrypoint.sh b/scripts/Common/Software/docker/services/jellyfin/rtorrent-entrypoint.sh new file mode 100644 index 00000000..c09689a5 --- /dev/null +++ b/scripts/Common/Software/docker/services/jellyfin/rtorrent-entrypoint.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +fallback="$(expr ${NATPMP_TIMEOUT} \* 3 / 4)" +export NATPMP_INTERVAL="${NATPMP_INTERVAL:-$fallback}" + +groupadd --gid $PGID $USERNAME >/dev/null +useradd --create-home --home-dir $PHOME $USERNAME --uid $PUID -g $USERNAME 2>/dev/null +chown $USERNAME:$USERNAME $PHOME +chown $USERNAME:$USERNAME "/etc/rtorrent" +mkdir -p /config/.local/share +chown $USERNAME:$USERNAME /data +chown -R $USERNAME:$USERNAME /data/rtorrent +ln -s /data/rtorrent /config/.local/share/ +chown -R $USERNAME:$USERNAME /config +echo "Opening a port using NAT-PMP for $NATPMP_TIMEOUT seconds…" +output="$(natpmpc -g 10.2.0.1 -a 0 0 tcp "$NATPMP_TIMEOUT")" +natpmpc -g 10.2.0.1 -a 0 0 udp "$NATPMP_TIMEOUT" +port="$(echo "$output" | grep -m 1 " public port [[:digit:]]\+ " | sed "s/.* public port \([[:digit:]]\+\).*/\\1/")" +echo "Port $port has been opened for P2P data transfer!" +echo "The NAT-PMP port forwarding will be updated every $NATPMP_INTERVAL seconds" +export PEERPORT="$port" + +set -m + +{ + while true + do + echo "Refreshing NAT-PMP port forwarding…" + natpmpc -g 10.2.0.1 -a 0 0 udp "$NATPMP_TIMEOUT" + natpmpc -g 10.2.0.1 -a 0 0 tcp "$NATPMP_TIMEOUT" + echo "NAT-PMP port forwarding has been refreshed!" + sleep "$NATPMP_INTERVAL" + done +} & + +cmd="rtorrent -o network.port_range.set=$PEERPORT-$PEERPORT,system.daemon.set=true $@" + +if [ ${MAX_UPTIME:-0} -gt 0 ] +then + sudo -iu $USERNAME $cmd & + pid=$! + sleep "$MAX_UPTIME" + pkill -9 $pid +else + sudo -u $USERNAME $cmd +fi diff --git a/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile b/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile index ea9945c5..53898ce7 100644 --- a/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile +++ b/scripts/Common/Software/docker/services/jellyfin/rtorrent.Dockerfile @@ -1,122 +1,44 @@ -FROM walt3rl/proton-privoxy AS proton -FROM jesec/rtorrent AS rtorrent -FROM debian +FROM ghcr.io/tprasadtp/protonwire AS protonwire +FROM debian AS vpn -ARG PVPN_CLI_VER=2.2.12 -ARG USERNAME=proton +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y \ + curl \ + iproute2 \ + jq \ + natpmpc \ + sudo \ + wireguard \ + && apt-get install -y gnupg2 \ + && curl https://repo.protonvpn.com/debian/dists/stable/main/binary-all/protonvpn-stable-release_1.0.8_all.deb -o protonvpn.deb \ + && dpkg -i ./protonvpn.deb \ + && apt-get remove -y gnupg2 \ + && apt-get update \ + && apt-get install -y protonvpn-cli \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=protonwire /usr/bin/protonwire /usr/bin/ +COPY --chmod=755 proton-entrypoint.sh /usr/local/bin/proton-entrypoint +COPY --chmod=755 pvpn-cli.py /usr/bin/pvpn-cli ENV PVPN_USERNAME= \ - PVPN_USERNAME_FILE= \ - PVPN_PASSWORD= \ - PVPN_PASSWORD_FILE= \ - PVPN_TIER=2 \ - PVPN_PROTOCOL=udp \ - PVPN_TAGS="+pmp" \ - PVPN_CMD_ARGS="connect --p2p --random" \ - PVPN_DEBUG= \ - HOST_NETWORK= \ - DNS_SERVERS_OVERRIDE= \ + PVPN_CMD_ARGS="--p2p --random" \ + USERNAME=proton \ PUID=1000 \ PGID=1000 \ - PHOME=/home/${USERNAME} \ - NATPMP_TIMEOUT=60 \ + PHOME=/home/${USERNAME} + +ENTRYPOINT [ "proton-entrypoint" ] + +FROM vpn AS rtorrent + +ENV NATPMP_TIMEOUT=60 \ NATPMP_INTERVAL= \ MAX_UPTIME= -WORKDIR /root -COPY --from=rtorrent / / - -RUN apt-get update -y \ - && apt-get upgrade -y \ - && apt-get install -y \ - git \ - iproute2 \ - iptables \ - natpmpc \ - openvpn \ - pipenv \ - procps \ - python3 \ - python3-pip \ - python3-setuptools \ - sudo \ - && rm -rf /var/lib/apt/lists - -RUN pip3 install --break-system-packages git+https://github.com/Rafficer/linux-cli-community.git@v$PVPN_CLI_VER#egg=protonvpn-cli - -RUN mkdir /app -COPY --from=proton /app/proton-privoxy/run /app/proton -COPY --from=proton /root/.pvpn-cli/pvpn-cli.cfg.clean /root/.pvpn-cli/pvpn-cli.cfg - -RUN \ - sed -i \ - -e "/^exec privoxy/d" \ - -e "/^ln -s/d" \ - /app/proton \ - && install -t /usr/local/bin /app/proton \ - && rm /app/proton - -RUN printf "%s\n" \ - "python3 /app/pvpn-cli.py \"\$@\"" > ./pvpn-cli \ - && install -Dm 755 ./pvpn-cli /usr/local/bin \ - && rm ./pvpn-cli - -RUN printf "%s\n" \ - "#!/bin/bash" \ - "groupadd --gid \$PGID ${USERNAME} > /dev/null" \ - "useradd --create-home --home-dir \$PHOME ${USERNAME} --uid \$PUID -g ${USERNAME} 2>/dev/null" \ - "chown ${USERNAME} \$PHOME" \ - '[ ! -z "$1" ] && [ "$1" = "init" ] && export PVPN_CMD_ARGS="$@"' \ - 'if [ -z "$PVPN_USERNAME" ] && [ -z "$PVPN_USERNAME_FILE" ]; then' \ - " echo 'Error: Either env var \$PVPN_USERNAME or \$PVPN_USERNAME_FILE is required.'" \ - "exit 1" \ - "fi" \ - "" \ - 'if [ -z "$PVPN_PASSWORD" ] && [ -z "$PVPN_PASSWORD_FILE" ]; then' \ - "echo 'Error: Either env var \$PVPN_PASSWORD or \$PVPN_PASSWORD_FILE is required.'" \ - "exit 1" \ - "fi" \ - "" \ - '[ -f "$PVPN_USERNAME_FILE" ] && PVPN_USERNAME=$(cat "$PVPN_USERNAME_FILE")' \ - '[ -f "$PVPN_PASSWORD_FILE" ] && PVPN_PASSWORD=$(cat "$PVPN_PASSWORD_FILE")' \ - "pvpn-cli || exit" \ - 'ip link show proton0 > /dev/null 2>&1 || exit' \ - 'fallback="$(expr ${NATPMP_TIMEOUT} \* 3 / 4)"' \ - 'export NATPMP_INTERVAL="${NATPMP_INTERVAL:-$fallback}"' \ - 'echo "Opening a port using NAT-PMP for $NATPMP_TIMEOUT seconds…"' \ - 'output="$(natpmpc -a 0 0 tcp "$NATPMP_TIMEOUT")"' \ - 'natpmpc -a 0 0 udp "$NATPMP_TIMEOUT"' \ - 'port="$(echo "$output" | grep -m 1 " public port [[:digit:]]\+ " | sed "s/.* public port \([[:digit:]]\+\).*/\\1/")"' \ - 'echo "Port $port has been opened for P2P data transfer!"' \ - 'echo "The NAT-PMP port forwarding will be updated every $NATPMP_INTERVAL seconds"' \ - 'export PEERPORT="$port"' \ - "{" \ - " while true" \ - " do" \ - ' echo "Refreshing NAT-PMP port forwarding…"' \ - ' natpmpc -a 0 0 udp "$NATPMP_TIMEOUT"' \ - ' natpmpc -a 0 0 tcp "$NATPMP_TIMEOUT"' \ - ' echo "NAT-PMP port forwarding has been refreshed!"' \ - ' sleep "$NATPMP_INTERVAL"' \ - " done" \ - "} &" \ - "set -m" \ - '[ ${MAX_UPTIME:-0} -gt 0 ] && {' \ - ' sudo -iu '"${USERNAME}"' rtorrent -o network.port_range.set=$port-$port,system.daemon.set=true $@ &' \ - ' pid=$!' \ - ' sleep "$MAX_UPTIME"' \ - ' pkill -9 $pid' \ - '} || {' \ - ' sudo -u '"${USERNAME}"' rtorrent -o network.port_range.set=$port-$port,system.daemon.set=true $@' \ - '}' > ./rtorrent-entrypoint \ - && install -Dm 755 ./rtorrent-entrypoint /usr/local/bin \ - && rm ./rtorrent-entrypoint - -COPY pvpn-cli.py /app/pvpn-cli.py - -#RUN apt-get update -y \ -# && apt-get install -y sudo -# RUN echo "${USERNAME} ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers - -VOLUME [ "/proton" ] -ENTRYPOINT [ "rtorrent-entrypoint" ] +RUN mkdir -p /data/rtorrent +COPY --chmod=777 --from=jesec/rtorrent / / +COPY --chmod=755 ./rtorrent-entrypoint.sh /usr/local/bin/rtorrent-entrypoint +ENTRYPOINT [ "proton-entrypoint", "rtorrent-entrypoint" ]