tprasadtp / protonvpn-docker

ProtonVPN Wireguard Docker Image. Supports ARMv8 (64-bit ) and x86 (64-Bit).
GNU General Public License v3.0
299 stars 25 forks source link

[RFE] - Port Forwarding #125

Open triesmon opened 2 years ago

triesmon commented 2 years ago

Version of protonvpn-docker

NA

Details about Feature/Enhancement

Having the ability to connect to a port forwarding server and maybe providing an endpoint or something in the logs that prints the currently enabled forwarded port would be useful.

Here's a reference to the feature in the desktop client: image

Code of Conduct & PII Redaction

gpascualg commented 2 years ago

Last time I checked, port forwarding was only available in the Windows Desktop version. Unless it has recently changed, it is not be possible to enable it in this service.

source: https://protonvpn.com/blog/port-forwarding/

adriy-be commented 1 year ago

There are two features request for that :

Ludofloria commented 1 year ago

Last time I checked, port forwarding was only available in the Windows Desktop version. Unless it has recently changed, it is not be possible to enable it in this service.

source: https://protonvpn.com/blog/port-forwarding/

Port forwarding works under Linux, even with an OpenVPN client ;)

First of all, append +pmp to your OpenVPN username.

Once connected to ProtonVPN networks, we should issue multiple commands:

natpmpc alone, which confirms (or not) the availability of Port Forwarding

If it's available, then we must do:

  1. natpmpc -a 0 0 udp 60
  2. natpmpc -a 0 0 tcp 60
  3. while true ; do date ; natpmpc -a 0 0 udp 60 && natpmpc -a 0 0 tcp 60 || { echo -e "ERROR with natpmpc command \a" ; break ; } ; sleep 45 ; done

Source: https://protonvpn.com/support/port-forwarding-manual-setup/

tsjk commented 1 year ago

I used 7.2.3-debug-alexis for my experiment here - as it uses Debian. Conveniently it was just created. Anyway, the following kind of works - given some extra tools such as natpmpc and moreutils.

docker-compose.yml:

version: '2.3'

services:
  protonwire:
    build: .
    container_name: protonwire
    image: localhost:5000/protonwire:latest
    init: true
    restart: never
    network_mode: bridge
    environment:
      DEBUG: "0"
      KILL_SWITCH: "0"
      PROTONVPN_SERVER: "xxx#N"
      TZ: "Etc/UTC"
    cap_add:
      - NET_ADMIN
    sysctls:
      - net.ipv4.conf.all.rp_filter=2
      - net.ipv6.conf.all.disable_ipv6=1
    healthcheck:
      test: ["CMD", "/bin/bash", "-c", "/usr/bin/protonwire check --container --silent || exit 1"]
      interval: 120s
      start_period: 20s
    volumes:
      - type: tmpfs
        target: /tmp
      - type: bind
        source: ./private.key
        target: /etc/protonwire/private-key
        read_only: true

  protonwire-natpmpc:
    container_name: protonwire-natpmpc
    image: localhost:5000/protonwire:latest
    restart: never
    depends_on:
      protonwire:
        condition: service_healthy
    network_mode: service:protonwire
    environment:
      TZ: "Etc/UTC"
    volumes:
      - type: bind
        source: /tmp/protonwire-natpmpc-log
        target: /log
    entrypoint:
      - "/bin/bash"
      - "-c"
    command: >
      "while true; do
           { date '+%Y-%m-%d %H:%M:%S'; natpmpc -a 1 0 udp 60 -g 10.2.0.1 && natpmpc -a 1 0 tcp 60 -g 10.2.0.1 || break; } 2>&1 | ts '[%Y-%m-%dT%H:%M:%S] |' >> '/log/protonwire-natpmpc.log'; sleep 45 &
             wait $!; [[ $(wc -l '/log/protonwire-natpmpc.log' | awk -F ' ' '{ print $1 }') -lt $((128 * 1024)) ]] || sed 1,$(( (128 * 1024) - (96 * 1024) ))d '/log/protonwire-natpmpc.log' | sponge '/log/protonwire-natpmpc.log';
       done"
    healthcheck:
      test: ["CMD-SHELL", "[ $$(( $$(date '+%s') - $$(stat -c '%Y' '/log/protonwire-natpmpc.log') )) -lt 120 ] && /usr/bin/tail -n 23 '/log/protonwire-natpmpc.log' | grep -q -E 'Public IP address : [0-9]{1,3}(\\.[0-9]{1,3}){3}\\s*$$' && /usr/bin/tail -n 23 '/log/protonwire-natpmpc.log' | grep -q -E 'Mapped public port [1-9][0-9]* protocol UDP to local port 0 liftime 60\\s*$$' && /usr/bin/tail -n 23 '/log/protonwire-natpmpc.log' | grep -q -E 'Mapped public port [1-9][0-9]* protocol TCP to local port 0 liftime 60\\s*$$"]
      interval: 120s
      start_period: 30s

