Contenerizando a Jellyfin en un Jail

El tema central será Jellyfin, pero se necesita tocar varios puntos primero.

A tomar en cuenta.
  • Luego de un tiempo las cosas se hicieron más faciles que cuando se creo este post.

  • Ya existe en los FreshPorts Jellyfin y un bastille tamplate que funciona bien, menos pasos que en este post, en mi caso tuve que remover esta linea CONFIG set ip6=inherit; para hacer la instalación completa.

  • Makejail de jellyfin creado con Appjail , gracias a @DtxdF

La idea de usar miniDLNA se queda muy muy corta
  1. La interface que nos ofrece muy poco intuitiva.

  2. Cada rescan reiniciaba las películas en plena reproducción. 🤢

  3. El protocolo DLNA no es compatible con todos los Smart-TV. 🤮

Resulta que freeBSD tiene algo muy potente, siendo nativo de el llamado jails (prisiones, cárcel) para contenerizar nuestras aplicaciones, muy parecido a docker, pero más maduro, dando un método de contenerización lijera, a veces llamado virtualización a nivel de OS(Sistema operativo), un jail normalmente contiene un sistema operativo completo en una zona de usuario que corre sobre el Sistema FreeBSD.

Los jails, no tienen su propio kernel, sino, que corren en una porción del kernel del host, el host puede controlar el jail, por medio de ciertos comandos sin entrar en el jail, o correr procesos dentro del jail si se prefiere.

La cuenta root del jail puede controlar todo dentro del jail, pero no fuera de el, cada uno de ellos posee una dirección ip dedicada.

Procesos del Jail o JID

xigmanas: ~# jls (1)
   JID  IP Address      Hostname                      Path
     1                  adguardhome                   /mnt/pool/extensions/bastille/jails/adguardhome/root
     2  192.168.1.248   jdownloader                   /mnt/pool/extensions/bastille/jails/jdownloader/root
     4                  nextcloud                     /mnt/pool/extensions/bastille/jails/nextcloud/root
     5                  reverse-proxy                 /mnt/pool/extensions/bastille/jails/reverse-proxy/root
     6                  jellyfin                      /mnt/pool/extensions/bastille/jails/jellyfin/root
1 Con el comando jls visualizamos nuestros jails, el JID es como un process ID, cada JID cambia una vez que el jail inicie.

Usando Bastille

35433989?s=200&v=4

Bastille para administrar nuestros jails con simples comandos, este lo instale desde la interface del xigmanas, no me toco probar desde la consola, pero si fue necesario tener primero a OBI(one button installer). para instalarlo desde ese plugin.

ObiConBastille

Subcommands

La lista de comandos de bastille es un poco variada subcommands.

Podemos instalar todo nuestros jails via web o por consola

todosLosJails

Jails.conf, fstab

Cada Jail tiene 2 ficheros importantes, por ejemplo:

  • jail.conf

  • fstab

Más info al respecto jail-config

Las rutas de ambos ficheros, estan el dataset donde esta bastille, en mi caso llamado pool

/mnt/pool/extensions/bastille/jails/jdownloader/jail.conf
/mnt/pool/extensions/bastille/jails/jdownloader/fstab

Si queremos entrar dentro de un jail usariamos el siguiente comando.

bastille console Jdownloader (1)
1 comando que permite entrar al jail llamado Jdownloader

Dirección ip del jail

Si queremos asignar una dirección ip a nuestro jail bien sea estatica o dinámica, nos vamos al etc/rc.conf

Donde tenemos la ruta siguiente de dicho fichero

/mnt/pool/extensions/bastille/jails/jellyfin-clone/root/etc/rc.conf
Una de las dos líneas se usara, estática o dinámica.
syslogd_flags="-ss"
sendmail_enable="NO"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"
cron_flags="-J 60"
ifconfig_e0b_bastille4_name="vnet0"
ifconfig_vnet0="inet 192.168.1.247/24" (1)
#ifconfig_vnet0="SYNCDHCP" (2)
defaultrouter="192.168.1.1"
jellyfinserver_enable="TRUE"
jellyfinserver_user="jellyfin"
jellyfinserver_group="jellyfin"
1 Se asigna a este jail una dirección ip estática dentro del pool DHCP de mi red local, con subnet /24.
2 Se usa SYNCDHCP y esto asignará una dirección ip dinámica, justo como lo comenta aquí el código fuente

El código del enlace anterior.

# If 0.0.0.0 set DHCP, else set static IP address
if [ "${IP}" == "0.0.0.0" ]; then
    sysrc -f "${bastille_jail_rc_conf}" ifconfig_vnet0="SYNCDHCP"
else
    sysrc -f "${bastille_jail_rc_conf}" ifconfig_vnet0="inet ${IP}"
fi

Bug al clonar jail con bastille

El bug no es nada complejo de resolver, solo que se debe fixear la vnet correctamente, solo cambiar y ya, porque cuando clona, copia la interface del jail original sin escribir la nueva interface del jail clonado entonces:

En nuestro jails.conf

jellyfin-clone {
  devfs_ruleset = 13;
  enforce_statfs = 2;
  exec.clean;
  exec.consolelog = /mnt/pool/extensions/bastille/logs/jellyfin-clone_console.log;
  exec.start = '/bin/sh /etc/rc';
  exec.stop = '/bin/sh /etc/rc.shutdown';
  host.hostname = jellyfin-clone;
  mount.devfs;
  mount.fstab = /mnt/pool/extensions/bastille/jails/jellyfin-clone/fstab;
  path = /mnt/pool/extensions/bastille/jails/jellyfin-clone/root;
  securelevel = 2;

  vnet;
  vnet.interface = e0b_bastille5; (1)
  exec.prestart += "jib addm bastille5 igb0"; (2)
  exec.prestart += "ifconfig e0a_bastille5 description \"vnet host interface for Bastille jail jellyfin-clone\""; (3)
  exec.poststop += "jib destroy bastille5"; (4)
  allow.mlock;
  allow.raw_sockets;
}
1 Nuestra inteface es e0b_bastille5, y debe estar también en las siguentes líneas.
2 Esta línea la cambie porque antes estaba la interface bastille1 del jail original
3 Se usa bastille5
4 Se usa bastille5 también.

Jail no arranca?

Al clonar, por lo visto el fstab toca actualizar los nombres correctos al nuevo nombre del jail.
xigmanas: ~# bastille start jellyfin-clone (1)
[jellyfin-clone]:
(2)
mount_nullfs: /mnt/pool/extensions/bastille/jails/jellyfin/root/mnt/series: Resource deadlock avoided
jail: jellyfin-clone: /sbin/mount -t nullfs -o rw /mnt/pool_2/series /mnt/pool/extensions/bastille/jails/jellyfin/root/mnt/series: failed

xigmanas: ~# bastille start jellyfin-clone (3)
[jellyfin-clone]:
e0a_bastille5
e0b_bastille5
jellyfin-clone: created

