Help setting up port-forwarding with Transmission and ProtonVPN on NixOS

The relevant part of my config is this:

services.transmission = {
  enable = true;
  openRPCPort = true;
  settings = {
    download-dir = "/media/Torrents/Complete";
    incomplete-dir = "/media/Torrents/Incomplete";
    port-forwarding-enabled = false;
    rpc-bind-address = "0.0.0.0";
    rpc-whitelist = "127.0.0.*,192.168.*.*";
  };
};

systemd.timers."transmission-port-forwarding" = {
    wantedBy = ["timers.target"];
    timerConfig = {
        OnBootSec = "45s";
        OnUnitActiveSec = "45s";
        Unit = "transmission-port-forwarding.service";
    };
};

systemd.services."transmission-port-forwarding" = {
    serviceConfig = {
        Type = "oneshot";
        User = "root";
    };
    script = ''
        set -eu

        tcp_port_file="$HOME/.local/state/transmission-tcp-port"
        udp_port_file="$HOME/.local/state/transmission-udp-port"

        tcp_result="$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 tcp 60 -g 10.2.0.1)"
        udp_result="$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 udp 60 -g 10.2.0.1)"
        echo "$tcp_result"
        echo "$udp_result"

        new_tcp_port="$(echo "$tcp_result" | ${pkgs.ripgrep}/bin/rg --only-matching --replace '$1' 'Mapped public port (\d+) protocol TCP to local port 0 lifetime 60')"
        old_tcp_port="$(cat "$tcp_port_file")"
        echo "New TCP port $new_tcp_port replacing old TCP port $old_tcp_port."
        echo "$new_tcp_port" >"$tcp_port_file"

        new_udp_port="$(echo "$udp_result" | ${pkgs.ripgrep}/bin/rg --only-matching --replace '$1' 'Mapped public port (\d+) protocol UDP to local port 0 lifetime 60')"
        old_udp_port="$(cat "$udp_port_file")"
        echo "New UDP port $new_udp_port replacing old UDP port $old_udp_port."
        echo "$new_udp_port" >"$udp_port_file"

        if [ "$new_tcp_port" -ne "$new_udp_port" ]
        then
          echo "New TCP port $new_tcp_port does not match new UDP port $new_udp_port, doing nothing as things will almost certainly break."
          exit 3
        elif [ "$old_tcp_port" -ne "$new_tcp_port" -o "$old_udp_port" -ne "$new_udp_port" ]
        then
          echo "Opening new TCP port $new_tcp_port."
          ${pkgs.iptables}/bin/iptables -A INPUT -p tcp --dport "$new_tcp_port" -j ACCEPT
          echo "Opening new UDP port $new-udp-port."
          ${pkgs.iptables}/bin/iptables -A INPUT -p udp --dport "$new_udp_port" -j ACCEPT

          echo "Updating transmission peer-port to $new_tcp_port."
          ${pkgs.transmission}/bin/transmission-remote --port "$new_tcp_port"

          if [ "$old_tcp_port" -ne "$new_tcp_port" -a "$old_tcp_port" -ne '-1' ]
          then
              echo "Closing old TCP port $old_tcp_port."
              ${pkgs.iptables}/bin/iptables -D INPUT -p tcp --dport "$old_tcp_port" -j ACCEPT
          else
              echo "Old TCP port is $old_tcp_port, not closing."
          fi

          if [ "$old_udp_port" -ne "$new_udp_port" -a "$old_udp_port" -ne '-1' ]
          then
              echo "Closing old UDP port $old_udp_port."
              ${pkgs.iptables}/bin/iptables -D INPUT -p udp --dport "$old_udp_port" -j ACCEPT
          else
              echo "Old UDP port is $old_udp_port, not closing."
          fi
        else
          echo 'Old and new ports are identical, doing nothing.'
        fi
    '';
};

(apologies for how bad that script is, I was just trying to get it to work first…)

I set up ProtonVPN using nmcli (since I couldn’t get the CLI client to work and I’m running this on a headless RPi), using the UK#77 server (which has P2P enabled) and appending +pmp to the OpenVPN username (as specified here):

$ nmcli connection show | rg uk-77
uk-77.protonvpn.udp  b70e0f1e-b089-459f-b89e-95ad43424e1e  vpn       wlan0
$ nmcli connection show uk-77.protonvpn.udp | rg user-name
vpn.user-name:                          ????????????????+pmp

From the perspective of my machine, everything seems to be working fine. I can connect to the transmission web UI from my laptop and it has the right port number, the IP of the RPi matches one of Proton’s IPs and not my home IP, the port-forwarding script seems to work fine