It seems to create a mapping. I tried to connect to it by throwing up a socat web server (given socat), like in a shell in the container

# socat \
    -v -d -d \
    TCP-LISTEN:<PORT_FROM_LOGS>,crlf,reuseaddr,fork \
    SYSTEM:"
        echo HTTP/1.1 200 OK;
        echo Content-Type\: text/plain;
        echo;
        echo \"Server: \$SOCAT_SOCKADDR:\$SOCAT_SOCKPORT\";
        echo \"Client: \$SOCAT_PEERADDR:\$SOCAT_PEERPORT\";
    "

Seems to also work.

As does like (I actually use podman and podman-compose in the above):

$ podman run --rm --network container:protonwire -it ghcr.io/static-web-server/static-web-server:2 -p <PORT_FROM_LOGS> -g info
tsjk commented 1 year ago

I guess one can use any container as a natpmpc-helper. I just did what was easiest. Then one can maybe publish this ip and port via something easily accessible - or just share the logs with another container and parse them there.

tsjk commented 1 year ago

The end result was this: https://github.com/tprasadtp/protonvpn-docker/compare/master...tsjk:protonvpn-docker:master

I'm using it for a service I run, and it seems ok.

tprasadtp commented 1 year ago

@tsjk 7.3.0-alpha1 has all the tools you need to run it (though without the custom scripts from your fork). Though adding those requires bit more work would you be open to add helpers as a sub-commands to protonwire?

tsjk commented 1 year ago

Sure. Let me work on it some more. The other day I realized that I really don't want this container to die and vanish (for whatever reason - I observed some fatal indexing error when metadata fails to refresh) as that will disable ALL networking in dependent containers. If the dependent containers have this and Tor via a socks port, for instance - the vpn temporarily going down is not a big problem. I'll tend to this issue first and then look at sub-commanding. I imagine looping the protonwire script with a signal handler.

le-martre commented 1 year ago

Hello guys ! I managed to make port forwarding work starting from the "caddy proxy" docker compose example in the readme, using 7.3.0-alpha1 as you suggested. I did it in an ugly way because I suck at bash but it'll probably help some people anyways (and it's thus pretty simple to understand).

Here is the important part of my docker compose :

services:
  protonvpn:
    container_name: protonvpn
    image: ghcr.io/tprasadtp/protonwire:7.3.0-alpha1
    command: "sh /config/protonvpn-init.sh"
    environment:
      - WIREGUARD_PRIVATE_KEY=YOURKEY
      - PROTONVPN_SERVER=YOURNODEURL
    cap_add:
      - NET_ADMIN
    sysctls:
      net.ipv4.conf.all.rp_filter: 2
      net.ipv6.conf.all.disable_ipv6: 1
    volumes:
      - type: tmpfs
        target: /tmp
      - /local_path_to/protonvpn-port:/config/protonvpn-port
      - /local_path_to/protonvpn-init.sh:/config/protonvpn-init.sh
    ports:
      - "yourport:yourport"
  yourservice:
    image: yourservice
    container_name: yourservice
    network_mode: service:protonvpn
    volumes:
      - /local_path_to/protonvpn-port:/config/protonvpn-port # Do whatever you want with this
    restart: always

Here is the entry script for protonvpn :

/usr/bin/protonwire connect --container &

sleep 10

natpmpc -a 1 0 udp 60 -g 10.2.0.1
natpmpc -a 1 0 tcp 60 -g 10.2.0.1 | grep -oP 'public\ port\ \K\w+' > /config/protonvpn-port 

echo "Port written to protonvpn-port file"
cat /config/protonvpn-port

while true ; do date ; natpmpc -a 1 0 udp 60 -g 10.2.0.1 && natpmpc -a 1 0 tcp 60 -g 10.2.0.1 || { echo -e "ERROR with natpmpc command \a" ; break ; } ; sleep 45 ; done

Nothing crazy here, I start protonwire in the background, put in an arbitrary sleep because I didn't know how else I could wait for protonwire to connect successfully (there is probably a smart way to do this), and then just execute the natpmpc commands exactly like in the protonvpn documentation and extract the port number to a file on the host system via regexp.

After that I just retrieve the content of the protonvpn-port file in my other container and update my application with it.

Of course, if any error happens, everything goes to hell, it's a quick script for non critical applications, don't use it for anything important !