xigmanas: ~# bastille console jellyfin-clone
[jellyfin-clone]:
root@jellyfin-clone:~ # ping google.com
PING google.com (142.250.201.78): 56 data bytes
64 bytes from 142.250.201.78: icmp_seq=0 ttl=116 time=5.877 ms
64 bytes from 142.250.201.78: icmp_seq=1 ttl=116 time=8.838 ms
64 bytes from 142.250.201.78: icmp_seq=2 ttl=116 time=8.711 ms
^C
--- google.com ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 5.877/7.809/8.838/1.367 ms
1 Iniciamos nuestro jail y vemos que en la consola arroja un error justo en el fstab.
2 El jail clonado tiene un nombre incorrecto apuntando al original jellyfin, entonces lo cambiamos al nuevo nombre que es jellyfin-clone y listo
3 Luego de correjir el fstab todo bien.

Problemas al borrar/eliminar un Jail 🔨

Muchas veces por alguna razón y los permisos del usuario root impiden borrar el directorio donde el jail se ha instalado, bien sea por la flag schg que esta presente en dicho directorio, o fichero, haciendolos inmutables (anti borrable entre comillas), para eso hay un comando útil por ejemplo:

chflags -R 0 borrarDirectorio (1)
rm -rf borrarDirectorio (2)
1 Esto remueve la flag schg y con -R de manera recursiva dentro del dir.
2 Luego podemos borrar el dir con facilidad.

Con los pasos anteriores bastaría para borrar el directorio y solucionar el problema.

Pero tenemos otro escenario, como estamos trabajando en un sistema de ficheros zfs, se incluyo en el código fuente de bastille el parametro -f para usarlo junto con destroy

servernas: jails# rm -rf mongodb
rm: mongodb/root: Device busy (1)
rm: mongodb: Device busy
1 No se puede borrar, según esta ocupada la unidad
servernas: /# cd /mnt/pool/extensions/bastille/jails
servernas: jails# ls
adguardhome   apps-java     jdownloader   jellyfin      mongodb       reverse-proxy
servernas: jails# bastille destroy -f mongodb (1)
Deleting Jail: mongodb.

servernas: jails# ls (2)
adguardhome   apps-java     jdownloader   jellyfin      reverse-proxy
1 Esto nos permite también poder borrar el jail anti-borrable 🤣.
2 Ya no existe el jail dentro del dataset que era mongodb, porque lo borramos.
Información al respecto.

Instalando Jdownloader

jdownloader

Desde hace muchos años, uso Jdownloader y de verdad que siempre me vino bien para reconexión automática, reanudar/continuar las descargas, descompresión automática, conectarme a otros servidores como mega etc…​

Instalar jdk

Viene bien la openjdk11

xigmanas: ~# bastille console jdownloader (1)
root@jdownloader:~ # pkg install openjdk11 (2)
1 Entramos al jail jdownloader
2 Instalamos la open jdk.
root@jdownloader:~ # java -version (1)
openjdk version "11.0.12" 2021-07-20
OpenJDK Runtime Environment (build 11.0.12+7-1)
OpenJDK 64-Bit Server VM (build 11.0.12+7-1, mixed mode)
1 Mostramos la versión de java actual.

Descargar megatools e iniciar Jdownloader

Megatools y url del .jar de Jdownloader
  1. Vamos a instalar megatools, para hacer descargas desde mega.nz

  2. Buscamos la versión other de aqui para el .jar

root@jdownloader:~ # pkg install megatools (1)
root@jdownloader:~/java # megadl https://mega.nz/file/2EkgUSga#Pjau9db2bBDES-ih4iWYlHfwC0t-444eFfm0SQegqRA (2)
Downloaded JDownloader.jar
root@jdownloader:~/java # ls -la
total 4134
drwxr-xr-x   2 root  wheel        3 Jul  4 16:32 .
drwxr-x---  19 root  wheel       37 Jul  4 16:32 ..
-rw-r--r--   1 root  wheel  4411735 Jul  4 16:32 JDownloader.jar
1 Instalamos megatools
2 Comando megadl para descargar desde mega
java -jar JDownloader.jar -norestart (1)
1 Este comando permite iniciar Jdownloader pero sin interface grafica, es decir modo consola, y muchas veces, requiere ejecutarlo nuevamente si nos sale la imagen de abajo.

jdownloaderRestart

Al final para conectarnos a el, lo haríamos mediante MyJDownloader este existe en versión app para Android, IOs no lo se, y un plugin para Google Chrome MyJDownloader Browser Extension

Jdownloader con autoboot

Este proceso de autoboot, podemos hacerlo con otra aplicación, asi como hace git automáticamente que también crea su propio daemon, con adguardhome aplique el mismo procedimiento.

El paso anterior con el parametro -norestart, no merece la pena por, dado que a veces, el jdownloader con alguna actualización pendiente requiere que se ejecute de nuevo.

Además si nuestro NAS lo apagamos, nuestro jail no iniciara al Jdownloader por lo comentado anteriormente, entonces, en nuestro jail de Jdownloader nos creamos un pequeño fichero de configuración.

Creando fichero en etc/rc.d

El contenido del fichero que permitirá que nuestro Jdownloader arranque en cada inicio es este:

root@jdownloader:/etc/rc.d # touch jdownloader (1)
root@jdownloader:/etc/rc.d # chmod +x jdownloader (2)
root@jdownloader:/etc/rc.d # nano jdownloader (3)
1 Creamos un fichero llamado jdownloader en esa ruta
2 Establecemos permisos de ejecución
3 Editamos con nano o el que sea 😆
#!/bin/sh

. /etc/rc.subr

name=jd2
rcvar=jd2_enable (1)

start_cmd="${name}_start"
stop_cmd="${name}_stop"

load_rc_config $name

jd2_start()
{
#!/bin/bash
echo "starting JDownloader2..."
umask 000
cd /jdownloader/
/usr/local/bin/java -Djava.awt.headless=true -jar /root/jdownloader/JDownloader.jar >/dev/null 2>/dev/null & (2)
}

jd2_stop()
{
#!/bin/bash
echo "killing java (and JDownloader2)..."
pgrep java | xargs kill
}

jd2_restart()
{
#!/bin/bash
echo "killing java (and JDownloader2)..."
pgrep java | xargs kill
umask 000
cd /jdownloader/
echo "starting JDownloader2..."
/usr/local/bin/java -Djava.awt.headless=true -jar /root/jdownloader/JDownloader.jar >/dev/null 2>/dev/null &
}

run_rc_command "$1"
1 Es el nombre de nuestro daemon j2d podemos llamarle como queramos
2 Esto inicia nuestro .jar y crea un PID del proceso/daemon

Ruta /etc/rc.conf

Necesitamos ahora ir al /etc/rc.conf de nuestro jail y añadir la siguiente líneas, para habilitar nuestro daemon de JDownloader en cada inicio del jail.

