From 3a9259dca5c0fe9beb9e2bca11256ea93d3a306a Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Tue, 26 May 2026 21:26:06 -0400 Subject: [PATCH] feat: upnp with transmission --- machines/prattle/default.nix | 42 +++++++++++++++++++++++++++++++++++ secrets/protonvpn-wg.age | Bin 1035 -> 1015 bytes 2 files changed, 42 insertions(+) diff --git a/machines/prattle/default.nix b/machines/prattle/default.nix index a505ffd..d8e8049 100644 --- a/machines/prattle/default.nix +++ b/machines/prattle/default.nix @@ -332,6 +332,48 @@ ''}" ]; + # ── ProtonVPN NAT-PMP port forwarding ───────────────────────────────── + # Requests a public port from ProtonVPN's gateway every 45s via NAT-PMP + # and pushes it to Transmission inside the VPN namespace. + systemd.services.protonvpn-port-forward = { + description = "NAT-PMP port forwarding through ProtonVPN for Transmission"; + bindsTo = [ "wg.service" "transmission.service" ]; + after = [ "wg.service" "transmission.service" ]; + wantedBy = [ "multi-user.target" ]; + unitConfig.JoinsNamespaceOf = "transmission.service"; + serviceConfig = { + Type = "simple"; + Restart = "on-failure"; + User = "transmission"; + Group = "media"; + ExecStart = pkgs.writeShellScript "protonvpn-port-forward" '' + sleep 5 + GATEWAY=10.2.0.1 + echo "starting NAT-PMP loop, gateway=$GATEWAY" + while true; do + TCP_OUT=$(${pkgs.libnatpmp}/bin/natpmpc -a 1 0 tcp 60 -g "$GATEWAY" 2>&1) || true + ${pkgs.libnatpmp}/bin/natpmpc -a 1 0 udp 60 -g "$GATEWAY" >/dev/null 2>&1 || true + echo "natpmpc: $TCP_OUT" | head -1 + PORT=$(echo "$TCP_OUT" | ${pkgs.gawk}/bin/awk '/Mapped public port/ {print $4}') + if [ -n "$PORT" ] && [ "$PORT" -ne 0 ] 2>/dev/null; then + echo "mapped port $PORT, updating transmission" + SID=$(${pkgs.curl}/bin/curl -s http://localhost:9091/transmission/rpc 2>&1 | ${pkgs.gnused}/bin/sed -n 's/.*X-Transmission-Session-Id: //p') + if [ -n "$SID" ]; then + ${pkgs.curl}/bin/curl -s -X POST \ + "http://localhost:9091/transmission/rpc" \ + -H "X-Transmission-Session-Id: $SID" \ + -H "Content-Type: application/json" \ + --data "{\"method\":\"session-set\",\"arguments\":{\"peer-port\":$PORT}}" \ + >/dev/null 2>&1 || true + echo "transmission port updated to $PORT" + fi + fi + sleep 45 + done + ''; + }; + }; + # ── Media dashboard + reverse proxy ─────────────────────────────────── services.caddy = { enable = true; diff --git a/secrets/protonvpn-wg.age b/secrets/protonvpn-wg.age index 86fe2f268e7ca769b0ec789451f469d4a03cd505..7bdae4c58d779b3aa470aed13c15015a60e23ebb 100644 GIT binary patch delta 989 zcmeC?_|85-qu$vtt0FTpvM9yKUEeu7+%>YGvNXvo*D*aRy`sX{+dtVLJv7`fDBm?b z$S0&Qw9qLqGR#vuGpQuoEwVHvHI1tvr8qP-#682+-7?%FDLpeYAUCNzu`o}&D9zB> zt{Ae#ni7fKP4s6Dc#*WJIlhLq98S}v_9RL%P`T?ptvZ{(KjF@BRImx zt2D*eBsid~I55aOFWAko#5E)&BseI-BrCA2Fu%wn$F<7HuhKKyL_Z{?GR!E|pUchN z+|@NS(6_*|*f-KSDbKXrJ;^=ECCJGz&o98Uq$(mREX~*1*djT=DAhA8Kf}8)**r8f z+^IM+rCvKPxwwkUEIG(M(jvq%CDJI_s4Cbew>-z!Gb_&}$Xwqn-N)R;EZf_|FWsX! zB*GvtF*)6^q_os4Imq9j#G}y2(4*3nt02#^Fw@tp!Z1qQ1;IDyqmJm%ay7E~j!d%?s$baK9_{ zgF0iviLckDyZUZUonr7R@$bq-^JLlVPyO1c%K9yJtyYhZMvOQ?ay8B(0@rxvxjoiJw3?}#PI?_2^>2sta-**}N6+x@s{c(BsyCIMD cu!BmTLA_nh%nwP?rzU;f^z^Ei>rR!O08}b~`v3p{ delta 1009 zcmey)-pw&VqdvscKQP3-q_D!w&myZhxZEqqN!#Dq+&CxPF*P#7z%V#TyFxq1$tNSw zF~}k?+c>Q%+$h-JBge4FEUhRcw}>kwEWpbsFWj=MB+N}e+uO%KRNo*q)!W0|JvYTO z$tW=`J1Re=%E>vku-GWl+blBF(Yq=t+{E0_JvTKxzdkpY%Opa-EHcnHARx#+t0Xfd zJk`z3JtD_B!XVkas>~?8FgLNv)!RTj-y`2B#mL_=JxSZJL_0koFf2UT#lxw-zT!rBiG%)BiGX<+1)2G#XC9NE5j??OuI1EH_Xu6G}AmOr92`` zUq9bGxZXF(E6|(EHN3>sGODyVH@vvW*|^LxJ0ip{&BCJ6AUQHB!pTUxD8D?|HOr}_ z+%vMm)H16e*(o?MSUbHa%0HkWBh1y5%hfn4)iK4#!`;Zk*Vn_W!l}eCL_0sq-6^8N zuiV5hsmwSzJ<8anpd{QNu{g-5)YZ2lB-FXUsNOH5C@eYAvWiPPJ;le@-N(>0I3p|5 z(<~&*#nPhKz|=Lk$kR(Z(XrH1yVSDGx5PBry(+sZ*vuk3tjNW=Jjo=-&C{)-z&)8O zB~`!5$-mUhGc-TMFC^b7Gb+VW-!m)G$)m!v)W^^_Im|6L(akZcGBP!&I3T~Q$~z}2 zr#{HsHP2Vu(kawkpNmUZS63mpAjKlj!nq>PFDp4DyTZ`Ss65TAKtCn4Fvl;+FUiO& zJ0;4b!aKy#+?DI*R-eC#L8_aaRf6QRc>QZs`getIjol^y(j&+q!a7lXsl0 zd@whSsn~JS9VXwbpNA*iQoL%ZQFCX`)bGWnysOw2+BWn4{mESRC}@r1KUV2gPq}Zc zK39FOBh_M~Y>V8!qx0r&KOfhb;^;YhvFBv5yRQExG_AQOcv0-xH;d4D&QggBwhH@P zD_{5e!k&&-PwPeY?y0Ezy5ioGKju<>>yBNvaY(eg^Jag}f{LFF#=YDf*O^LYRfT9h zOcmWB@O|oR?h^-o>&VYc2;?`LbZBklDIb{&OVZ@JRv2>rHNPGa`ejOXp%eRs__H>g zA8%~VbHAY#ktpy(S@m4(&B4nUd%0Vm zr}VvYV>tij?n-kO6CTFTs$0cp+vM2nII2(}a(vIGXDaCQIp?U#BUOg9KUx