ianhundere commented 1 year ago

Here is the entry script for protonvpn :

/usr/bin/protonwire connect --container &

sleep 10

natpmpc -a 1 0 udp 60 -g 10.2.0.1
natpmpc -a 1 0 tcp 60 -g 10.2.0.1 | grep -oP 'public\ port\ \K\w+' > /config/protonvpn-port 

echo "Port written to protonvpn-port file"
cat /config/protonvpn-port

while true ; do date ; natpmpc -a 1 0 udp 60 -g 10.2.0.1 && natpmpc -a 1 0 tcp 60 -g 10.2.0.1 || { echo -e "ERROR with natpmpc command \a" ; break ; } ; sleep 45 ; done

i'm using @le-martre's solution for a k8s deployment:

command: ["/bin/bash", "-c"]
args:
  [
    "/usr/bin/protonwire connect --container & sleep 10; natpmpc -a 1 0 udp 60 -g 10.2.0.1; natpmpc -a 1 0 tcp 60 -g 10.2.0.1 | grep -oP 'public\\ port\\ \\K\\w+' > /config/protonvpn-port; echo \"Port written to protonvpn-port file\"; cat /config/protonvpn-port; while true; do date; natpmpc -a 1 0 udp 60 -g 10.2.0.1 && natpmpc -a 1 0 tcp 60 -g 10.2.0.1 || { echo -e \"ERROR with natpmpc command \\a\"; break; }; sleep 45; done",
  ]
ivan-pinatti commented 3 months ago

Hello everyone,

I've made a few improvements to the scripts shared in this thread.

I am using on my Docker Compose as a multi-line command for now, perhaps if it gets bigger I will transform in a script with its own file.

To pass the port value to a secondary container, you can write to a shared file and use the healthcheck to control the initialization of the secondary container. In Docker Compose multi-line will be something like this;

services:
  protonvpn:
    <YOUR_OTHER_CONTAINER_CONFIGS>
    volumes:
      - ./natpmp.env:/tmp/natpmp.env:z
    healthcheck:
      test: protonwire healthcheck --silent --service-status-file || exit 1
      interval: 60s
      retries: 3
      start_interval: 45s
      timeout: 3s
    entrypoint: ["/bin/sh","-c"]
    command:
      - |
        /usr/bin/protonwire connect --service --p2p --kill-switch --check-interval 60 &

        __check_connection () {
          protonwire healthcheck --silent --service-status-file
        }

        __config_and_renew_port_forward () {
          while true; do
            natpmpc -a 1 0 udp 60 -g 10.2.0.1 > /dev/null || { 
              echo "[ERROR   ][NATPMP] Command natpmpc has failed! Retrying in 3 seconds...";
              sleep 3;
              break;
            }; 

            natpmpc -a 1 0 tcp 60 -g 10.2.0.1 \
              | grep -oP 'public\ port\ \K\w+' > /tmp/protonvpn-port-forward.txt;
            echo "NATPMP_PORT=$(cat /tmp/protonvpn-port-forward.txt)" > /tmp/natpmp.env

            echo "[SUCCESS ][NATPMP] Port forward successful! Will renew in 45 seconds...";
            echo "[INFO    ][NATPMP] Port forward enabled at port: $(cat /tmp/protonvpn-port-forward.txt)";
            sleep 45;
          done
        }

        until __check_connection; do
          echo "[ERROR   ][NATPMP] VPN connection NOT established! Retrying in 10 seconds..."
          sleep 10
        done
        echo "[SUCCESS ][NATPMP] VPN connection established! Configuring it now..."

        __config_and_renew_port_forward

  other_container:
    image: other_container
    container_name: other_container
    depends_on:
      protonvpn:
        condition: service_healthy
        restart: true
    env_file: ["./natpmp.env"]

Some details about the improvements.

First of all, the option --container is deprecated as it is pointed here, --service is the new one.

Second, instead of using the arbitrary sleep to check the connection, there is a command that we could utilize; protonwire healthcheck --silent --service-status-file

Lastly, the logs output is cleaner and it can be easily filtered directly with the docker logs command OR if you are ingesting this logs into any observability platform like ELK, New Relic, etc... The command docker logs -f protonvpn | grep NATPMP would produce this;

image

Finally, there are some corner cases that could still be improved. For example, when dealing with error control of the natpmpc commands, and specially if the port changes and requires a restart of the secondary container. The latter could be solved if there is a way to instead of the ProtonVPN reconnecting when the connection drops there was a way to make the container restart, hence restarting the secondary container also. Maybe I will work on it in the future to address these.