syslogd_flags="-ss"
sendmail_enable="NO"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"
cron_flags="-J 60"
ifconfig_e0b_bastille4_name="vnet0"
ifconfig_vnet0="inet 192.168.1.248/24"
defaultrouter="192.168.1.1"
jd2_enable="YES" (1)
1 Habilitando el daemon en el boot del jail
Es importante ejecutar nuevamente el .jar para añadir nuestra autenticación con email y password como en la imagen de abajo.

login jdownloader

Verificando actualización de JDownloader

63|Log.L.log 12/2/23, 2:57:50 PM - INFO [ org.appwork.storage.JsonKeyValueStorage(<init>) ] -> Prefer (merged) JSon Storage from File: /root/jdownloader/cfg/org.jdownloader.captcha.v2.solver.solver9kw.Captcha9kwSettings.json
63|Log.L.log 12/2/23, 2:57:50 PM - INFO [ org.appwork.loggingv3.LogV3(info) ] -> Overridden Config Key found public abstract boolean org.jdownloader.captcha.v2.solver.solver9kw.Captcha9kwSettings.isEnabled()<-->public abstract boolean org.jdownloader.captcha.v2.ChallengeSolverConfig.isEnabled()
Start Update
Update Message: Check for updates
Update Progress: 3%
Update Progress: -1%
Update Message: Contact Server...
Update Message: Check for updates
Update Progress: 100% (1)
1 Actualización lista.

Ahora mismo podriamos ir a la web https://my.jdownloader.org/ y loguearnos.

En caso de reiniciar el jail por nuestra parte bastaría con un ejecutar jexec o bastille cmd que es una utilidad para ejecutar comandos dentro de un jail, sin estár dentro de el.

xigmanas: /# bastille restart jdownloader (1)
[jdownloader]:
jdownloader: removed

[jdownloader]:
e0a_bastille4
e0b_bastille4
jdownloader: created

xigmanas: /# jexec -l jdownloader top (2)
last pid: 16487;  load averages:  0.16,  0.10,  0.04
4 processes:   1 running, 3 sleeping
CPU:  0.3% user,  0.0% nice,  0.0% system,  0.0% interrupt, 99.7% idle
Mem: 295M Active, 962M Inact, 8604M Wired, 21G Free
ARC: 5636M Total, 566M MFU, 5007M MRU, 96K Anon, 17M Header, 47M Other
     4611M Compressed, 5333M Uncompressed, 1.16:1 Ratio
Swap: 2048M Total, 2048M Free

  PID USERNAME    THR PRI NICE   SIZE    RES STATE    C   TIME    WCPU COMMAND
16135 root         50  52    0    10G   316M uwait   21   0:24   2.40% java (3)
16487 root          1  20    0    14M  3580K CPU4     4   0:00   0.05% top
16411 root          1  20    0    13M  2772K select  15   0:00   0.00% syslogd
16459 root          1  20    0    13M  2604K nanslp  10   0:00   0.00% cron
1 reiniciando el jail y todo bien
2 Mirando que el proceso java dentro del jail este activo
3 daemon de jdownloader activo 🔥
xigmanas: ~# bastille cmd jdownloader top
[jdownloader]:

last pid: 56994;  load averages:  0.00,  0.00,  0.00                                                     up 26+10:35:47  19:30:08
6 processes:   1 running, 5 sleeping
CPU:  0.0% user,  0.0% nice,  0.1% system,  0.0% interrupt, 99.9% idle
Mem: 143M Active, 12G Inact, 8208K Laundry, 18G Wired, 1243M Free
ARC: 16G Total, 1615M MFU, 14G MRU, 96K Anon, 120M Header, 299M Other
     14G Compressed, 16G Uncompressed, 1.13:1 Ratio
Swap: 2048M Total, 2048M Free

  PID USERNAME    THR PRI NICE   SIZE    RES STATE    C   TIME    WCPU COMMAND
 5670 root         53  52    0    11G   745M uwait   11  54:58   0.15% java (1)
56974 root          1  20    0    14M  3700K CPU7     7   0:00   0.01% top
 5996 root          1  20    0    13M  2580K nanslp  20   0:07   0.00% cron
 5951 root          1  20    0    13M  2772K select  17   0:05   0.00% syslogd
56975 root          1  20    0    13M  2584K piperd   7   0:00   0.00% cron
56976 root          1  20    0    13M  2584K nanslp  23   0:00   0.00% cron
1 proceso java activo.

Template de jdownloader

Con el parametro template usamos bajo fondo un bastillefile que nos permitira automatizar procesos luego de crear un jail, muy util tal cual como hace un dockerfile.

El fichero bastillefile debe ir dentro del directorio de bastille/template

bastille template jdownloader test/bastille-jdownloader (1)
1 Aplicamos el template al jail corriendo, llamado jdownloader.

Tenemos también 2 parametros opcionales, para cambiar la versión de la jdk y la de jdownloader que casi siempre actualizan el enlace de mega.

  • OPEN_JDK_VERSION

  • J2_URL

bastille template jdownloader test/bastille-jdownloader --arg OPEN_JDK_VERSION=17
ARG OPEN_JDK_VERSION=11
ARG J2_URL=https://mega.nz/file/fRNwxBza#oTUD5FEuWccnIvHzPdMwdE_ju4HndEDXbaf-VtoBnao

PKG openjdk${OPEN_JDK_VERSION} megatools

CMD mkdir jdownloader
CMD chmod -R 775 jdownloader
CMD cd jdownloader && megadl ${J2_URL}
CMD chmod +x jdownloader/JDownloader.jar

#Copy the etc file from the template to the jdownloader jail etc file
CP etc /
RENDER /etc/rc.d/jdownloader
CMD chmod +x /etc/rc.d/jdownloader

SYSRC jd2_enable="YES"

CONFIG set allow.mlock;
CONFIG set allow.raw_sockets;

CMD java -jar jdownloader/JDownloader.jar
CMD pkill java
CMD java -jar jdownloader/JDownloader.jar -norestart (1)
1 El último comando en la cual lanzara un login a Jdownloader, e introduciremos nuestro user y password.

Actualizar desde MyJDownloader

En la interface web, en settings podemos actualizarle, pero como tenemos autoboot él se reiniciará automáticamente.

jdownloaderUpdateAndRestart

Esta actualización la realize por gusto, en realidad es opcional, sin hacerla me permitía usar jdownloader tranquilamente.

Ruta de descarga

Aquí en settings también podemos cambiar la ruta de descarga, el directorio debe existir en nuestro jail

carpetaDownloads


Instalando Jellyfin

Esto no es Netflix logo, nosotros nos corresponde hacer todo el trabajo de guardar las películas en nuestro NAS, y desde el Jellyfin, acceder a ese directorio, pool/dataset donde las tengamos, para poder verlas 😁

45698031?s=200&v=4

Jellyfin es un fork de emby, como plex, pero opensource, como muchas opciones para tener nuestras películas en nuestro NAS organizadas, editar subtítulos, control de usuarios hasta control parental, añadir musica, tiene un Api para aplicaciones externas, extensiones, la versión que usare es 10.7.7 es un poco vieja, pero viene bastante bien, muy superior al miniDLNA 😅.