$ journalctl -u transmission-port-forwarding
Jan 18 20:19:17 vpn systemd[1]: Starting transmission-port-forwarding.service...
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: initnatpmp() returned 0 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: using gateway : 10.2.0.1
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: sendpublicaddressrequest returned 2 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: readnatpmpresponseorretry returned 0 (OK)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: Public IP address : 146.70.133.136
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: epoch = 4434776
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: sendnewportmappingrequest returned 12 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: readnatpmpresponseorretry returned 0 (OK)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: Mapped public port 50131 protocol TCP to local port 0 lifetime 60
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: epoch = 4434776
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: closenatpmp() returned 0 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: initnatpmp() returned 0 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: using gateway : 10.2.0.1
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: sendpublicaddressrequest returned 2 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: readnatpmpresponseorretry returned 0 (OK)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: Public IP address : 146.70.133.136
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: epoch = 4434776
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: sendnewportmappingrequest returned 12 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: readnatpmpresponseorretry returned 0 (OK)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: Mapped public port 50131 protocol UDP to local port 0 lifetime 60
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: epoch = 4434776
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: closenatpmp() returned 0 (SUCCESS)
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: New TCP port 50131 replacing old TCP port 50131.
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: New UDP port 50131 replacing old UDP port 50131.
Jan 18 20:19:18 vpn transmission-port-forwarding-start[39651]: Old and new ports are identical, doing nothing.
Jan 18 20:19:18 vpn systemd[1]: transmission-port-forwarding.service: Deactivated successfully.
Jan 18 20:19:18 vpn systemd[1]: Finished transmission-port-forwarding.service.
Jan 18 20:19:18 vpn systemd[1]: transmission-port-forwarding.service: Consumed 45ms CPU time, received 168B IP traffic, sent 140B IP traffic.

and the port is open in my iptables output:

$ sudo iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination
nixos-fw   all  --  anywhere             anywhere
ACCEPT     udp  --  anywhere             anywhere             udp dpt:50131
ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:50131

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain nixos-drop (0 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere

Chain nixos-fw (1 references)
target     prot opt source               destination
nixos-fw-accept  all  --  anywhere             anywhere
nixos-fw-accept  all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
nixos-fw-accept  tcp  --  anywhere             anywhere             tcp dpt:ssh
nixos-fw-accept  tcp  --  anywhere             anywhere             tcp dpt:xmltec-xmlmail
nixos-fw-accept  icmp --  anywhere             anywhere             icmp echo-request
nixos-fw-log-refuse  all  --  anywhere             anywhere

Chain nixos-fw-accept (5 references)
target     prot opt source               destination
ACCEPT     all  --  anywhere             anywhere

Chain nixos-fw-log-refuse (1 references)
target     prot opt source               destination
LOG        tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,ACK/SYN LOG level info prefix "refused connection: "
nixos-fw-refuse  all  --  anywhere             anywhere             PKTTYPE != unicast
nixos-fw-refuse  all  --  anywhere             anywhere

Chain nixos-fw-refuse (2 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere

However the Transmission Web UI says that the port is closed, and https://canyouseeme.org reports that the port is closed as well (although I’m checking that using an SSH tunnel, so maybe that’s causing issues?).

Any ideas what is causing this? My current thinking is that I’m not handling iptables correctly, but I have no idea in what way…

Since the error you’re getting is that the port is closed this might related, for Discord I had to specifically open a big range of ports or voice wouldnt work, this did it for me:

  networking.firewall.allowedUDPPortRanges = [
    {
        from = 50000;
        to = 65535;
    }
  ];

Maybe your firewall still needs some tweaking?

Hope it helps

Thanks for the suggestion, but that doesn’t work either :frowning:

By current best attempt is this script

set -u

renew_port() {
  protocol="$1"
  port_file="$HOME/.local/state/transmission-$protocol-port"

  result="$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 "$protocol" 60 -g 10.2.0.1)"
  echo "$result"

  new_port="$(echo "$result" | ${pkgs.ripgrep}/bin/rg --only-matching --replace '$1' 'Mapped public port (\d+) protocol ... to local port 0 lifetime 60')"
  old_port="$(cat "$port_file")"
  echo "Mapped new $protocol port $new_port, old one was $old_port."
  echo "$new_port" >"$port_file"

  echo "Mapping new $protocol port $new_port to itself."
  ${pkgs.libnatpmp}/bin/natpmpc -a "$new_port" "$new_port" "$protocol" 60 -g 10.2.0.1

  if ${pkgs.iptables}/bin/iptables -C nixos-fw -p "$protocol" --dport "$new_port" -j nixos-fw-accept
  then
    echo "New $protocol port $new_port already open, not opening again."
  else
    echo "Opening new $protocol port $new_port."
    ${pkgs.iptables}/bin/iptables -A nixos-fw -p "$protocol" --dport "$new_port" -j nixos-fw-accept
  fi

  if [ "$protocol" = tcp ]
  then
    echo "Telling transmission to listen on peer port $new_port."
    ${pkgs.transmission}/bin/transmission-remote --port "$new_port"
  fi

  if [ "$new_port" -eq "$old_port" ]
  then
    echo "New $protocol port $new_port is the same as old port $old_port, not closing old port."
  else
    if ${pkgs.iptables}/bin/iptables -C nixos-fw -p "$protocol" --dport "$old_port" -j nixos-fw-accept
    then
      echo "Closing old $protocol port $old_port."
      ${pkgs.iptables}/bin/iptables -D nixos-fw -p "$protocol" --dport "$old_port" -j nixos-fw-accept
    else
      echo "Old $protocol port $old_port not open, not attempting to close."
    fi
  fi
}

renew_port tcp
renew_port udp

in case anyone has any other ideas on what to try, but I’m going to give up on it for now.

Ok so it definitely is a problem on my end, as when I try using https://canyouseeme.org journalctl shows:

Jan 23 19:35:49 vpn kernel: refused connection: IN=starr-vpn OUT= MAC= SRC=52.202.215.126 DST=10.2.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=48700 DF PROTO=TCP SPT=59179 DPT=42546 WINDOW=26883 RES=0x00 SYN URGP=0
Jan 23 19:35:50 vpn kernel: refused connection: IN=starr-vpn OUT= MAC= SRC=52.202.215.126 DST=10.2.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=48701 DF PROTO=TCP SPT=59179 DPT=42546 WINDOW=26883 RES=0x00 SYN URGP=0
Jan 23 19:35:52 vpn kernel: refused connection: IN=starr-vpn OUT= MAC= SRC=52.202.215.126 DST=10.2.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=50 ID=48702 DF PROTO=TCP SPT=59179 DPT=42546 WINDOW=26883 RES=0x00 SYN URGP=0

which means that at the very least the connection is reaching my box. So presumably there’s still something I’m missing vis a vis the firewall.

Ok, I managed to get https://canyouseeme.org to see the port as open by inserting (iptables -I) instead of appending (iptables -A) :smiley: but unfortunately transmission still doesn’t recognise the port as open :sob:

Ok, might be something to do with Transmission Port Closed [Possible Solution SOLVED] - Transmission will try disabling IPv6 tomorrow.

Yes, it was the IPv6!

If anyone runs into the same issue, my final working setup is:

# Transmission peer ports don't work when IPv6 is enabled.
networking.enableIPv6 = false;

services.transmission = {
  enable = true;
  openRPCPort = true;
  settings = {
    port-forwarding-enabled = false;
    rpc-bind-address = "0.0.0.0";
    rpc-whitelist = "127.0.0.*,192.168.*.*";
  };
};

systemd.timers."transmission-port-forwarding" = {
  wantedBy = ["timers.target"];
  timerConfig = {
    OnBootSec = "45s";
    OnUnitActiveSec = "45s";
    Unit = "transmission-port-forwarding.service";
  };
};

systemd.services."transmission-port-forwarding" = {
  serviceConfig = {
    Type = "oneshot";
    User = "root";
  };
  script = ''
    set -u

    renew_port() {
      protocol="$1"
      port_file="$HOME/.local/state/transmission-$protocol-port"

      result="$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 "$protocol" 60 -g 10.2.0.1)"
      echo "$result"

      new_port="$(echo "$result" | ${pkgs.ripgrep}/bin/rg --only-matching --replace '$1' 'Mapped public port (\d+) protocol ... to local port 0 lifetime 60')"
      old_port="$(cat "$port_file")"
      echo "Mapped new $protocol port $new_port, old one was $old_port."
      echo "$new_port" >"$port_file"

      if ${pkgs.iptables}/bin/iptables -C INPUT -p "$protocol" --dport "$new_port" -j ACCEPT
      then
        echo "New $protocol port $new_port already open, not opening again."
      else
        echo "Opening new $protocol port $new_port."
        ${pkgs.iptables}/bin/iptables -I INPUT -p "$protocol" --dport "$new_port" -j ACCEPT
      fi

      if [ "$protocol" = tcp ]
      then
        echo "Telling transmission to listen on peer port $new_port."
        ${pkgs.transmission}/bin/transmission-remote --port "$new_port"
      fi

      if [ "$new_port" -eq "$old_port" ]
      then
        echo "New $protocol port $new_port is the same as old port $old_port, not closing old port."
      else
        if ${pkgs.iptables}/bin/iptables -C INPUT -p "$protocol" --dport "$old_port" -j ACCEPT
        then
          echo "Closing old $protocol port $old_port."
          ${pkgs.iptables}/bin/iptables -D INPUT -p "$protocol" --dport "$old_port" -j ACCEPT
        else
          echo "Old $protocol port $old_port not open, not attempting to close."
        fi
      fi
    }

    renew_port udp
    renew_port tcp
  '';
};
1 Like

Turns out that worked for precisely one (1) evening :sob:

Right, final message into this thread. Turns out that to get it to work again you need to enable IPv6, rebuild, then disable IPv6, and rebuild again. This feels very anti-nix…