Pasos básicos de instalación más repositorio de inspiración

Configuración del jails.conf

Para jellyfin es importante añadir las últimas 2 líneas:

jellyfin {
  devfs_ruleset = 13;
  enforce_statfs = 2;
  exec.clean;
  exec.consolelog = /mnt/pool/extensions/bastille/logs/jellyfin_console.log;
  exec.start = '/bin/sh /etc/rc';
  exec.stop = '/bin/sh /etc/rc.shutdown';
  host.hostname = jellyfin;
  mount.devfs;
  mount.fstab = /mnt/pool/extensions/bastille/jails/jellyfin/fstab;
  path = /mnt/pool/extensions/bastille/jails/jellyfin/root;
  securelevel = 2;

  vnet;
  vnet.interface = e0b_bastille0;
  exec.prestart += "jib addm bastille0 igb0";
  exec.prestart += "ifconfig e0a_bastille0 description \"vnet host interface for Bastille jail jellyfin\"";
  exec.poststop += "jib destroy bastille0";
  allow.mlock; (1)
  allow.raw_sockets; (2)
}
1 Relacionado con la memoria del jail.
2 Habilita utilidades como ping y traceroute dentro del jail.
La dirección ip a usar es la que le asignamos al jail y puerto por defecto 8096

peliculasJellyFin

Error al reinstalar Jellyfin

Me paso muchas veces que al querer reinstalar jellyfin, no podía hacerlo por tener un UID repetido, en realidad por usuarios/grupos.
The service file uses daemon to restart jellyfinserver if it crashes.
The service file will also change the permissions so that the updater works.
If this behavior is unwanted you will need to edit the RC file manually and
remove the daemon and/or the permissions changes.

If you are running this in a jail please set "allow_mlock=1" or similar
for this jail otherwise the program will fail to start.

dotNET does not work well inside jails that are missing either a) VNET or
one of b) ip6=inherit c) ip6=new. The service file will try workaround any
user misconfiguration but is not perfect.

root@jailvnet2:~ # pw user add jellyfin -c jellyfin -u 710 -d /nonexistent -s /usr/bin/nologin
pw: uid `710' has already been allocated (1)

root@jailvnet2:~ # ln -s /usr/local/lib/libsqlite3.so /usr/local/lib/libe_sqlite3

root@jailvnet2:~ # sysrc jellyfinserver_enable=TRUE
jellyfinserver_enable:  -> TRUE
root@jailvnet2:~ # sysrc jellyfinserver_user=jellyfin
jellyfinserver_user:  -> jellyfin

root@jailvnet2:~ # sysrc jellyfinserver_group=jellyfin
jellyfinserver_group:  -> jellyfin

root@jailvnet2:~ # service jellyfinserver start
install: unknown group jellyfin
install: unknown group jellyfin
Starting jellyfinserver.
su: unknown login: jellyfin
/usr/local/etc/rc.d/jellyfinserver: WARNING: failed to start jellyfinserver (2)
root@jailvnet2:~ #
1 El UID 710 estaba repetido, pareciendo que otro usuario lo tiene, entonces lo que hariamos es colocar otro.
2 Al intentar iniciar el servidor jellyfin tenemos este error.

Lo fixeamos con lo siguiente

pw user add jellyfin -c jellyfin -u 715 -d /nonexistent -s /usr/bin/nologin (1)
1 Usamos ahora UID 715, para fixear el error.
root@jailvnet2:~ # service jellyfinserver start
Starting jellyfinserver. (1)
1 Con este solo mensaje, indica que todo esta bien, ahora podemos acceder con la dirección ip del jail mas el puerto 8096

Actualizando Jellyfin

Es buena idea tener un backup o clonar el jail en casos como estos, bastille lo permite, vía consola o web.
root@jellyfin:~ # service jellyfinserver stop (1)
Stopping jellyfinserver.
root@jellyfin:~ # fetch https://github.com/Thefrank/jellyfin-server-freebsd/releases/download/v10.8.1/jellyfinserver-10.8.1.pkg (2)
jellyfinserver-10.8.1.pkg                               58 MB 7305 kBps    08s
root@jellyfin:~ # ls
.cshrc				.k5login			.shrc
.history			.login				jellyfinserver-10.7.7.pkg
.hushlogin			.profile			jellyfinserver-10.8.1.pkg
root@jellyfin:~ # pkg install jellyfinserver-10.8.1.pkg (3)
Updating FreeBSD repository catalogue...
[jellyfin] Fetching packagesite.pkg: 100%    6 MiB   3.3MB/s    00:02
Processing entries: 100%
FreeBSD repository update completed. 31614 packages processed.
All repositories are up to date.
New version of pkg detected; it needs to be installed first.
The following 1 package(s) will be affected (of 0 checked):

Installed packages to be UPGRADED:
	pkg: 1.17.5 -> 1.18.3 (4)

Number of packages to be upgraded: 1

8 MiB to be downloaded.

Proceed with this action? [y/N]: y (5)
[jellyfin] [1/1] Fetching pkg-1.18.3.pkg: 100%    8 MiB   2.2MB/s    00:04
Checking integrity... done (0 conflicting)
[jellyfin] [1/1] Upgrading pkg from 1.17.5 to 1.18.3...
[jellyfin] [1/1] Extracting pkg-1.18.3: 100%
Updating FreeBSD repository catalogue...
FreeBSD repository is up to date.
All repositories are up to date.
The following 2 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
	jellyfinserver: 10.8.1
	krb5-120: 1.20

Number of packages to be installed: 2

The process will require 135 MiB more space.
1 MiB to be downloaded.

Proceed with this action? [y/N]: y (6)
[jellyfin] [1/2] Fetching krb5-120-1.20.pkg: 100%    1 MiB   1.2MB/s    00:01
Checking integrity... done (0 conflicting)
[jellyfin] [2/2] Installing jellyfinserver-10.8.1...
===> Creating groups.
Using existing group 'jellyfinserver'.
===> Creating users
Using existing user 'jellyfinserver'.
[jellyfin] Extracting jellyfinserver-10.8.1: 100%
[jellyfin] [1/2] Installing krb5-120-1.20...
[jellyfin] [1/2] Extracting krb5-120-1.20: 100%
=====
Message from jellyfinserver-10.8.1:

--
jellyfinserver relies on Microsoft dotNET5+ SDK to be built
Microsoft does not have an official version of dotNET for FreeBSD

This package was built with an UNOFFICIAL UNSUPPORTED version of dotNET
If this is something that you do not want, remove this package with
"pkg remove jellyfinserver"

This package installs a service file.
Enable it with "sysrc jellyfinserver_enable=TRUE"
Start it with "service jellyfinserver start".

The service file uses daemon to restart jellyfinserver if it crashes.
The service file will also change the permissions so that the updater works.
If this behavior is unwanted you will need to edit the RC file manually and
remove the daemon and/or the permissions changes.

If you are running this in a jail please set "allow_mlock=1" or similar
for this jail otherwise the program will fail to start.

dotNET does not work well inside jails that are missing either a) VNET or
b) ip6=inherit. The service file will try workaround any user misconfiguration
but is not perfect.
root@jellyfin:~ # service jellyfinserver start (7)
Starting jellyfinserver.
1 Paramos el servidor jellyfin
2 fetch a la nueva versión de jellyfin
3 Se instala el .pkg versión 10.8.1 de jellyfin
4 Se muestra la versión que se actualizara.
5 Yes
6 Yes
7 Iniciamos el servidor jellyfin

Tenemos la actualización lista

version10.8.1.jellyfin

Registros / Logs

Esta parte nos permite visualizar nuestros logs, muy util.

logsJellyfin

Puede suceder, que las imagenes de las películas no sean las correctas, pero tampoco puedan cambiarse correctamente desde la interface web, pero puede ser falta de permisos de escritura en el NAS gracias a los logs se puede verificar eso.
[2022-05-08 15:57:59.602 +00:00] [ERR] [18] MediaBrowser.Providers.Manager.ProviderManager: UnauthorizedAccessException - Access to path "/mnt/movies_1/Spiderman un nuevo universo (2018)/folder.jpg" is denied. Will retry saving to "/var/db/jellyfinserver/metadata/library/a4/a45b8287c09ccc439897b6158d8d88bd/poster.jpg"
[2022-05-08 15:57:59.828 +00:00] [ERR] [18] MediaBrowser.Providers.Manager.ProviderManager: UnauthorizedAccessException - Access to path "/mnt/movies_1/Spiderman un nuevo universo (2018)/backdrop.jpg" is denied. Will retry saving to "/var/db/jellyfinserver/metadata/library/a4/a45b8287c09ccc439897b6158d8d88bd/backdrop.jpg"
[2022-05-08 15:58:02.249 +00:00] [ERR] [24] MediaBrowser.Providers.Manager.ProviderManager: Error in metadata saver
(1)
System.UnauthorizedAccessException: Access to the path '/mnt/movies_1/Spiderman un nuevo universo (2018)/Spider-Man Un nuevo universo (2018).nfo' is denied.
 ---> System.IO.IOException: Permission denied
   --- End of inner exception stack trace ---
   at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirectory, Func`2 errorRewriter)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   at MediaBrowser.XbmcMetadata.Savers.BaseNfoSaver.SaveToFile(Stream stream, String path)
   at MediaBrowser.XbmcMetadata.Savers.BaseNfoSaver.Save(BaseItem item, CancellationToken cancellationToken)
   at MediaBrowser.Providers.Manager.ProviderManager.SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable`1 savers)
1 Error a la hora de escribir en el directorio, entonces, no se podrian editar las portadas y demás.
Las imagenes aún con problemas de permisos podemos cambiarla, pero es fastidioso.

besoDragon

Ajustes de permisos en el dataset

El dataset que tiene las películas debe tener el usuario jellyfin y grupo asignado también debe coincidir con el usuario jellyfin del contenedor como lo hacemos ? fácil, entramos en el contenedor jellyfin y ejecutamos esto para conocer el id de dicho usuario:

servernas: /mnt# bastille console jellyfin
[jellyfin]:
root@jellyfin:~ # id jellyfin (1)
uid=868(jellyfin) gid=868(jellyfin) groups=868(jellyfin)
root@jellyfin:~ # id 868 (2)
uid=868(jellyfin) gid=868(jellyfin) groups=868(jellyfin)
root@jellyfin:~ #
1 Buscamos el id del usuario jellyfin dentro del container.
2 Lo mísmo pero al contrario buscamos el usuario del id 868 dentro del container.

Creamos el usuario/grupo con el id del contenedor, para que tengas permisos de escritura lectura, ejecución en este dataset.

permisos jellyfin dataset

Para el dataset tengo esta configuración actual y bien todo funciona correctamente.

Justo en el dataset donde guardo todas las peliculas.

permisos dataset ACL Mode

Recordemos que podemos crear el usuario dentro del contenedor igual al del host con el comando anterior para los permisos.
pw user add jellyfin -c jellyfin -u 868 \ (1)
-d /nonexistent -s /usr/bin/nologin
1 Id 868 como el del host.

Creando Watcher con Java

Si un poco loco, porque se puede usar un cron job, pero desde la UI de XigmaNAS es un dolor de cabeza y por alguna razón no funciona, tampoco se dispara correctamente.

Entonces suele pasar que cuando se añade una película al directorio /mnt/pool/movies los permisos no son los adecuados y la app jellyfin no puede pushear la portada de la película como debería (aunque si lo hace porque suele escribir en otro directorio donde si tiene permisos, pero no justifica igual) ni escribir el .nfo, entonces simplemente lo que se desea hacer es que, si se añaden datos al directorio se seteen los permisos más el usuario correcto y listo sin tanto problema.

Necesitamos:

  • El .jar con el código necesario para actuar como listener de los cambios que se producen en un directorio

  • Acceso root para setear los permisos correctos.

  • El script para añadirlo al arranque, en el rc.d

  • Habilitar el proceso en el inicio en el /etc/rc.conf

El script del Watcher

#!/bin/sh

. /etc/rc.subr

name=watcher
rcvar=watcher_enable

start_cmd="${name}_start"
stop_cmd="${name}_stop"

load_rc_config $name

: ${watcher_args_movies_pool="-u jellyfin -p /media/downloads/movies -o 775"} (1)
: ${watcher_args_series_pool_3="-u jellyfin -p /media/downloads/series_pool_3 -o 775"}
: ${watcher_args_movies_pool_3="-u jellyfin -p /media/downloads/movies_pool_3 -o 775"}

watcher_start()
{
  #!/bin/bash
  echo "starting Watcher..."
  umask 000                                                 (2)
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_movies_pool} >/dev/null 2>/dev/null &
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_series_pool_3} >/dev/null 2>/dev/null &
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_movies_pool_3} >/dev/null 2>/dev/null &

}

watcher_stop()
{
  #!/bin/bash
  echo "killing java (and Watch)..."
  pgrep java | xargs kill
}

watcher_restart()
{
  #!/bin/bash
  echo "killing java (and Watcher)..."
  pgrep java | xargs kill
  umask 000
  echo "starting watcher..."
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_movies_pool} >/dev/null 2>/dev/null &
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_series_pool_3} >/dev/null 2>/dev/null &
  /usr/local/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar ${watcher_args_movies_pool_3} >/dev/null 2>/dev/null &
}

run_rc_command "$1"
1 Estos son los parámetros que se le pasan al watcher
  • -u El usuario del nuevo directorio añadido.

  • -p El path a escuchar.

  • -o El modo octal, bien sea 775, 770, etc

2 Importante, nuestro .jar debe estar en el directorio /root/nombre-cualquier-directorio/watcher.jar en este caso estamos ejecutando este .jar en un jail.

El código Java

Ya el código existe en gran parte, pero debemos adaptarlo a nuestro caso, porque dicho código solo escanea cambios en el directorio para ficheros de texto solamente, nosotros vamos más que eso.

La ventaja es que, con cron tenemos un tiempo definido, 2, 3 minutos o el que sea, pero si no queremos esperar 2 minutos o 3 ?, y que los cambios de los directorios se den de inmediato, o mejor dicho en un futuro solo si existen cambios ? pues tenemos esta forma, por medio de eventos, este servicio procesa 3 tipos, CREATE, DELETE, y MODIFY
// imports removed for brevity

/**
 * Listening for changes in the <strong>/mnt/pool/movies</strong>  dataset
 * <p>
 * And if there is a creation event we change the owner to <strong>jellyfin</strong> with the octal mode 775
 */
public class WatchDir {

    private static final Logger LOGGER = Logger.getLogger(WatchDir.class.getName());

    /**
     * Octal 775
     */
    private static final String OCTAL_775 = "rwxrwxr-x";
    /**
     * Octal 770
     */
    private static final String OCTAL_770 = "rwxrwx---";
    /**
     * The dataset path to watch, is used with the <strong>-p</strong> parameter
     */
    private final Path path;
    /**
     * The octal mode, is used with the <strong>-o</strong> parameter
     */
    private final String octal;
    /**
     * The user name to apply in this directory, is used with the <strong>-u</strong> parameter
     */
    private final String userName;

    public WatchDir(final String userName, final String path, final String octal) {
        this.userName = userName;
        this.path = Path.of(path);
        this.octal = octal;

        this.watchDirectory();
    }

    @SuppressWarnings("unchecked")
    private void watchDirectory() {
        try {
            WatchService watcher = FileSystems.getDefault().newWatchService(); (1)
            this.registerAll(path, watcher);  (2)
            WatchKey key;
            LOGGER.info("Listening in..." + path);
            for (;;) {
                // wait for key to be signaled
                try {
                    key = watcher.take();
                } catch (InterruptedException x) {
                    return;
                }

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();

                    if (kind == OVERFLOW) {
                        continue;
                    }

                    WatchEvent<Path> ev = (WatchEvent<Path>) event;
                    Path filename = ev.context();

                    if (kind == ENTRY_CREATE) { (3)
                        /*
                         *   Si pasamos el filename solo se procesara el directorio nuevo.
                         *   Si pasamos el path del parametro -p por el filename, procesaremos el directorio completo
                         */
                        LOGGER.info("CREATE: " + filename);
                        changePermissionsAndOwnerRecursively(path); (4)
                    }

                    if (kind == ENTRY_DELETE) {
                        LOGGER.info("DELETE: " + filename);
                        changePermissionsAndOwnerRecursively(path);
                    }

                    if (kind == ENTRY_MODIFY) {  (5)
                        LOGGER.info("MODIFY: " + filename);
                        changePermissionsAndOwnerRecursively(path);
                    }

                }

                boolean valid = key.reset();
                if (!valid) {
                    break;
                }
            }
        } catch (IOException x) {
            LOGGER.severe(x.getMessage());
        }

    }

    /**
    * Register the given directory and all its sub-directories with the WatchService.
     */
    private void registerAll(final Path start, final WatchService watchService) throws IOException {
        // register directory and sub-directories
        Files.walkFileTree(start, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {
                dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
                return FileVisitResult.CONTINUE;
            }

        });
    }

    /**
     * Change permissions recursively
     *
     * @param pathParam
     */
    private void changePermissionsAndOwnerRecursively(Path pathParam) {
        try (Stream<Path> stream = Files.walk(pathParam)) { // FOLLOW_LINKS (6)
            stream.forEach(path -> {
                this.setPermissions(path); (7)
                this.changeOwner(path); (8)
            });
            LOGGER.info("Owner changed correctly to " + this.userName);
            LOGGER.info("Permissions changed correctly");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Set octal permissions
     *
     * @param path the current dir or a filename
     *
     */
    private void setPermissions(final Path path) {
        try {
            final String octalMode = this.octal.equals("775") ? OCTAL_775 : OCTAL_770;
            Set<PosixFilePermission> permissions = PosixFilePermissions.fromString(octalMode);
            Files.setPosixFilePermissions(path, permissions); (9)
        } catch (IOException e) {
            LOGGER.severe(e.getMessage());
        }
    }

    /**
     * The user to the directory and files is set up, you must have root permissions to run this.
     * <p>
     * The user must exist, otherwise, a <strong>UserPrincipalNotFoundException</strong>
     *
     * @param path
     * @throws IOException
     */
    private void changeOwner(final Path path) {
        try {
            UserPrincipalLookupService lookupService = path.getFileSystem().getUserPrincipalLookupService();
            UserPrincipal userPrincipal = lookupService.lookupPrincipalByName(this.userName);
            Files.setOwner(path, userPrincipal); (10)
        } catch (IOException ex)  {
            LOGGER.severe("The user "+ this.userName + " does not exist");
            LOGGER.severe("leaving now...");
            System.exit(0);
        }

    }

    /**
     *
     * @param args
     */
    public static void main(String... args) {
        System.out.println("");
        System.out.println("rubn");
        System.out.println("https://rubn0x52.com");

        if (args.length == 0) {
            LOGGER.info("No input parameters present");
            System.exit(1);
        } else if (("-h".equals(args[0])
                || "-help".equals(args[0])
                || "--help".equals(args[0])
                || "--h".equals(args[0]))) {
            LOGGER.info("usage: java -jar watcher.jar -u [--user ex: an existing user] -p [--path ex: path,dir,dataset] -o [--octal mode ex: 775, 770]");
            System.exit(1);
        } else {
            if (args.length > 3 && args.length <=5) {// valid path args[3]
                LOGGER.info("Please view usage with --h, -h, -help, --help" );
                System.exit(0);
            } else if(args.length > 3 && args.length <=6) {
                (11)
                new WatchDir(args[1].trim(), args[3].trim(), args[5].trim());
            } else {
                LOGGER.info("Please view usage with --h, -h, -help, --help" );
                System.exit(0);
            }
        }
    }

}
1 El servicio WatchService la magia pura
2 Esta línea se le pasan los eventos que queremos procesar recursivamente por los directorios hijos, se usa el método walkFileTree quedando el código mas limpio que usando el método walk.
3 Cuando tenemos un evento de tipo CREATE, o sea, un nuevo directorio.
4 Aquí esta la logica nuestra, usamos el método walk para hacer un recorrido recursivo por ese directorio.
5 Es necesario procesar eventos tipo MODIFY porque se da el caso donde se añada un directorio en otro existente contando como una modificación.
6 El método walk que hace la magia también, puede recorrer enlaces simbólicos pero no es nuestro caso, el número 3 es para la profundidad del recorrido, es decir, si añadimos un directorio que contenta más de 3 carpetas, el solo busca hasta la tercera carpeta las demás las ignorará, en un inicio lo dejo, pero ya no, mejor quitarlo y listo.
7 Cambiamos permisos al directorio recursivamente.
8 Cambiamos el owner del directorio recursivamente también.
9 Sete los permisos en modo octal.
10 Esto cambia el owner.
11 Aquí instanciamos solo si tenemos los 3 parámetros correctos.

Verificar si el script se ha ejecutado

Para verificar que nuestro .jar se ha iniciado correctamente usamos

root@jdownloader:~ # ps -aux | grep -e jar
root 7211  0.7  0.1 27799236  94264  0  IJ   11:55   0:01.94 /usr/local/openjdk11/bin/java -Djava.awt.headless=true -jar /root/watcher/watcher.jar -u jellyfin -p /me (1)
root 7209  0.2  0.7 28413212 696620  0  IJ   11:55   0:35.96 /usr/local/openjdk11/bin/java -Djava.awt.headless=true -jar /root/jdownloader/JDownloader.jar
root 7591  0.0  0.0    12812   2360  0  S+J  11:56   0:00.00 grep -e jar
1 Ejecución correcta.
En caso de inhabilitar el Script podemos hacerlo en el etc/rc.conf y marcarlo a "NO"

Usar imdb por defecto para las portadas

Tenemos que ir a administrar la biblioteca

gestionarBibliotecaParaUsarImdbPorDefecto

Con esto lograremos que cada portada use por defecto la que esta en imdb una de las más confiables y viejas.

select imbd open movie database

Aquí vemos que las imágenes son la misma, es decir todo bien.
La imagen de portada en jellyfin

2fast2furious

La imagen de portada en imdb

imdb2fast2Furious

OpenSubtitles

catalogoPluginOpenSubtitles

Este es un plugin para subtítulos muy útil, en caso de tener una película con caracteres raros en las traducciones.

Necesitamos crearnos un usuario para tener acceso al api, dicho usuario lo introduciremos en nuestro server jellyfin

userPassOpenSubtitles

Estatus del api OpenSubtitles

Esta url permite ver el estado del api de OpenSubtitles con pequeñas metricas.

Buscando subtítulos

En caso de un película vieja, y queramos subtítulos, en la esquina derecha, tenemos 3 puntos verticales, luego en "Editar subtítulos", que nos mostrara la modal siguente:

Por defecto tenemos

  • Subtitulos Latino - Es

  • Pero queremos Inglés por ejemplo

busquedaSubtitulos

Metadatos con MKVToolNix

mkvtoolnix logo

Muchos archivos por ahí (.mp4 .mkv etc), tendran metadatos, e incluso llegan a ser un poco molestos.

En el caso de archivos .mkv viene muy bien el MKVToolNix vía GUI, es muy rápido, importamos nuestra película y editamos el metadato que queramos…​

Lo mejor es editar el fichero en local, si editamos el video directamente en el directorio del NAS, tardaria muchisimo (aún en LAN), podemos descargar la película desde el mismo jellyfin a nuestra pc editar y luego subirla.

mkvtoolnixProperties

En el menu donde "multiplexador" tenemos varias cosas:

  • Codec

    • Cada item corresponde a una parte que podemos editar.

  • Propiedades

    • Podemos cambiar por ejemplo "Nombre de pista".

  • Pestaña/tab Salida

    • General → Título del archivo: escribimos el nombre del video(o simplemente borrar el texto original que no queramos), en ciertos reproductores, este nombre se mostrara en el inicio de la película.

  • Archivo de salida:

    • Este será el nombre del fichero en realidad puede ser cualquiera, no es un metadato, se puede editar como cualquier archivo.

  • Iniciar multiplexado

    • Luego de tener todo listo, clickeamos ese botón.

iniciarMultiplexado


Seteando path del ffmpeg

Hacer esto es obligatorio, para poder reproducir.

Desde la release de jellyfin 10.8.13, el input del ffmeg fue deshabilitado por seguridad, y debemos setearla nosotros desde la consola

Dentro del jail tenemos el fichero encoding.xml justo en esta ruta y es el que debemos editar

/root/var/db/jellyfin/config

Con el comando which ffmpeg obtenemos la ruta usr/local/bin/ffmpeg

Hacemos service jellyfin stop y editamos con nano encoding.xml para añadir la ruta anterior.

Editamos

<EncoderAppPath>/usr/local/bin/ffmpeg</EncoderAppPath>
<EncoderAppPathDisplay>/usr/local/bin/ffmpeg</EncoderAppPathDisplay>

Nuevamente iniciamos el servicio con service jellyfin start y tendriamos ya el input seteado correctamente como aquí

input ffmpeg en jellyfin


Amazon fire stick 4k

em>

El Amazon fire stick nos permite instalar aplicaciones en el, basado en Android tv, nos quitamos esa molestia que nos pasa con ciertos smart-tv a la hora de instalar una app, evitando usar el protocolo DLNA que es más inestable de lo que parece.

En el se instalará la versión cliente de jellyfin, muy fácil e intuitivo, lo cuál permitirá acceder al jellyfin instalado en el jail, versión servidor de él, con ip y puerto igual.

Ofrece conector macho HDMI, la mayoría de los smart-tv tienen puerto`s HDMI hembra, son un standard prácticamente hoy en día.

También ideal si lo llevamos a un lugar remoto, lo conectamos y accedemos a nuestro NAS tanto local, o remotamente con ciertos cambios, sería útil tener un dns dinámico como veremos próximamente con duckdns para esas conexiónes remotas."

amazonfireStickMasVisual

Dimensiones 99 x 30 x 14 mm (solo el dispositivo) 108 x 30 x 14 mm (incluyendo el conector)

Peso

53,6

Procesador

Quad-Core de 1,7 GHz

GPU

IMG GE8300

Almacenamiento

8 GB

Wifi

Wifi de doble banda y doble antena (MIMO) que permite una transferencia de datos más rápida y pérdida de conexión menor que con el wifi estándar. Compatible con redes wifi 802.11a/b/g/n/ac.

Bluetooth

Bluetooth 5.0 y LE. Se puede vincular con altavoces, auriculares, mandos de videojuegos y otros dispositivos Bluetooth compatibles.


Reverse proxy para acceso remoto

cropped NGINX product square 1 150x150

Ian me trajo recuerdos con una cosa interesante, para mejor administración de los certificados SSL con un server nginx.

Nuevamente nos creamos un Jail para instalar nuestro servidor nginx y redireccionar las peticiones http que queramos que pasen por aquí.

/usr/local/etc/nginx/nginx.conf (1)
1 Ruta de configuración del proxy nginx

En caso de editar el fichero nginx.conf

service nginx reload (1)
1 Para reiniciar el servidor nginx

Redirección a Https

Por lo visto si accedemos con http a nuestros servidores con nginx por defecto es posible entrar normal, y eso no lo queremos, podemos hacer una redirección a https, pero debemos ajustar nuestro nginx.conf

Fue necesario para esto, abrir el puerto 80 en el router con la ip del reverse-proxy, porque sin eso no funciona.
server {
    listen       443 ssl; (1)
    server_name localhost nuestroDominio.duckdns.org;
    ssl_certificate      /usr/local/etc/letsencrypt/live/dominio/fullchain.pem;
    ssl_certificate_key  /usr/local/etc/letsencrypt/live/dominio/privkey.pem;
    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;
    location / {
      proxy_pass http://192.168.1.130:8096; (2)
    }
    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000 includeSubDomains; preload" always;
    #error_page  404              /404.html;
    # redirect server error pages to the static page /50x.html
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/local/www/nginx-dist;
    }
    set $https_redirect 0;
    #if request came from port 80/http
    if ($server_port = 80) {
        set $https_redirect 1;
    }
    # or if the requested host came with www
    if ($host ~ '^www\.') {
        set $https_redirect 1;
    }
    #then it will redirects
    if ($https_redirect = 1) {
        return 301 https://$server_name$request_uri;
    }
}
server { (3)
    listen 80 default_server;
    server_name _;
    return 301 https://$host$request_uri;
}
1 Config para el https
2 Nuestro server en local de jellyfin
3 Confi necesaria de http

Como renovar el certificado SSL de letsencrypt con certbot?

certbot logo

Podemos usar certbot con instrucciones para freeBSD los certificados que hemos creado con letsencrypt, estos se vencen a los 90 días.

root@reverse-proxy:/usr/local/etc/nginx # certbot renew (1)
root@reverse-proxy:/usr/local/etc/nginx # certbot renew --dry-run (2)
1 Para renovar los certificados.
2 Sirve para testear la renovación automática, sin generar los certificados.

Pero obtenemos este error

Saving debug log to /var/log/letsencrypt/letsencrypt.log (1)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /usr/local/etc/letsencrypt/renewal/mydomain.bla.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for mydomain.bla

Certbot failed to authenticate some domains (authenticator: standalone). The Certificate Authority reported these problems:
  Domain: mydomain.bla
  Type:   unauthorized
  Detail: ip: Invalid response from http://mydomain.bla/.well-known/acme-challenge/e5P1zpHbQUyQ6R28jew3Z9rRxpMPzUdsLE7Gdlp5ZqI: 404
(2)
Hint: The Certificate Authority failed to download the challenge files from the temporary standalone webserver started by Certbot on port 80. Ensure that the listed domains point to this machine and that it can accept inbound connections from the internet.

Failed to renew certificate mydomain.bla with error: Some challenges have failed.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
All renewals failed. The following certificates could not be renewed:
  /usr/local/etc/letsencrypt/live/mydomain.bla/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 renew failure(s), 0 parse failure(s)
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
1 Ruta del log generado, y que debemos inspeccionar en caso de algo raro.
2 Por lo visto esta estrechamente relacionado con nuestro error, y el puerto 80 bloqueado

Si investigamos mas con el comando certbot renew -v tenemos que se usa el:

http-01 challenge for xxx.xxx.net pero justo este no nos sirve.

Necesitamos usar otro challenge que nos permita hacer esto, nuestro proveedor nos bloquea el puerto 80 😡.

Usando DNS dinámico con DuckDNS

ducky icon small

Anteriormente me valia con no-ip para mi ip dinámica, pero con este dns-01-challenge necesito escribir los record TXT en el, y no me va, porque habría que pagar, prefiero darle ese dinero a los pobres.

Ahora con duckdns me da todo, gratis, y un api-rest para actualizar el record TXT, una vez que introduzca el comando necesario con certbot:

certbot certonly --manual --preferred-challenges dns -d miDominio.duckdns.org (1)
1 Mi dominio
root@reverse-proxy:/logs-letsencrypt # certbot certonly --manual --preferred-challenges dns -d foo.duckdns.org
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Certificate not yet due for renewal

You have an existing certificate that has exactly the same domains or certificate name you requested and isn't close to expiry.
(ref: /usr/local/etc/letsencrypt/renewal/foo.duckdns.org.conf)

What would you like to do?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Keep the existing certificate for now (1)
2: Renew & replace the certificate (may be subject to CA rate limits)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel):
1 La opción (1) me tocará ya que tengo el certificado vigente.

En caso de tener que renovar el certificado los pasos para actualizar el record TXT es aquí.

root@reverse-proxy:/logs-letsencrypt # certbot certonly --manual --preferred-challenges dns -d mySubName (1)
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for bar.duckdns.org

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:

_acme-challenge.bar.duckdns.org.

with the following value:

L9-------------------------------------(2)

Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.bar.duckdns.org.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 mySubName ejemplo bar.duckdns.org
2 Token generado de 43 caracteres para nuestro record TXT, para usar en los query-parameters del endpoint ofrecido por duck dns, es el paso siguiente.

Invocando al endpoint para actualizar certificado

Desde la consola usando curl o desde el navegador, o sea, enviar un GET para consumir la URL.

actualización token con chrome

https://www.duckdns.org/update?domains=mySubName&token=myTokenDeDuckDns&txt=tokenGenerado&verbose=true (1)
1 Insertamos nuestro token de duckdns (el que nos genera la web) en el parametro token, también nuestro token generado en el paso anterior para usarle en el parametro txt

Cuando el certificado se ha actualizado correctamente

Successfully received certificate.
Certificate is saved at: /usr/local/etc/letsencrypt/live/boo.duckdns.org/fullchain.pem
Key is saved at: /usr/local/etc/letsencrypt/live/foo.duckdns.org/privkey.pem
This certificate expires on 2023-12-06.
These files will be updated when the certificate renews.

NEXT STEPS:

This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate's expiry date.
If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

Una vez que setemos la nueva ruta de nuestros certificados fullchain.pem y privkey.pem nos bastaría con reiniciar nuestro server nginx

root@reverse-proxy:/servernginx # service nginx reload (1)
Performing sanity check on nginx configuration:
nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok
nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
1 reiniciando el nginx server.

Abrir puertos en el router

Toca también abrir los puertos en el router para el 443 del reverse-proxy y del 8096 del jellyfin para poder dar acceso fuera de nuestra red…​

Nuestro certificado activo

certificadoValidaDuckDNS

CGNAT y puertos

Para los que están dentro de un CGNAT, deben hablar con su ISP y comentarles que los saquen de la CGNAT, para poder abrir sus puertos tranquilamente.

Puede pasar que nuestra IP cambie, y todos nuestros puertos se cierren, y nos volvamos locos.

La idea es apagar el router por 15 minutos para que obtenga una IP fuera del CGNAT.

En un inicio sin dar solución, use ngrok para comprobar que todo andara bien y así fue, con eso me di cuenta de que era mi ISP, además de, abrir el jellyfin con el navegador TOR usando ngrok en ese momento.

Comentar que el jail del reverse proxy se le establecio ip dinámica con:

  • ifconfig_vnet0="SYNCDHCP"

pero, estaba con amarre ip-mac en el router, debído a que en cierto momento ese jail del reverse-proxy se quedo sin internet, cosas internas ya de mi red.

Y por último se actualizo la dirección ip de duckdns a la nueva, quizás se puedo automatizar mejor con un script o cron.