<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet href="/rss/stylesheet/" type="text/xsl"?>
<rss xmlns:content='http://purl.org/rss/1.0/modules/content/' xmlns:taxo='http://purl.org/rss/1.0/modules/taxonomy/' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:itunes='http://www.itunes.com/dtds/podcast-1.0.dtd' xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0" xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:atom='http://www.w3.org/2005/Atom' xmlns:podbridge='http://www.podbridge.com/podbridge-ad.dtd' version='2.0'>
<channel>
  <title>Jaryl Chng&apos;s Knowledge Base</title>
  <language>en-us</language>
  <generator>microfeed.org</generator>
  <itunes:type>episodic</itunes:type>
  <itunes:explicit>false</itunes:explicit>
  <atom:link rel="self" href="https://kb.jarylchng.com/rss/" type="application/rss+xml"/>
  <link>https://kb.jarylchng.com</link>
  <description>
    <![CDATA[<p>Welcome to the index page of my knowledge base, if you haven't done so, do visit my website at <a href="https://jarylchng.com" rel="noopener noreferrer" target="_blank">https://jarylchng.com</a>.</p><p>I will mainly use this site to document stuff, most of which will likely be in the public domain.</p>]]>
  </description>
  <itunes:author>Jaryl Chng</itunes:author>
  <itunes:image href="https://kb-static.jarylchng.com/kb-jarylchng-com/production/images/channel-c68f1f55f856ab833b4365991609dbec.png"/>
  <image>
    <title>Jaryl Chng&apos;s Knowledge Base</title>
    <url>https://kb-static.jarylchng.com/kb-jarylchng-com/production/images/channel-c68f1f55f856ab833b4365991609dbec.png</url>
    <link>https://kb.jarylchng.com</link>
  </image>
  <copyright>©2024</copyright>
  <itunes:category text="Technology"/>
  <item>
    <title>Sonos - Asus Router Merlin Yazfi Guest Network with One Way to Guest</title>
    <guid>72mN-cdfjph</guid>
    <pubDate>Sun, 28 Apr 2024 01:40:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>Update 13/01/2025</h2><p>For Sonos products that only support Airplay v2 (ERA100s, ERA300s, Sonos Arc Ultram etc.), there is currently a bug when placing your Sonos devices in a separate VLAN and trying to Airplay to them.</p><p>See this community thread for more information: <a href="https://en.community.sonos.com/advanced-setups-229000/unable-to-airplay-to-era-100-in-separate-vlan-ubiquity-6920955" rel="noopener noreferrer" target="_blank">https://en.community.sonos.com/advanced-setups-229000/unable-to-airplay-to-era-100-in-separate-vlan-ubiquity-6920955</a></p><h2>Update 16/05/2024 notice: version 80.xx.xx.xx</h2><p>After the mobile app update to version 80.xx.xx.xx, the method that the app use to discover speakers has completely changed. It is now required to allow destination UDP port 1900 (UPNP) traffic and source TCP port 1443 to flow to br0 instead:</p><pre class="ql-syntax" spellcheck="false">iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p udp --dport 1900 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p tcp --sport 1443 -j ACCEPT
</pre><p>Where 172.27.15.3 is the IP address of the Sonos system.</p><p>For Airplay to work it is also required to allow destination TCP ephemeral ports, source TCP 7000 (BBS), along with destination UDP 319:320 (PTP) ports to flow to br0:</p><pre class="ql-syntax" spellcheck="false">iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p tcp --dport 32768:65535 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p tcp --sport 7000 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p udp --dport 319:320 -j ACCEPT
</pre><p>Where 172.27.15.3 is the IP address of the Sonos system.</p><p>I've also noticed that Sonos also need source TCP 4444 (krb524) to br0 while running updates:</p><pre class="ql-syntax" spellcheck="false">iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p tcp --sport 4444 -j ACCEPT
</pre><p>Where 172.27.15.3 is the IP address of the Sonos system.</p><p>Relevant new tcpdumps:</p><pre class="ql-syntax" spellcheck="false"># SONOS APP TESTING
jarylc@RT-AX88U:/jffs/addons/YazFi.d/userscripts.d# tcpdump -i any src 172.27.15.3 and dst 172.27.14.98
08:23:59.721602 wl0.1 In&nbsp; IP 172.27.15.3.34242 &gt; 172.27.14.98.upnp: UDP, length 741
08:23:59.721669 br0&nbsp;&nbsp; Out IP 172.27.15.3.34242 &gt; 172.27.14.98.upnp: UDP, length 741
08:23:59.721678 eth7&nbsp; Out IP 172.27.15.3.34242 &gt; 172.27.14.98.upnp: UDP, length 741
08:23:59.870068 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [S.], seq 165454314, ack 134039494, win 28960, options [mss 1460,sackOK,TS val 355714560 ecr 4169303348,nop,wscale 7], length 0
08:23:59.870090 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [S.], seq 165454314, ack 134039494, win 28960, options [mss 1460,sackOK,TS val 355714560 ecr 4169303348,nop,wscale 7], length 0
08:23:59.870093 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [S.], seq 165454314, ack 134039494, win 28960, options [mss 1460,sackOK,TS val 355714560 ecr 4169303348,nop,wscale 7], length 0
08:23:59.926271 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 0
08:23:59.926303 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 0
08:23:59.926307 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 0
08:23:59.926275 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 1:63, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 62
08:23:59.926330 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 1:63, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 62
08:23:59.926333 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 1:63, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 62
08:23:59.926280 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], seq 63:1511, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 1448
08:23:59.926357 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], seq 63:1511, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 1448
08:23:59.926361 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [.], seq 63:1511, ack 157, win 235, options [nop,nop,TS val 355714574 ecr 4169303497], length 1448
08:24:01.100909 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 2093:2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 31
08:24:01.100937 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 2093:2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 31
08:24:01.100942 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [P.], seq 2093:2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 31
08:24:01.100913 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 0
08:24:01.100960 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 0
08:24:01.100963 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714868 ecr 4169304590], length 0
08:24:01.190110 wl0.1 In&nbsp; IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714891 ecr 4169304590], length 0
08:24:01.190127 br0&nbsp;&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714891 ecr 4169304590], length 0
08:24:01.190130 eth7&nbsp; Out IP 172.27.15.3.1443 &gt; 172.27.14.98.50146: Flags [F.], seq 2124, ack 476, win 243, options [nop,nop,TS val 355714891 ecr 4169304590], length 0
08:25:01.196936 wl0.1 In&nbsp; IP 172.27.15.3.33005 &gt; 172.27.14.98.57454: UDP, length 741
08:25:03.196969 br0&nbsp;&nbsp; Out IP 172.27.15.3.33005 &gt; 172.27.14.98.57454: UDP, length 741
08:25:05.196976 eth7&nbsp; Out IP 172.27.15.3.33005 &gt; 172.27.14.98.57454: UDP, length 741

# AIRPLAY TESTING
jarylc@RT-AX88U-0BC0:/tmp/home/root# tcpdump -i any src 172.27.15.3 and dst 172.27.14.110
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
08:59:34.272732 wl0.1 In&nbsp; IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [S.], seq 361871852, ack 1905326041, win 28960, options [mss 1460,sackOK,TS val 356248160 ecr 2914930491,nop,wscale 7], length 0
08:59:34.272761 br0&nbsp;&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [S.], seq 361871852, ack 1905326041, win 28960, options [mss 1460,sackOK,TS val 356248160 ecr 2914930491,nop,wscale 7], length 0
08:59:34.272769 eth7&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [S.], seq 361871852, ack 1905326041, win 28960, options [mss 1460,sackOK,TS val 356248160 ecr 2914930491,nop,wscale 7], length 0
08:59:34.284023 wl0.1 In&nbsp; IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [.], ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 0
08:59:34.284038 br0&nbsp;&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [.], ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 0
08:59:34.284041 eth7&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [.], ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 0
08:59:34.288263 wl0.1 In&nbsp; IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [P.], seq 1:977, ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 976
08:59:34.288281 br0&nbsp;&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [P.], seq 1:977, ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 976
08:59:34.288285 eth7&nbsp; Out IP 172.27.15.3.bbs &gt; 172.27.14.110.53646: Flags [P.], seq 1:977, ack 281, win 235, options [nop,nop,TS val 356248164 ecr 2914930539], length 976
08:59:35.333401 wl0.1 In&nbsp; IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 112
08:59:35.333430 br0&nbsp;&nbsp; Out IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 112
08:59:35.333435 eth7&nbsp; Out IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 112
08:59:35.333405 wl0.1 In&nbsp; IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 142
08:59:35.333452 br0&nbsp;&nbsp; Out IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 142
08:59:35.333455 eth7&nbsp; Out IP 172.27.15.3 &gt; 172.27.14.110: ICMP 172.27.15.3 udp port 320 unreachable, length 142
08:59:35.333408 wl0.1 In&nbsp; IP 172.27.15.3.46785 &gt; 172.27.14.110.53647: Flags [S.], seq 4199358917, ack 972859376, win 28960, options [mss 1460,sackOK,TS val 356248426 ecr 2879974157,nop,wscale 7], length 0
08:59:35.333474 br0&nbsp;&nbsp; Out IP 172.27.15.3.46785 &gt; 172.27.14.110.53647: Flags [S.], seq 4199358917, ack 972859376, win 28960, options [mss 1460,sackOK,TS val 356248426 ecr 2879974157,nop,wscale 7], length 0
08:59:35.333477 eth7&nbsp; Out IP 172.27.15.3.46785 &gt; 172.27.14.110.53647: Flags [S.], seq 4199358917, ack 972859376, win 28960, options [mss 1460,sackOK,TS val 356248426 ecr 2879974157,nop,wscale 7], length 0

# UPDATE TESTING
jarylc@RT-AX88U-0BC0:/tmp/home/root# tcpdump -i any src 172.27.15.3 or src 172.27.15.1 or src 172.27.15.3
08:22:01.901621 wl0.1 In&nbsp; IP 172.27.15.3.krb524 &gt; 172.27.14.98.35390: Flags [R.], seq 0, ack 3400968069, win 0, length 0
08:22:01.901646 br0&nbsp;&nbsp; Out IP 172.27.15.3.krb524 &gt; 172.27.14.98.35390: Flags [R.], seq 0, ack 1, win 0, length 0
08:22:01.901653 eth7&nbsp; Out IP 172.27.15.3.krb524 &gt; 172.27.14.98.35390: Flags [R.], seq 0, ack 1, win 0, length 0
</pre><p>Overall, my latest script looks like this now:</p><pre class="ql-syntax" spellcheck="false">HOME_ASSISTANT='172.27.14.254'
MEDIA_RANGE='172.27.15.2-172.27.15.20'
iptables -I YazFiFORWARD -i wl0.1 -o br0 -d ${HOME_ASSISTANT} -p tcp -m multiport --dports 1400,8123 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -m iprange --src-range "${MEDIA_RANGE}" -p tcp --dport 32768:65535 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -m iprange --src-range "${MEDIA_RANGE}" -p udp -m multiport --dports 319,320,1900 -j ACCEPT
iptables -I YazFiFORWARD -i wl0.1 -o br0 -m iprange --src-range "${MEDIA_RANGE}" -p tcp -m multiport --sports 1400,1443,4444,7000 -j ACCEPT
</pre><blockquote>End of update here, original post follows</blockquote><p>Most recently I've discovered that <a href="https://www.asuswrt-merlin.net/" rel="noopener noreferrer" target="_blank">Asuswrt-Merlin (community firmware for Asus routers)</a> has a terminal UI interface `<a href="https://github.com/RMerl/asuswrt-merlin.ng/wiki/AMTM" rel="noopener noreferrer" target="_blank">amtm</a>`.</p><p>Within it there is a project called <a href="https://github.com/jackyaz/YazFi" rel="noopener noreferrer" target="_blank">Yazfi by @jackyaz</a> which provides a "feature expansion of guest Wi-Fi networks on AsusWRT-Merlin".</p><p>I've always wanted to move all my smart devices to another VLAN with only pinhole access to my main LAN. Thanks to Yazfi, the effort is reduced tremendously trying to configure these while leveraging on the Guest Network functionality of Asus Routers. With that, I started my project to reformat my Asus Router, rotating my Wi-Fi passwords and start moving everything over.</p><p>I did however encounter some issues with discovering Sonos devices on the mobile app even with `One way to guest` enabled, it was either totally not discovering or very slow, and decided to start digging into it.</p><h2>Investigation</h2><blockquote>This section explains how I investigated how the Sonos works in my network under the hood and you can skip to the Solution section below if you only care about that!</blockquote><h3>Temporarily allowing two-way traffic between LAN and Guest Network</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/72mN-cdfjph/image-5ac9a50702c7676fa5aa8c61de1a85a3.png"></p><p>Since we are investigating how Sonos operates, I temporarily allowed two-way communication between the 2 networks in Yazfi configuration.</p><h3>Enabling multicast routing</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/72mN-cdfjph/image-57274ae829415195afe92f3d8103d890.png"></p><p>Since various online sources says that Sonos uses <a href="https://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol" rel="noopener noreferrer" target="_blank">Simple Service Discovery Protocol (SSDP)</a> and <a href="https://en.wikipedia.org/wiki/Multicast_DNS" rel="noopener noreferrer" target="_blank">multicast DNS (mDNS)</a> to support discoverability, for good measure, I enabled multicast routing under LAN &gt; IPTV &gt; Enable multicast routing</p><p>However I'm not 100% sure there is a need for this.</p><h3>Installing entware and tcpdump on my router</h3><p>You can <a href="https://github.com/RMerl/asuswrt-merlin.ng/wiki/Entware" rel="noopener noreferrer" target="_blank">follow these instructions to install entware on your router</a>.</p><p>After installationof entware is complete, it's as simple as</p><pre class="ql-syntax" spellcheck="false">jarylc@RT-AX88U:/tmp/home/root# opkg install tcpdump
</pre><h3>Running tcpdump</h3><p>After snooping around the UI and the Sonos App, the private IP addresses of the relevant devices are the following:</p><ul><li>My Android phone: 172.27.14.98 (172.27.14.x is my main LAN)</li><li>One of my Sonos devices: 172.27.15.3 (172.27.15.x is my smart devices Guest network)</li></ul><p>I limited the snooping to UDP packets only as that is what SSDP and mDNS primarily uses</p><pre class="ql-syntax" spellcheck="false">jarylc@RT-AX88U:/jffs/addons/YazFi.d/userscripts.d# tcpdump -i any 'udp and (src 172.27.15.3 or src 172.27.14.98)'
09:26:30.044460 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:30.044469 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:30.044433 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:30.044582 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:30.044589 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:30.044443 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:30.045267 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.045274 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.045259 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.045539 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.045544 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.045530 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.048134 wl0.4 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:30.048139 wl0.1 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:30.048125 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:30.227307 wl0.1 M&nbsp;&nbsp; IP 172.27.15.3.5353 &gt; mdns.mcast.net.5353: 0*- [0q] 1/0/6 PTR Sonos-XXXXXXXXXXXX@Media Room._sonos._tcp.local. (676)
09:26:30.266863 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.266871 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.266828 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:30.266945 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.266950 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.266840 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:30.580416 wl0.1 In&nbsp; IP 172.27.15.3.37360 &gt; 172.27.14.98.37972: UDP, length 741
09:26:30.580456 br0&nbsp;&nbsp; Out IP 172.27.15.3.37360 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.072123 wl0.4 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:31.072128 wl0.1 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:31.071869 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:31.072203 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:31.072208 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:31.071885 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 200
09:26:31.072269 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:31.072274 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:31.071892 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 200
09:26:31.309102 wl0.1 In&nbsp; IP 172.27.15.3.38139 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.309125 br0&nbsp;&nbsp; Out IP 172.27.15.3.38139 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.343765 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:31.343773 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:31.343737 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:31.343853 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:31.343858 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:31.343748 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:31.692594 wl0.1 In&nbsp; IP 172.27.15.3.38570 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.692617 br0&nbsp;&nbsp; Out IP 172.27.15.3.38570 &gt; 172.27.14.98.37972: UDP, length 741
09:26:32.271746 wl0.4 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:32.271753 wl0.1 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:32.271598 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 1 PTR (QU)? _sonos._tcp.local. (35)
09:26:32.278203 eth7&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:32.278226 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:32.278232 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:32.278203 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:32.278296 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:32.278301 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:32.278214 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:32.465141 wl0.1 M&nbsp;&nbsp; IP 172.27.15.3.5353 &gt; mdns.mcast.net.5353: 0*- [0q] 1/0/6 PTR Sonos-XXXXXXXXXXXX@Media Room._sonos._tcp.local. (676)
09:26:32.701778 wl0.1 In&nbsp; IP 172.27.15.3.41457 &gt; 172.27.14.98.37972: UDP, length 741
09:26:32.701801 br0&nbsp;&nbsp; Out IP 172.27.15.3.41457 &gt; 172.27.14.98.37972: UDP, length 741
09:26:33.215347 wl0.4 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:33.215355 wl0.1 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:33.215331 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 2 PTR (QM)? _sonos._tcp.local. (35)
09:26:33.905757 wl0.4 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:33.905763 wl0.1 Out IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:33.905736 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 239.255.255.250.upnp: UDP, length 254
09:26:33.905846 wl0.4 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:33.905851 wl0.1 Out IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:33.905744 br0&nbsp;&nbsp; B&nbsp;&nbsp; IP 172.27.14.98.37972 &gt; 255.255.255.255.upnp: UDP, length 254
09:26:34.034082 wl0.1 In&nbsp; IP 172.27.15.3.59807 &gt; 172.27.14.98.37972: UDP, length 741
09:26:34.034122 br0&nbsp;&nbsp; Out IP 172.27.15.3.59807 &gt; 172.27.14.98.37972: UDP, length 741
09:26:34.220437 wl0.4 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 3 PTR (QM)? _sonos._tcp.local. (35)
09:26:34.220444 wl0.1 Out IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 3 PTR (QM)? _sonos._tcp.local. (35)
09:26:34.220422 br0&nbsp;&nbsp; M&nbsp;&nbsp; IP 172.27.14.98.5353 &gt; mdns.mcast.net.5353: 3 PTR (QM)? _sonos._tcp.local. (35)
09:26:34.387343 wl0.1 M&nbsp;&nbsp; IP 172.27.15.3.5353 &gt; mdns.mcast.net.5353: 0*- [0q] 1/0/6 PTR Sonos-XXXXXXXXXXXX@Media Room._sonos._tcp.local. (676)
</pre><h3>The problem</h3><pre class="ql-syntax" spellcheck="false">09:26:30.580416 wl0.1 In&nbsp; IP 172.27.15.3.37360 &gt; 172.27.14.98.37972: UDP, length 741
09:26:30.580456 br0&nbsp;&nbsp; Out IP 172.27.15.3.37360 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.309102 wl0.1 In&nbsp; IP 172.27.15.3.38139 &gt; 172.27.14.98.37972: UDP, length 741
09:26:31.309125 br0&nbsp;&nbsp; Out IP 172.27.15.3.38139 &gt; 172.27.14.98.37972: UDP, length 741
09:26:32.701778 wl0.1 In&nbsp; IP 172.27.15.3.41457 &gt; 172.27.14.98.37972: UDP, length 741
09:26:32.701801 br0&nbsp;&nbsp; Out IP 172.27.15.3.41457 &gt; 172.27.14.98.37972: UDP, length 741
09:26:34.034082 wl0.1 In&nbsp; IP 172.27.15.3.59807 &gt; 172.27.14.98.37972: UDP, length 741
09:26:34.034122 br0&nbsp;&nbsp; Out IP 172.27.15.3.59807 &gt; 172.27.14.98.37972: UDP, length 741
</pre><p>Filtering out all the noise, I noticed that the Sonos device was trying to send a UDP packet from the ephemeral UDP ports (range 32768-65535) to my mobile device.</p><p>This is actually not surprising as that is how SSDP works as the device would switch to unicast after being discovered. Prior to this, I was not very familiar with SSDP and did not know that.</p><p>I noticed in a previous tcpdump before enabling two-way communication that this was being blocked:</p><pre class="ql-syntax" spellcheck="false">00:13:65.371058 wl0.1 In&nbsp; IP 172.27.15.3.46814 &gt; 172.27.14.98.49972: UDP, length 737
00:13:54.373059 wl0.1 Out IP 172.27.15.1 &gt; 172.27.15.3: ICMP 172.27.14.98 udp port 49972 unreachable, length 556
</pre><h3><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/image-f00ed809361ceabd0d29380d07f2d3d5.png"></h3><h2>Solution</h2><h3>Adding custom firewall allow rules to Yazfi to allow this UDP traffic from Sonos devices</h3><p><a href="https://github.com/jackyaz/YazFi?tab=readme-ov-file#custom-firewall-rules" rel="noopener noreferrer" target="_blank">Yazfi allows additional iptables rules by adding a shell script with iptables commands to any file in `/jffs/addons/YazFi.d/userscripts.d/`</a></p><p>With that I created a file called `myscript.sh` within the folder and placed this in the file to allow UDP traffic on the ephemeral ports (range 32768-65535) originating from the Sonos device to flow back to the main LAN</p><pre class="ql-syntax" spellcheck="false">#!/bin/sh
iptables -I YazFiFORWARD -i wl0.1 -o br0 -s 172.27.15.3 -p udp --dport 32768:65535 -j ACCEPT
</pre><ul><li>`-I YazFiForward` - adds the rule to the FORWARD chain in the YazFi table</li><li>`-i wl0.1` - <strong>make sure to change this to your actual Guest Wi-Fi interface</strong></li><li>`-o br0` - packets moving to the bridge interface</li><li>`-s 172.27.15.3` - <strong>make sure to change this to your actual Sonos device's IP address</strong></li><li>`-p udp` - UDP packets only</li><li>`--dport 32768:65536` - ephemeral UDP port ranges only</li><li>`-j ACCEPT` - allow packets filtered by this rule</li></ul><blockquote>You can duplicate that iptables line for as many Sonos device you have and their respective IPs, or you can also use `-srcrange` (i.e. `-m iprange --src-range 172.27.15.2-172.27.15.9`) if you have configured continuous static DHCP IPs for them.</blockquote><p>Be reminded to `chmod +x` the file afterwards</p><pre class="ql-syntax" spellcheck="false">jarylc@RT-AX88U:/jffs/addons/YazFi.d/userscripts.d# chmod +x myscript.sh
</pre><h3>Reverting to only allowing one-way (LAN to Guest) communication</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/72mN-cdfjph/image-f772aec66b26bfe8d57b92437818a68f.png"></p><p>During investigation, I allowed two-way communication, so I am reverting it to one-way only in the Yazfi configuration.</p><h3>Testing the Sonos app again</h3><p>Sure enough, after allowing the UDP packet to flow back allows the Sonos app to be snappy again.</p><h2>Bonus</h2><h3>HomeAssistant</h3><p>I integrated my Sonos systems with HomeAssistant as well, it is also <a href="https://www.home-assistant.io/integrations/sonos/#network-requirements" rel="noopener noreferrer" target="_blank">recommended by them to enable communication back via port 1400</a></p><pre class="ql-syntax" spellcheck="false">iptables -I YazFiFORWARD -i wl0.1 -o br0 -d 172.27.14.100 -p tcp --dport 1400 -j ACCEPT
</pre><ul><li>`-I YazFiForward` - adds the rule to the FORWARD chain in the YazFi table</li><li>`-i wl0.1` - <strong>make sure to change this to your actual Guest Wi-Fi interface</strong></li><li>`-o br0` - packets moving to the bridge interface</li><li>`-s 172.27.14.100` - <strong>make sure to change this to your actual HomeAssistant's IP address</strong></li><li>`-p tcp` - TCP packets only</li><li>`-dport 1400` - port 1400 only</li><li>`-j ACCEPT` - allow packets filtered by this rule</li></ul><p>It is also highly recommended to allow the Sonos systems to communicate with HomeAssistant directly (especially to use local media storage if you have any)</p><pre class="ql-syntax" spellcheck="false">iptables -I YazFiFORWARD -i wl0.1 -o br0 -d 172.27.14.100 -p tcp --dport 8123 -j ACCEPT
</pre><ul><li>`-I YazFiForward` - adds the rule to the FORWARD chain in the YazFi table</li><li>`-i wl0.1` - <strong>make sure to change this to your actual Guest Wi-Fi interface</strong></li><li>`-o br0` - packets moving to the bridge interface</li><li>`-s 172.27.14.100` - <strong>make sure to change this to your actual HomeAssistant's IP address</strong></li><li>`-p tcp` - TCP packets only</li><li>`-dport 8123` - <strong>make sure to change this to your actual HomeAssistant's port</strong></li><li>`-j ACCEPT` - allow packets filtered by this rule</li></ul>]]>
    </description>
    <link>https://kb.jarylchng.com/i/sonos-asus-router-merlin-yazfi-guest-network-wit-72mN-cdfjph/</link>
    <itunes:episodeType>full</itunes:episodeType>
    <enclosure url="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/image-f00ed809361ceabd0d29380d07f2d3d5.png" type="image/png" length="206925"/>
  </item>
  <item>
    <title>n8n &amp; Authelia - Bypass n8n native login page using Trusted Header Single Sign-On and custom hooks configuration</title>
    <guid>sNRmS-7j5u1</guid>
    <pubDate>Thu, 18 Apr 2024 03:55:34 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>From v1.110.1 onwards, please use this snippet instead</h2><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('router/lib/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app.router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (!user.role) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; user.role = {}
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><h2>From v1.87.0 onwards, please use this snippet instead</h2><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('router/lib/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app.router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><p><br></p><p>I use Authelia as my SSO solution for my home services, however after n8n released v1.0, they seem to have removed Basic Authentication and locked SAML and OIDC behind an enterprise license. They seem to have removed the options to disable user management completely as well. I do believe they have never supported <a href="https://www.authelia.com/integration/trusted-header-sso/introduction/" rel="noopener noreferrer" target="_blank">Trusted Header SSO</a> even before v1.0.</p><p>My main goal was to avoid the need to key in 2 sets of credentials every time (Authelia and n8n), and to have a more seamless SSO experience by bypassing the n8n login page.</p><h2>Solution</h2><blockquote>I would not have easily found this solution without the help of <a href="https://community.n8n.io/t/self-hosted-user-management/30520/4" rel="noopener noreferrer" target="_blank">@MutedJam and @netroy on the N8N community forum</a>.</blockquote><p>With the help of the `n8n.ready` <a href="https://docs.n8n.io/embed/configuration/#backend-hooks" rel="noopener noreferrer" target="_blank">backend external hook</a>, we are able to intercept and add another middleware to issue a JWT authentication token when the header is detected.</p><h2>Assumptions</h2><ul><li>n8n is hosted using Docker</li><li><a href="https://www.authelia.com/integration/trusted-header-sso/introduction/#forwarding-the-response-headers" rel="noopener noreferrer" target="_blank">Your reverse proxy is setting and forwarding the Remote-Email header from Authelia</a></li><li>n8n is already secured by Authelia on a reverse proxy</li></ul><blockquote>Remote-Email is used by majority of the reverse proxy guides on Authelia documentation, if you did not customize much, it should be the same.</blockquote><h2>Instructions</h2><h3>Ensure your user's e-mail matches Authelia</h3><p>Make sure to change it to match your LDAP store or <a href="https://github.com/authelia/authelia/blob/master/examples/compose/lite/authelia/users_database.yml#L15" rel="noopener noreferrer" target="_blank">flat file</a> depending on your Authelia configuration.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/sNRmS-7j5u1/image-65fff4e06bf50ca4cf8cff012be341ec.png"></p><h3>Creating the External Hook file</h3><p>Create a file named `hooks.js` and place this file somewhere your n8n instance can reach.</p><p>In the case of the official Docker image, it is where you mounted `/home/node/` to, and for the sake of this guide, I will place it at where it would be effectively mounted at `/home/node/.n8n/hooks.js`.</p><p>Copy and paste the following into the file:</p><pre class="ql-syntax" spellcheck="false">const { dirname, resolve } = require('path')
const Layer = require('express/lib/router/layer')
const { issueCookie } = require(resolve(dirname(require.resolve('n8n')), 'auth/jwt'))
const ignoreAuthRegexp = /^\/(assets|healthz|webhook|rest\/oauth2-credential)/
module.exports = {
    n8n: {
        ready: [
            async function ({ app }, config) {
                const { stack } = app._router
                const index = stack.findIndex((l) =&gt; l.name === 'cookieParser')
                stack.splice(index + 1, 0, new Layer('/', {
                    strict: false,
                    end: false
                }, async (req, res, next) =&gt; {
                    // skip if URL is ignored
                    if (ignoreAuthRegexp.test(req.url)) return next()

                    // skip if user management is not set up yet
                    if (!config.get('userManagement.isInstanceOwnerSetUp', false)) return next()

                    // skip if cookie already exists
                    if (req.cookies?.['n8n-auth']) return next()

                    // if N8N_FORWARD_AUTH_HEADER is not set, skip
                    if (!process.env.N8N_FORWARD_AUTH_HEADER) return next()

                    // if N8N_FORWARD_AUTH_HEADER header is not found, skip
                    const email = req.headers[process.env.N8N_FORWARD_AUTH_HEADER.toLowerCase()]
                    if (!email) return next()

                    // search for user with email
                    const user = await this.dbCollections.User.findOneBy({email})
                    if (!user) {
                        res.statusCode = 401
                        res.end(`User ${email} not found, please have an admin invite the user first.`)
                        return
                    }

                    // issue cookie if all is OK
                    issueCookie(res, user)
                    return next()
                }))
            },
        ],
    },
}
</pre><h3>Configure extra Docker container environment variables</h3><p>Ensure to append a 2 new environment variable to your n8n instance and re-create the container depending on how you deployed it:</p><pre class="ql-syntax" spellcheck="false">EXTERNAL_HOOK_FILES=/home/node/.n8n/hooks.js
N8N_FORWARD_AUTH_HEADER=Remote-Email
</pre><p>Reference: <a href="https://docs.n8n.io/embed/configuration/#registering-hooks" rel="noopener noreferrer" target="_blank">Registering hooks with EXTERNAL_HOOK_FILES</a></p><h2>Precautions</h2><p>Make sure to secure n8n properly by making it only accessible via your reverse proxy. Do not allow direct access as any user would be able to impersonate someone by sending a custom `Remote-Email` header if so.</p><h2>Afterword</h2><p>It seems that it is likely possible to re-create a whole SAML/OIDC set-up just by implementing it in the hook, but it is definitely a project on its own and definitely out of scope for this post. However, you are free to customize the script however you wish according to your needs. There is likely a way to automatically create users as well, but I have yet to really dig into the n8n codes.</p><p>I've really only tested this on Nginx, but it should be reverse proxy agnostic.</p><p>If you would like another workflow automation software that does not gate OIDC (at least for the first 10 users) with a license, you could look at <a href="https://www.windmill.dev/" rel="noopener noreferrer" target="_blank">Windmill</a>.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/n8n-and-authelia-bypass-n8n-native-login-page-usin-sNRmS-7j5u1/</link>
    <itunes:episodeType>full</itunes:episodeType>
    <enclosure url="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/video-b978a4a52c0ab6c33e8a8a75f1851add.mp4" type="video/mp4" length="174006"/>
    <itunes:duration>00:00:04</itunes:duration>
  </item>
  <item>
    <title>PowerShell - Lethal Company BepInEx one-click mods synchronizer powered by S3 buckets (Cloudflare R2) and RClone</title>
    <guid>D0SMMNp96yY</guid>
    <pubDate>Sun, 14 Apr 2024 07:55:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>A few months ago, I was very active in playing Lethal Company with my peers. With the introduction of the mods from the modding community, it enhanced gameplay tremendously and me and my friends had a lot of fun.</p><p>However, my friends are not very tech-savvy, and I did not want them to go through the trouble of installing r2modman, traversing the UI, typing the export UUID and getting my mods every single time I update. But I would like to continue to manage my mods using r2modman as it makes updating mods a breeze.</p><p>I had a few friends who, unfortunately, were working with data limits as well and could not keep downloading a zip file containing hundreds of megabytes worth of all my mods.</p><p>Most of my friends are on Windows as well, so I had to cater for that fact.</p><h2>Solution</h2><p>A PowerShell script that allows a one-click automatic mod update that also does delta downloads (only downloads changed files).</p><p>Said PowerShell script is powered by an S3 bucket, in my case Cloudflare R2, and RClone.</p><h2>Prerequisites</h2><ol><li>A Cloudflare account</li><li>A payment method to enable the use of R2 (pay-per-usage)</li><li>r2modman installed with a few Lethal Company mods already added</li></ol><h2>Instructions</h2><h3>Add the Cloudflare R2 subscription on your Cloudflare dashboard</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-4da53a80abf3b1ab1da81c77e384798b.png"></p><p>Make sure to take note of the limits and keep it below that to stay on the free tier. Unless you have a lot of mods or a lot of friends, it's unlikely to hit the free-tier limits with just this.</p><h3>Create an R2 bucket</h3><p>After subscribing, you can now go ahead and create an R2 bucket, it is recommended to select a location hint to a region near yourself and your friends.</p><p>In this case, we are calling the bucket `lethalcompany`.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-608d29889776dddc5e5314182c03230e.png"></p><h3>Generating a read-only and a read-write API tokens for the bucket</h3><p>In our case, we would want 2 types of keys, a read-only one that will be sent to your friends and the other for you to upload mods.</p><p>You can manage your API tokens here on the right side of the R2 overview page as of the time of posting:</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-2d182f34d7d47480bcec57da977ebe32.png"></p><blockquote>Note: Make sure to note down the Access ID here for later.</blockquote><p>You would want to create a read-only token similar to the following:</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-6d350fed7c5957da5c920f8cca89d2f1.png"></p><blockquote>Note: Make sure to note down the Access Key ID and Secret Access Key after creating and note down this is for read-only!</blockquote><p>And a read-write token as follows:</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-5e34e92861891186df5e7f9b7a5c2b5f.png"></p><blockquote>Note: Make sure to note down the Access Key ID and Secret Access Key after creating and note down this is for read-write!</blockquote><h3>Find your Lethal Company r2modman profile folder</h3><p>Open r2modman to Lethal Company and open the Settings section</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-174f2a65940e116bd97dba3c0e3c1a86.png"></p><p>From there you would want to click on `Browse profile folder` and it would open up your profile folder in Windows Explorer</p><h3>Download RClone</h3><p>You have a choice of downloading from 2 official places:</p><ol><li><a href="https://rclone.org/downloads/" rel="noopener noreferrer" target="_blank">Official website</a></li><li><a href="https://github.com/rclone/rclone/releases" rel="noopener noreferrer" target="_blank">Github</a></li></ol><p>In most cases, you should be downloading the Windows Intel/AMD - 64 Bit build.</p><p>After downloading, open up the zip archive and traverse into the folder until you see `rclone.exe`</p><p>Drag and drop `rclone.exe` onto the Lethal Company r2modman profile folder you've found above</p><h3>Create RClone configuration for uploading</h3><p>In your Lethal Company r2modman profile folder, beside the new `rclone.exe`, create a file named: `z_rclone.conf`</p><p>Copy and paste the following into `z_rclone.conf`:</p><pre class="ql-syntax" spellcheck="false">[r2]
type = s3
provider = Cloudflare
access_key_id = PASTE_OVER_YOUR_READ_WRITE_ACCESS_KEY_HERE
secret_access_key = PASTE_OVER_YOUR_READ_WRITE_SECRET_ACCESS_KEY_HERE
endpoint = https://PASTE_OVER_YOUR_R2_ID_HERE.r2.cloudflarestorage.com
</pre><p>Make sure to replace the PASTE_OVER values according to your <strong>read-write</strong> R2 token and Account ID.</p><blockquote>Note: The `z_` prefix is important to set up ignore rules, but can be customized later on.</blockquote><h3>Create mods uploader script</h3><p>In the same folder, create a file named: `z_upload.bat`</p><p>Copy and paste the following into `z_upload.bat`:</p><pre class="ql-syntax" spellcheck="false"># 2&gt;NUL &amp; @CLS &amp; PUSHD "%~dp0" &amp; "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -nol -nop -ep bypass "[IO.File]::ReadAllText('%~f0')|iex" &amp; POPD &amp; EXIT /B

if (Test-Path -Path '.\Lethal Company.exe') {
&nbsp; &nbsp; Write-Host "Error: Lethal Company.exe detected! Do not run the uploader here!"
&nbsp; &nbsp; cmd /c pause
&nbsp; &nbsp; Exit
}

$ErrorActionPreference = "Inquire"

Write-Host 'Uploading...'
./rclone --config ./z_rclone.conf -v sync --checkers 12 --transfers 12 --update --checksum --use-server-modtime --fast-list --delete-excluded . r2:lethalcompany --exclude z_rclone.conf --exclude-from z_exclude.txt

Write-Host "Mod upload complete!"
cmd /c pause
</pre><blockquote>Note: The `z_` prefix is important to set up ignore rules, but can be customized later on.</blockquote><h3>Create files to exclude list</h3><p>In the same folder, create a file named: `z_exclude.txt`</p><p>Copy and paste the following into `z_exclude.txt`:</p><pre class="ql-syntax" spellcheck="false">z_*
mods.yml
LogOutput.log
*.zip
_state/**
BepInEx/cache/**
BepInEx/plugins/**/*.old
BepInEx/plugins/**/icon.png
BepInEx/plugins/**/manifest.json
BepInEx/plugins/**/README
BepInEx/plugins/**/CHANGELOG
BepInEx/plugins/**/LICENSE
BepInEx/plugins/**/INSTALLING
BepInEx/plugins/**/*.md
BepInEx/plugins/**/*.txt
</pre><blockquote>Note: The `z_` prefix is important to set up ignore rules, but can be customized later on.</blockquote><h3>Run the upload script</h3><p>Double-click on `z_upload.bat` and run it to upload your mods, if you have configured your `rclone.conf` file properly, it should get uploaded smoothly.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-3a4eb03abdcada6207b8d969551fa4fa.png"></p><blockquote>Note: Your output should be longer than this as you are uploading mods from scratch</blockquote><p>Now that your mods are all uploaded, we can set up the mod downloader configuration and script to send to your friends</p><h3>Create RClone configuration for downloading</h3><p>In the same folder, create a file named: `rclone.conf`</p><p>Copy and paste the following into `rclone.conf`:</p><pre class="ql-syntax" spellcheck="false">[r2]
type = s3
provider = Cloudflare
access_key_id = PASTE_OVER_YOUR_READ_ONLY_ACCESS_KEY_HERE
secret_access_key = PASTE_OVER_YOUR_READ_ONLY_SECRET_ACCESS_KEY_HERE
endpoint = https://PASTE_OVER_YOUR_R2_ID_HERE.r2.cloudflarestorage.com
</pre><p>Make sure to replace the PASTE_OVER values according to your <strong>read-only </strong>R2 token and Account ID.</p><h3>Create the mod downloader script</h3><p>In the same folder, create a file named: `update_mods.bat`</p><p>Copy and paste the following into `update_mods.bat`:</p><pre class="ql-syntax" spellcheck="false"># 2&gt;NUL &amp; @CLS &amp; PUSHD "%~dp0" &amp; "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -nol -nop -ep bypass "[IO.File]::ReadAllText('%~f0')|iex" &amp; POPD &amp; EXIT /B

if (-not(Test-Path -Path '.\Lethal Company.exe')) {
&nbsp; &nbsp; Write-Host "Error: Make sure this file is in the same folder as Lethal Company.exe before running!"
&nbsp; &nbsp; cmd /c pause
&nbsp; &nbsp; Exit
}

$ErrorActionPreference = "Inquire"

Write-Host '(1/2) Syncing base files...'
./rclone.exe --config ./rclone.conf -P --stats-one-line copy --checkers 12 --transfers 12 --checksum --use-server-modtime --fast-list r2:lethalcompany . --exclude BepInEx/

Write-Host '(2/2) Syncing mods...'
./rclone.exe --config ./rclone.conf -P --stats-one-line sync --checkers 12 --transfers 12 --checksum --use-server-modtime --fast-list r2:lethalcompany/BepInEx/ ./BepInEx/

Write-Host "Mod update complete!"
cmd /c pause
</pre><h3>Test the mod downloader</h3><p>Open Steam and click on Lethal Company</p><p>Click on the gear icon on the right side</p><p>Traverse to `Manage` and click on `Browse local files` to open up the Lethal Company game folder on Windows Explorer</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-e10b737a15c4294419f4e41bea5b0fdc.png"></p><p>Copy `rclone.conf`, `rclone.exe` and `update_mods.bat` from your Lethal Company r2modman profile folder here.</p><p>Double click update_mods.bat and run it.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-5510f829629ebb284d63b7486e50fc7c.png"></p><p>If everything is configured well, the mods should download nicely.</p><h3>Ship the mod downloader to your friends</h3><p>Select `rclone.conf`, `rclone.exe` and `update_mods.bat`.</p><blockquote><strong>Important:</strong> do not select the files starting with `z_`! They are meant to be for you only.</blockquote><p>Right click any of the selected files, traverse to `Send to` and click on `Compressed (zipped) folder`</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/D0SMMNp96yY/image-a61c4c90fa92d5170ab67a27510ae3e0.png"></p><p>Go ahead and rename the zip file if needed and send it to your friends.</p><p>Instruct them to find the Lethal Company folder with the same steps as Test the mod downloader, extract the 3 files there and double-click `update_mods.bat` and run it.</p><p>They should observe the same results as you.</p><p>After which, the game can be launched and your friends will have the exact same mods as you!</p><h2>Afterword</h2><p>Note that a few antiviruses will flag rclone.exe as a virus as a false positive, you may need to add it to your antivirus scan exclusion list or just not use this at all.</p><p>Although the setup is lengthy, after everything, the mod update flow is really just a matter of (as seen in the demo video)</p><ol><li>updating mods with r2modman</li><li>z_upload.bat</li><li>get your friends to run update_mods.bat</li></ol><p>No need for traversing the r2modman UI at all and pasting UUIDs.</p><p>You may notice in my demo, my mod updater has a few more steps, I've excluded them as feel that they are out of scope for this post.</p><p>As we are already doing a lot of do-it-yourself, everything above is customizable, and you can add and remove features if you wish to do so, your imagination is the limit!</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/powershell-lethal-company-bepinex-one-click-mods-D0SMMNp96yY/</link>
    <itunes:episodeType>full</itunes:episodeType>
    <enclosure url="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/video-24bdd7921c9c801e2c88abb528af32c9.mp4" type="video/mp4" length="887187"/>
    <itunes:duration>00:00:12</itunes:duration>
  </item>
  <item>
    <title>Docker - Access Docker containers from the host via container names by synchronizing /etc/hosts</title>
    <guid>m3vvl9sdUjw</guid>
    <pubDate>Sun, 14 Apr 2024 06:04:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>When using Docker, containers within the same shared network are able to resolve one another via their hostnames. However, the same functionality is not replicated on the host of the Docker containers.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/m3vvl9sdUjw/image-ab670ddfb37199687af993c4359f53aa.png"></p><p>For personal reasons, I kept Nginx load balancer on the host and did not want to forward ports of containers, but I still wanted to reverse proxy my containers.</p><h2>Solution</h2><h3><a href="https://hub.docker.com/r/jarylc/docker-hosts-sync" rel="noopener noreferrer" target="_blank">https://hub.docker.com/r/jarylc/docker-hosts-sync</a></h3><p>Source: <a href="https://gitlab.com/jarylc/docker-hosts-sync" rel="noopener noreferrer" target="_blank">https://gitlab.com/jarylc/docker-hosts-sync</a></p><p>A simple Golang program that I created that updates the host's /etc/hosts file whenever the Docker containers are updated, capturing the private IPs of all the containers each time. Said program is also packaged in a Docker image, so it is easily appended to your own Docker system!</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/m3vvl9sdUjw/image-f3e9ca5601224962a12949fa7a60168b.png"></p><p>This image automatically mirrors /etc/hosts whenever your Docker containers are changed</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/m3vvl9sdUjw/image-1d47c873370e880f95d7503aa3d1fd9d.png"></p><h2>Alternatives</h2><h3><a href="https://hub.docker.com/r/jaschweder/hosts" rel="noopener noreferrer" target="_blank">jaschweder/hosts</a></h3><p>Not multi-architecture. Checks every 5 second intervals (not configurable), instead of on container creation and deletion. Big Docker image even when after compression 35mb vs 2.5mb.</p><h3><a href="https://hub.docker.com/r/itsziget/hosts-gen" rel="noopener noreferrer" target="_blank">itsziget/hosts-gen</a></h3><p>Not multi-architecture. Based off a <a href="https://hub.docker.com/r/jwilder/docker-gen" rel="noopener noreferrer" target="_blank">larger templating project</a> for more use-cases, 8mb vs 2.5mb.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/docker-access-docker-containers-from-the-host-vi-m3vvl9sdUjw/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux Docker - &quot;Native&quot; Jellyfin Push 2FA MFA with LLDAP, Duo and DuoAuthProxy</title>
    <guid>1MzzsuokbLV</guid>
    <pubDate>Fri, 12 Apr 2024 01:01:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>I wanted multifactor authentication on Jellyfin especially for my administrators which is unfortunately a missing feature natively.</p><p>Unfortunately configuring Authelia Forward Authentication or an SSO solution with OpenID Connect (OIDC) or Security Assertion Markup Language (SAML) would break native apps on non-PC devices due to drastically changing the authentication flow.</p><p>I wanted a solution that taps on Jellyfin's native authentication flow and only introduces a "pause" after clicking the log in button, which can be handled by any client, while waiting for the Duo push to be approved.</p><p>While this solution does not save a lot more space, I was inspired by <a href="https://forum.jellyfin.org/t-jellyfin-authentik-duo-2fa-solution-tutorial" rel="noopener noreferrer" target="_blank">Jellyfin, Authentik, DUO. 2FA solution</a> but wanted a smaller, non Authentik, alternative.</p><p>Also, Docker Hub images for <a href="https://duo.com/docs/authproxy-reference" rel="noopener noreferrer" target="_blank">DuoAuthProxy</a> were all outdated, so this was also an opportunity to bring it into my <a href="https://gitlab.com/users/jarylc/projects" rel="noopener noreferrer" target="_blank">list of automatically updating images powered by Gitlab CI/CD</a>.</p><p><a href="https://hub.docker.com/r/minimages/duoauthproxy" rel="noopener noreferrer" target="_blank">Just looking for the Alpine-based DuoAuthProxy docker image built by me? Click here!</a></p><p>Note: Since we will be accessing everything locally in this guide, even all within the docker network, I will not be going through setting up LDAP transit encryption (i.e. Secure LDAPS and StartTLS) as it would be out of scope.</p><h2>More Demos</h2><h3>Android app login video</h3><p> Your browser does not support the video element.</p><p><br></p><h2>Prerequisites</h2><ol><li><a href="https://jellyfin.org/" rel="noopener noreferrer" target="_blank">Jellyfin</a> on Docker set-up prior</li><li>A free/paid account with <a href="https://duo.com/" rel="noopener noreferrer" target="_blank">Duo</a> and access to the admin portal</li></ol><h2>Steps</h2><h3>Starting LLDAP</h3><p>Replace the following accordingly</p><ul><li>dc=example,dc=com (people normally follow their domain (i.e. example.com for this))</li><li>REPLACE_ME_WITH_YOUR_OWN_JWT_SECRET (this can be any long string really, string length is debatable and out of scope)</li><li>REPLACE_ME_WITH_YOUR_OWN_KEY_SEED (this can be any long string really, string length is debatable and out of scope)</li></ul><pre class="ql-syntax" spellcheck="false">docker run -d \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --name=LLDAP \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --network auth \
        -p 17170:17170 \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -e LLDAP_LDAP_BASE_DN=dc=example,dc=com \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -e LLDAP_JWT_SECRET=REPLACE_ME_WITH_YOUR_OWN_JWT_SECRET \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -e LLDAP_KEY_SEED=REPLACE_ME_WITH_YOUR_OWN_KEY_SEED \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -v /path/to/data:/data \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --restart unless-stopped \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; lldap/lldap:stable
</pre><p>Reference: <a href="https://github.com/lldap/lldap" rel="noopener noreferrer" target="_blank">https://github.com/lldap/lldap</a></p><h3>Configuring LLDAP</h3><p>Visit the frontend at port 17170 and create a service user for DuoAuthProxy accordingly, you can replace the values if needed.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-0453045526fe05a2d49b0bd950cbd409.png"></p><p>For the sake of this example, the password of the svc-duoauthproxy user is `password`</p><p>Make sure to add the user to either lldap_strict_readonly (read-only) or lldap_password_manager (allow password change on Jellyfin) groups</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-752a821b48ce5a1afb8cc3c730861dbc.png"></p><h3>Creating Duo application</h3><p>Access your <a href="https://admin.duosecurity.com" rel="noopener noreferrer" target="_blank">Duo admin console</a></p><p>Go to Applications -&gt; Protect an Application</p><p>Search for `proxy` and protect an `LDAP Proxy` application</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-19c285167451cd3104c2f320eebc5e0b.png"></p><p>Copy the credentials to prepare for the next step</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-93fcb6188216f32127a7ddf081b5718e.png"></p><h3>Configuring DuoAuthProxy</h3><p>If you are using <a href="https://hub.docker.com/r/minimages/duoauthproxy" rel="noopener noreferrer" target="_blank">my image</a>, it is necessary to craft the configuration file first</p><p>Replace the following keys accordingly</p><ul><li>bind_dn</li><li>service_account_username</li><li>service_account_password</li><li>search_dn</li><li>ikey</li><li>skey</li><li>api_host</li></ul><pre class="ql-syntax" spellcheck="false">; Complete documentation about the Duo Auth Proxy can be found here:
; https://duo.com/docs/authproxy_reference

[main]
log_stdout=true

[ad_client]
host=LLDAP
port=3890
auth_type=plain
bind_dn=uid=svc-duoauthproxy,ou=people,dc=example,dc=com
service_account_username=svc-duoauthproxy
service_account_password=password
search_dn=ou=people,dc=example,dc=com
username_attribute=uid
at_attribute=mail

[ldap_server_auto]
ikey=DIXXXXXXXXXXXXXXXXXX
skey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
api_host=api-XXXXXXXX.duosecurity.com
failmode=secure
client=ad_client
port=1812
exempt_primary_bind=false
exempt_ou_1=uid=svc-duoauthproxy,ou=people,dc=example,dc=com
</pre><p>Reference: <a href="https://duo.com/docs/authproxy_reference" rel="noopener noreferrer" target="_blank">https://duo.com/docs/authproxy_reference</a></p><h3>Starting DuoAuthProxy</h3><pre class="ql-syntax" spellcheck="false">docker run -d \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --name=DuoAuthProxy \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --network auth \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -v /path/to/authproxy.cfg:/app/conf/authproxy.cfg \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; --restart unless-stopped \
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; minimages/duoauthproxy
</pre><p>Feel free to use alternatives as well or even <a href="https://duo.com/docs/authproxy-reference#installation" rel="noopener noreferrer" target="_blank">host it natively or generate your own image using the official instructions</a>.</p><h3>Preparing Jellyfin</h3><p>I would assume you would already have Jellyfin set up, add Jellyfin to the auth network by re-running the container while adding `--network auth` argument to the docker run</p><h3>Installing LDAP Authentication Jellyfin Plugin</h3><p>Note: I'm currently using the unstable version of Jellyfin (10.9.0) as of posting, I had to use the unstable plugins repository.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-20a91c182831f4b80999ec519c7eb3c2.png"><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-87410bcb3ef2dfbc2be257927231d614.png"></p><p>Please be reminded to restart Jellyfin after installing the plugin.</p><h3>Setting up LDAP Authentication Jellyfin Plugin</h3><p>Set the following configuration parameters for LDAP Server Settings section</p><ul><li>LDAP Server: DuoAuthProxy</li><li>LDAP Port: 1812</li><li>Secure LDAP: unchecked</li><li>StartTLS: unchecked</li><li>Skip SSL/TLS Verification: checked</li><li>Allow Password Change: (optional, but ensure the service account have the lldap_password_manager group if checked)</li><li>LDAP Bind User: uid=svc-duoauthproxy,ou=people,dc=example,dc=com</li><li>LDAP Bind User Password: password</li><li>LDAP Base DN for searches: ou=people,dc=example,dc=com</li></ul><p>At this point click `Save and Test LDAP Server Settings` to test connectivity, it should pass if all your settings are good.</p><p>Set the following configuration parameters for LDAP User Settings section</p><ul><li>LDAP Search Filter: (uid=*)</li><li>LDAP Search Attributes: uid, mail</li><li>LDAP Uid Attribute: uid</li><li>LDAP Username Attribute: uid</li><li>LDAP Password Attribute: userPassword</li><li>LDAP Admin Filter: (memberof=cn=lldap_admin,ou=example,dc=com)</li></ul><p>Note you should change your LDAP Search Filter and LDAP Admin Filter according to your needs, reference: <a href="https://github.com/lldap/lldap/blob/main/example_configs/jellyfin.md" rel="noopener noreferrer" target="_blank">https://github.com/lldap/lldap/blob/main/example_configs/jellyfin.md</a></p><p>At this point click `Save and Test LDAP Filter Settings` and check if there are more than 0 users and admins found, it should if all your settings are good.</p><p>Afterwhich, enter `admin` in `Test Login Name` field and `Save Search Attribute Settings and Query User` button to do a final lookup check and save.</p><p>Finally, configure Jellyfin User Settings sections to your needs and don't forget to hit the big blue `Save` button.</p><h3>(If Jellyfin has existing users) Switch users' Authentication Provider to LDAP-Authentication</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/1MzzsuokbLV/image-fb7d2f5a7d9b7f612cdcc169dd69fce5.png"></p><h3>Test the new authentication flow</h3><p>The new authentication flow should be immediately active, you can log out and test it.</p><p>If not, you can try another restart of Jellyfin first.</p><h2>Afterword</h2><p>Of course, this guide will not be a one-size fit all like the Authentik solution was for me.</p><p>A lot of settings can and should be customized to your needs.</p><p>This guide only follows through the most basic setup.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-docker-native-jellyfin-push-2fa-mfa-with-1MzzsuokbLV/</link>
    <itunes:episodeType>full</itunes:episodeType>
    <enclosure url="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/video-398b8f79d8e76c39dd60285dd980bf2e.mp4" type="video/mp4" length="1304209"/>
    <itunes:duration>00:00:09</itunes:duration>
  </item>
  <item>
    <title>Telegram - A new bot to simply get your user, group or chat ID on Telegram hosted on Cloudflare Workers</title>
    <guid>zI8FLkFDeD3</guid>
    <pubDate>Sat, 06 Apr 2024 16:23:41 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h1>UPDATE 10 APRIL 2024: Bot is now fixed.</h1><p>I know there are a lot of Telegram bots out there that do this, however I realized there has been a few I tried (even those recommended on online forums) which are outright no longer maintained and stopped working long ago. It took me many tries to find the one that worked.</p><h2>Solution</h2><p>Bot: <a href="http://t.me/icanhazidbot" rel="noopener noreferrer" target="_blank">http://t.me/icanhazidbot</a></p><p>Source: <a href="https://gitlab.com/jarylc/cf-workers-icanhazid-telegram-bot" rel="noopener noreferrer" target="_blank">https://gitlab.com/jarylc/cf-workers-icanhazid-telegram-bot</a></p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/zI8FLkFDeD3/image-972394c050b325837978d1a6dcfeff51.png"></p><p>I decided to take matters into my own hands and leveraged Cloudflare Workers uptime and reliability to host a bot that simply does the job without a lot of maintenance required (due to serverless). This was inspired by <a href="https://icanhazip.com" rel="noopener noreferrer" target="_blank">https://icanhazip.com</a> as well which is also <a href="https://major.io/2021/06/06/a-new-future-for-icanhazip/" rel="noopener noreferrer" target="_blank">hosted on Cloudflare Workers</a>.</p><p>If a developer ever requests your user ID, you can simply just tag the bot @icanhazid, wait for the prompt, and tap on the ID.</p><p>Feel free to start a chat with that bot to get your own ID as well.</p><p>To grab a group chat ID, you would have to invite the bot to a group.</p><p>If everything is in order, the reply is almost instantaneous as well!</p><p>Hack away!</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/telegram-a-new-bot-to-simply-get-your-user-grou-zI8FLkFDeD3/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Javascript - Using chromedp/headless-shell Docker image for Puppeteer</title>
    <guid>ybhdO8oR3sV</guid>
    <pubDate>Sat, 06 Apr 2024 16:23:08 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>I had a personal project that made use of an awesome Puppeteer plugin called <a href="https://www.npmjs.com/package/puppeteer-extra-plugin-portal" rel="noopener noreferrer" target="_blank">puppeteer-extra-plugin-portal</a> by <a href="https://github.com/claabs/puppeteer-extra-plugin-portal" rel="noopener noreferrer" target="_blank">claabs</a>. I wanted to create a Docker container that was small and found <a href="https://hub.docker.com/r/chromedp/headless-shell" rel="noopener noreferrer" target="_blank">chromedp/headless-shell</a>.</p><h2>Sources</h2><h3>main.js</h3><pre class="ql-syntax" spellcheck="false">import puppeteer from 'puppeteer';
puppeteer.launch(
{
  headless: true,
  args: [
    "--disable-gpu",
    "--disable-dev-shm-usage",
    "--disable-setuid-sandbox",
    "--no-sandbox",
  ],
}).then(async browser =&gt; {
    const page = await browser.newPage();
    // further logic...
  }
)
</pre><blockquote>Not much difference from a regular Puppeteer setup</blockquote><h3>.dockerignore</h3><pre class="ql-syntax" spellcheck="false">.git
*.log
node_modules
Dockerfile
</pre><blockquote>We don't want node_modules folder especially as it might contain locally downloaded Chromium for development</blockquote><h3>Dockerfile</h3><pre class="ql-syntax" spellcheck="false">FROM chromedp/headless-shell

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/headless-shell/headless-shell

COPY . /app
WORKDIR /app

RUN apt-get update &amp;&amp; \
    apt-get install -y tini curl &amp;&amp; \
    curl -fsSL https://deb.nodesource.com/setup_17.x | bash - &amp;&amp; \
    apt-get install -y nodejs &amp;&amp; \
    rm -rf /var/lib/apt/lists/* &amp;&amp; \
    npm install

ENTRYPOINT ["tini", "--"]
CMD ["/usr/bin/node main.js"]
</pre><blockquote>PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true is the most important to keep the image small</blockquote><blockquote>chromedp/headless-shell runs Debian and not Alpine for smaller image sadly, hence using apt, I might look into creating an automated build for Alpine images in the future</blockquote>]]>
    </description>
    <link>https://kb.jarylchng.com/i/javascript-using-chromedpheadless-shell-docker-ybhdO8oR3sV/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Home Automation - Jovision CloudSEE Doorbell Trigger to Telegram Chat</title>
    <guid>rykxH8CCzDk</guid>
    <pubDate>Sat, 06 Apr 2024 16:21:20 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>The doorbell, according to the MAC address, is by China Dragon Technology Limited</p><p>The reason for this is because this model is from China and requires me to install HUAWEI Push Kit to receive notifications. But I did not want that and needed an alternative.</p><p>Now this may seem like a very simple integration, trust me it is not. The doorbell itself has all ports closed and it only communicates with their own cloud servers.</p><h2>Intercepting Traffic with Zanti Man-in-the-Middle (MitM) Attack</h2><p>I tried many ways to intercept traffic and see the requests sent by the doorbell when the alarm button is triggered. I settled onto my trusty Android device with <a href="https://www.zimperium.com/zanti-mobile-penetration-testing" rel="noopener noreferrer" target="_blank">Zanti Penetration Testing Application</a>.</p><p>Here's a sample request sent according to Zanti where they upload an image to their cloud servers when the button is pressed.</p><pre class="ql-syntax" spellcheck="false">URL: http://oss-cn-hangzhou.aliyuncs.com/xw-cloudstorage/ecateye/7days/(CAMERA_ID)/20210613/A01142912.jpg
Date: Sun Jun 13 14:29:10 GMT+08:00 2021
Method: PUT
Auth: null
Cookie: null
User Agent: (CAMERA_ID)
Mime Type: null

Headers:
date : Sun, 13 Jun 2021 06:29:19 GMT
authorization : OSS (AUTH_KEY)
content-length : 20857
host : oss-cn-hangzhou.aliyuncs.com
content-type : application/octet-stream
user-agent : (CAMERA_ID)

Form params:
</pre><h2>DNS "Poisoning" with AdGuard Home DNS Server</h2><p>Since my entire household is under <a href="https://github.com/AdguardTeam/AdGuardHome" rel="noopener noreferrer" target="_blank">AdGuard Home DNS</a> (previously <a href="https://github.com/pi-hole/pi-hole" rel="noopener noreferrer" target="_blank">PiHole</a>) and the device doesn't seem to be using any custom DNS servers, I have decided to reroute traffic going to oss-cn-hangzhou.aliyuncs.com to my <a href="https://www.nginx.com/" rel="noopener noreferrer" target="_blank">NGINX</a> server.</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/rykxH8CCzDk/image-f5b9b669278c565672cfb493541f8ed0.png"></p><h2>Proxying and Mirroring with NGINX</h2><p>Luckily for me, the device uses insecure HTTP connection and I don't have to worry about any certificate checks. I pinged the address to find out that the IP address is 118.31.219.251. It may be a CDN IP but it's a low priority for me to keep up with it. I used the mirror keyword to route traffic to a webhook trigger on my local <a href="https://n8n.io/" rel="noopener noreferrer" target="_blank">N8N.io</a> instance</p><pre class="ql-syntax" spellcheck="false">server {
  listen 80;
  listen [::]:80;

  server_name oss-cn-hangzhou.aliyuncs.com;

  location / {
    mirror /mirror;
    mirror_request_body on;
    proxy_pass http://118.31.219.251;
  }
  location /mirror {
    internal;
    proxy_method POST;
    proxy_pass http://(N8N_IO_HOSTNAME)/webhook/(WEBHOOK_ID)/;
  }
}
</pre><h2>Handling request with <a href="http://N8N.io" rel="noopener noreferrer" target="_blank">N8N.io</a></h2><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/rykxH8CCzDk/image-4cb96c45acc8a28add5ea0779e9f1964.png"></p><ol><li>Grabs the request body from POST webhook (make sure to enable Binary Data option)</li><li>Sends the image data captured in data binary object to my personal <a href="https://min.io/" rel="noopener noreferrer" target="_blank">MinIO</a> server</li><li>Sends the photo and a message to my family's Telegram channel</li></ol><h2>Final output</h2><p>It's time to press the doorbell!</p><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/rykxH8CCzDk/image-3c74d614c8c4b7f3beef59a3ce1248c3.png"></p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/home-automation-jovision-cloudsee-doorbell-trigg-rykxH8CCzDk/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Sharing Computer Audio with Discord &amp; Zoom</title>
    <guid>qg9ed0d2i8s</guid>
    <pubDate>Sat, 06 Apr 2024 16:19:46 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>Required software packages</h2><p>pulseaudio and pactl</p><h2>Instructions</h2><h3>List all existing input sinks</h3><pre class="ql-syntax" spellcheck="false">pactl list sinks
</pre><h3>Find the name of the existing target input sink (your mic)</h3><p>Mine was: Name: alsa_output.usb-Kingston_HyperX_Cloud_Revolver_S_000000000000-00.analog-surround-71</p><h3>Create virtual sinks and combine mic and computer audio as inputs</h3><pre class="ql-syntax" spellcheck="false">pacmd load-module module-null-sink sink_name=Share sink_properties=device.description=Share
pacmd load-module module-combine-sink slaves=alsa_output.usb-Kingston_HyperX_Cloud_Revolver_S_000000000000-00.analog-surround-71,Share
pacmd load-module module-loopback sink=Share latency_msec=1
pactl load-module module-remap-source master=Share.monitor source_name=Share_Mic source_properties=device.description="Share_Mic"
</pre><h3>Route output audio accordingly</h3><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/qg9ed0d2i8s/image-97117276fae27e201aa3559ee3a4c807.png"></p><ul><li>Route to Share if you don't wish to hear the output</li><li>Route to Simultaneous output if you do wish to hear the output</li></ul><h3>Use the new Share_Mic as input</h3><h4>Discord</h4><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/qg9ed0d2i8s/image-183d5a7f96497ebbfac64848ad75aa71.png"></p><h4>Zoom</h4><p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/qg9ed0d2i8s/image-aa9bb748839c6943d7e1bc90b3d317a2.png"></p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-sharing-computer-audio-with-discord-and-zoom-qg9ed0d2i8s/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Java - Downloading Maven Dependencies as a Local Repository for Offline Build and Usage</title>
    <guid>DAeWYjb1OU2</guid>
    <pubDate>Sat, 06 Apr 2024 16:17:33 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>I had a scenario where in a Jenkins slave machine while configuring CI/CD, it was not allowed to access the internet to download Maven dependencies due to restrictions. Hence, I had to find a way to download the remote repositories of only required files to a local repository and push it along with the codes and map pom.xml to use it.</p><h2>Instructions</h2><h3>1. Download offline repository of only required dependencies into a file path</h3><p>Run command in project root directory:</p><pre class="ql-syntax" spellcheck="false">mvn dependency:go-offline -Dmaven.repo.local=./repo
</pre><h3>2. Map pom.xml</h3><p>Bottom of pom.xml:</p><pre class="ql-syntax" spellcheck="false">&lt;repositories&gt;
    &lt;repository&gt;
        &lt;id&gt;offline&lt;/id&gt;
        &lt;name&gt;Offline Repository&lt;/name&gt;
        &lt;url&gt;file://${project.basedir}/repo&lt;/url&gt;
    &lt;/repository&gt;
&lt;/repositories&gt;
&lt;pluginRepositories&gt;
    &lt;pluginRepository&gt;
        &lt;id&gt;offline&lt;/id&gt;
        &lt;name&gt;Offline Repository&lt;/name&gt;
        &lt;url&gt;file://${project.basedir}/repo&lt;/url&gt;
    &lt;/pluginRepository&gt;
&lt;/pluginRepositories&gt;
</pre><p>As simple as that.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/java-downloading-maven-dependencies-as-a-local-r-DAeWYjb1OU2/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Setting up linuxserver/netbootxyz Docker Image and dnsmasq DHCP in Proxy Mode When Your Main Router Has Locked DHCP Settings</title>
    <guid>r6dsVb36y-l</guid>
    <pubDate>Sat, 06 Apr 2024 16:17:02 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>Prerequisites</h2><ol><li>Docker is installed</li><li>dnsmasq is installed</li></ol><h2>Steps</h2><h3>1. Start Docker image from <a href="http://linuxserver.io" rel="noopener noreferrer" target="_blank">linuxserver.io</a></h3><p>Head down to <a href="https://hub.docker.com/r/linuxserver/netbootxyz" rel="noopener noreferrer" target="_blank">linuxserver/netbootxyz</a> and follow the instructions to get a netboot.xyz container up and running.</p><h3>2. Configuration</h3><h4>/etc/dnsmasq.conf</h4><pre class="ql-syntax" spellcheck="false">port=0
interface=eth0
bind-dynamic
log-dhcp
dhcp-authoritative
dhcp-range=192.168.1.0,proxy,255.255.255.0
pxe-service=x86PC, "NETBOOT (BIOS)", "netboot.xyz.kpxe", 192.168.1.253
pxe-service=X86-64_EFI, "NETBOOT (EFI)", "netboot.xyz.efi", 192.168.1.253
</pre><h5>Explanations</h5><ul><li>port=0 - <strong>(optional)</strong> disable DNS server if not in use</li><li>interface=eth0 - <strong>(optional)</strong> only listen on interface eth0, repeat this line for more interfaces, remove for all interfaces</li><li>log-dhcp - <strong>(optional)</strong> log DHCP requests</li><li>bind-dynamic - <strong>(optional)</strong> binds to new interfaces added after start</li><li>dhcp-authoritative - prioritize this DHCP server in a network</li><li>dhcp-range - replace 192.168.1.0 and 255.255.255.0 according to your IP range and subnet, set to proxy mode to proxy your original DHCP server</li><li>pxe-service=x86PC for BIOS systems, replace 192.168.1.253 with the IP of the machine hosting netbootxyz</li><li>pxe-service=X86-64_EFI for UEFI systems, replace 192.168.1.253 with the IP of the machine hosting netbootxyz</li><li>enable-tftp is <strong>not included</strong> as it already hosted by the Docker container</li></ul><h3>3. Start your services</h3><p>Start your services.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-setting-up-linuxservernetbootxyz-docker-i-r6dsVb36y-l/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Port Forwarding Through a VPS With The Help of WireGuard and iptables</title>
    <guid>z4FxEVf3KT_</guid>
    <pubDate>Sat, 06 Apr 2024 16:16:47 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>Recently I wanted to host a Minecraft server on a network that I can not port forward with. Although this can be done with an SSH tunnel too, I believe Wireguard will give a slightly better performance overall.</p><h2>Commands</h2><h3>Add</h3><pre class="ql-syntax" spellcheck="false">iptables -t nat -A PREROUTING -p tcp -d 123.123.123.123 --dport 25565 -j DNAT --to 10.123.123.123:25565
iptables -A FORWARD -p tcp -d 10.123.123.123 --dport 25565 -j ACCEPT
iptables -t nat -o wg0 -A POSTROUTING -j MASQUERADE
</pre><h3>Delete</h3><pre class="ql-syntax" spellcheck="false">iptables -t nat -D PREROUTING -p tcp -d 123.123.123.123 --dport 25565 -j DNAT --to 10.123.123.123:25565
iptables -D FORWARD -p tcp -d 10.123.123.123 --dport 25565 -j ACCEPT
iptables -t nat -o wg0 -D POSTROUTING -j MASQUERADE
</pre><ul><li>Change 123.123.123.123 to your external facing server's public IP address</li><li>Change 10.123.123.123 to the server's Wireguard IP address</li><li>Change all instances of 25565 to a port you wish to forward</li><li>Change wg0 to your Wireguard interface name</li></ul>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-port-forwarding-through-a-vps-with-the-hel-z4FxEVf3KT_/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Wireguard Server and Peer Configuration</title>
    <guid>cGYI6ANCvu6</guid>
    <pubDate>Sat, 06 Apr 2024 16:15:32 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>Wireguard is a new VPN tool that is vastly easier to setup than the popular alternative OpenVPN. Also reports state that it is also superior in speed and reliability.</p><h2>Setup</h2><h3>General</h3><h4>Installation</h4><p>Most package managers should have the required packages named</p><p>wireguard-tools wireguard-dkms</p><p>Install them on both your server and client(s).</p><p>Note: very soon, Wireguard will become baked into the Linux kernel by default and wireguard-dkms will not be needed anymore.</p><h4>Generation of private and public key pair</h4><pre class="ql-syntax" spellcheck="false">(umask 077 &amp;&amp; printf "[Interface]\nPrivateKey = " | sudo tee /etc/wireguard/wg0.conf &gt; /dev/null)
wg genkey | sudo tee -a /etc/wireguard/wg0.conf | wg pubkey | sudo tee /etc/wireguard/publickey
</pre><p>Replace wg0 with your desired network device id throughout the article if needed.</p><p>This generates a private key and automatically inserts as a configuration line to /etc/wireguard/wg0.conf and a public key saved to /etc/wireguard/publickey automatically. Run it on both your server and client(s) respectively.</p><h3>Server</h3><h4>Edit /etc/wireguard/wg0.conf</h4><pre class="ql-syntax" spellcheck="false">[Interface]
# Private key, automatically generated by above command on the server (should be only 44 characters as of writing)
PrivateKey = -auto generated-

# Private IPv4 and IPv6 address of Server for peers to communicate with when connected, you can replace `123.210` and `123:210` with anything you like throughout the article
Address = 10.123.210.1/24,fd00:123:210::1/112

# Listen port, can be any port you like including 53 if you don't use it for DNS. Must be the same throughout the article.
ListenPort = 51820

# Setup IPv4 and IPv6 iptables to forward the network of peers through the server, not required if only a LAN connection is required (optional)
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i %i -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i %i -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Save the configuration to the file on every shutdown, personally I prefer it off because I find it easier to edit the configuration directly rather than to rely on tools
SaveConfig = false


# CLIENT 1
[Peer]

# Public key of the peer, generated by the above command on the peer (also should be only 44 characters as of writing)
PublicKey = -auto generated and copied here-

# Allow IPv4 and IPv6 range from 10.123.210.1-10.123.210.254 and fd00:123:210::1-fd00:123:210::ffff respectively
AllowedIPs = 10.123.210.0/24,fd00:123:210::0/112


# CLIENT 2
[Peer]
# Public key of the peer, generated by the above command on the peer (also should be only 44 characters as of writing)
PublicKey = -auto generated and copied here-

# Allow IPv4 and IPv6 range from 10.123.210.1-10.123.210.254 and fd00:123:210::1-fd00:123:210::ffff respectively
AllowedIPs = 10.123.210.0/24,fd00:123:210::0/112

# ... More peers if required ...
</pre><h4>Additional step to allow forwarding (optional)</h4><pre class="ql-syntax" spellcheck="false">echo -e "net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1" | sudo tee -a /etc/sysctl.d/99-sysctl.conf
sudo sysctl -p
</pre><h4>Start the server</h4><pre class="ql-syntax" spellcheck="false">sudo systemctl enable --now wg-quick@wg0
</pre><h3>Client(s)</h3><h4>Edit /etc/wireguard/wg0.conf</h4><pre class="ql-syntax" spellcheck="false">[Interface]
# Private key, automatically generated by above command on the client (should be only 44 characters as of writing)
PrivateKey = -auto generated-

# Private IPv4 and IPv6 address of client, must be static IP (no clashes) because there is no DHCP provided by Wireguard as of writing. Change the `2` to an incremental number for every client
Address = 10.123.210.2/32,fd00:123:210::2/128

# DNS server to use, currently set to Cloudflare
DNS = 1.1.1.1


# SERVER
[Peer]
# Public key of server, generated by the above command on the server (only 44 characters as of writing)
PublicKey = -auto generated and copied here-

# Public IP of server and port configured in the server
Endpoint = -public key of server-:51820

# IP ranges Wireguard will listen on and forward
# AllowedIPs = 10.123.210.0/24,fd00:123:210::0/112 # ROUTE ONLY VIRTUAL PRIVATE NETWORK TRAFFIC
AllowedIPs = 0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2, 128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10, 172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, ::/0, 10.123.210.0/32 # ROUTE ALL INTERNET TRAFFIC LESS LAN THROUGH

# Constant pings to keep the connection alive and not time out on inactivity
PersistentKeepalive = 25
</pre><h4>Connect to server</h4><pre class="ql-syntax" spellcheck="false">sudo wg-quick up wg0
</pre><h4>Connection information</h4><p>You can run these commands to check the connection</p><pre class="ql-syntax" spellcheck="false">sudo wg
ping 10.123.210.1
</pre><h4>Disconnect from server</h4><pre class="ql-syntax" spellcheck="false">sudo wg-quick down wg0
</pre><h2>Extra information</h2><h3>networkmanager-wireguard</h3><p>If you use NetworkManager (especially nm-applet) you can install networkmanager-wireguard or networkmanager-wireguard-git (AUR) for Wireguard capabilities and configuration.</p><h3>Forward other UDP ports to Wireguard port with iptables</h3><p>On the server:</p><pre class="ql-syntax" spellcheck="false">sudo iptables -t nat -I PREROUTING -i eth0 -p udp -m multiport --dports 53,80,123,161,443 -j REDIRECT --to-ports 51820
</pre><p>To disable:</p><pre class="ql-syntax" spellcheck="false">sudo iptables -t nat -D PREROUTING -i eth0 -p udp -m multiport --dports 53,80,123,161,443 -j REDIRECT --to-ports 51820
</pre><p>You can add it to PostUp and PostDown. Don't forget ip6tables if needed.</p><h3>More reading</h3><p><a href="https://wiki.jarylchng.com/en/operating-systems/linux/wireguard-server-and-peers-configuration" rel="noopener noreferrer" target="_blank">https://github.com/pirate/wireguard-docs#Interface</a></p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-wireguard-server-and-peer-configuration-cGYI6ANCvu6/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Overclocking Multiple Headless Nvidia GPU for CUDA Cryptocurrency Mining</title>
    <guid>lp-s3OrdZuR</guid>
    <pubDate>Sat, 06 Apr 2024 16:14:44 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>Setup</h2><pre class="ql-syntax" spellcheck="false">lspci | grep VGA
  01:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
  02:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
  03:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
  04:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
  06:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
  07:00.0 VGA compatible controller: NVIDIA Corporation GP104 [GeForce GTX 1070] (rev a1)
</pre><p>6x Gigabyte GTX 1070 G1 Gaming (7.93 GB VRAM) pool mining Ethereum.</p><h2>Steps</h2><p>Firstly we will have to one-time initialize /etc/X11/xorg.conf by using Nvidia's xconfig tool including a <a href="https://wiki.archlinux.org/index.php/NVIDIA/Tips_and_tricks#Enabling_overclocking" rel="noopener noreferrer" target="_blank">cool-bits</a> value of 28.</p><pre class="ql-syntax" spellcheck="false">nvidia-xconfig --allow-empty-initial-configuration --enable-all-gpus --cool-bits=28
</pre><p>Afterwards, we can start a Xorg server headlessly as Nvidia's setting tool requires it to be up.</p><pre class="ql-syntax" spellcheck="false">X :0 &amp; # display :0
</pre><p>Since I am running multiple cards, I would require a loop to go through all of it. Currently these values works best for the cards I used.</p><ul><li>-200 core clock</li><li>+1200 memory clock</li></ul><pre class="ql-syntax" spellcheck="false"># export DISPLAY=:0
# IFS=$'\n'
# for gpu in $(nvidia-smi -L); do
    id=$(echo ${gpu} | sed -e 's/GPU //g' -e 's/:.*//g')
    nvidia-settings -a [gpu:${id}]/GPUPowerMizerMode=1 -a [gpu:${id}]/GPUGraphicsClockOffset[3]=-200 -a [gpu:${id}]/GPUMemoryTransferRateOffset[3]=1200
done
</pre><p>As a final step, I would apply a -40 max watts undervolt as root.</p><pre class="ql-syntax" spellcheck="false">nvidia-smi -pl 140
</pre><p>Optionally, since we do not need the Xorg server to be running anymore, we can kill it</p><pre class="ql-syntax" spellcheck="false">killall Xorg
</pre><p>These configuration alone increased ~5MH/s per card, resulting in a 25-30MH/s overall increase!</p><h2>Automated bash script</h2><pre class="ql-syntax" spellcheck="false">#!/usr/bin/env bash
sudo nvidia-smi -pl 140
sudo X :0 &amp;
sleep 5
export DISPLAY=:0
IFS=$'\n'
for gpu in $(nvidia-smi -L); do
    id=$(echo ${gpu} | sed -e 's/GPU //g' -e 's/:.*//g')
    sudo nvidia-settings -a [gpu:${id}]/GPUPowerMizerMode=1 -a [gpu:${id}]/GPUGraphicsClockOffset[3]=-200 -a [gpu:${id}]/GPUMemoryTransferRateOffset[3]=1200
done
sudo killall Xorg
sudo systemctl start ethminer # or your own command to start your own miner
</pre>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-overclocking-multiple-headless-nvidia-gpu-lp-s3OrdZuR/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - Default File and Directory Permissions</title>
    <guid>11i9Zvy40NI</guid>
    <pubDate>Sat, 06 Apr 2024 16:14:04 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>Just yesterday night while I was configuring Grav, I realised some of the permissions were not setting right and I was unable to modify files created by Grav via a privileged user with SFTP.</p><p>Looking into it, I checked the properties via ls -l and sure enough:</p><pre class="ql-syntax" spellcheck="false">ls -l /-snip-grav-parent-directory-/
    total -snip-
    drwxr--r-x 13 -snip-grav-user- -snip-grav-group- 4096 Aug  6 08:00 -snip-grav-folder-
</pre><p>Since the files were owned by the Grav user, while my user was in the Grav group. I was unable to edit the files due to the absense of write permissions for the group clause.</p><h2>Simple chmod</h2><p>At first, running a simple recursive chmod everytime I met the issue was fine as normally my web files were not created by the Grav user as I mainly host static pages before:</p><pre class="ql-syntax" spellcheck="false">chmod -R g+w /-snip-grav-directory-/
</pre><h2>Default groups and permissions</h2><p>But there was two problems, the first problem is that new files created by my privileged user had the group not set to the Grav user and thus not allowing Grav to modify it too. I didn't wish to add the grav user to any groups so I had to resort to recursively adding the <em>setgid</em> bit on the directory:</p><pre class="ql-syntax" spellcheck="false">$ chmod -R g+s /-snip-grav-directory-/
</pre><p>That solves the first problem by having new files automatically set its' group to the parent directory whenever they are created.</p><p>The other problem was there was no default permissions and new files were created were not allowed to be modified by my privileged user. That's where this nifty tool comes in, <a href="https://wiki.archlinux.org/index.php/Access_Control_Lists" rel="noopener noreferrer" target="_blank">acl (Access Control Lists)</a> which extends the basic chmod, chown and chgrp commands.</p><p>This tool requires you to add acl your filesystem mount option to always apply if it's not already set, you can check it by running this and seeing if acl is returned:</p><pre class="ql-syntax" spellcheck="false">tune2fs -l /dev/sdXY | grep "Default mount options:"
</pre><p>Most filesystems turn it on by default, but if it does not, proceed to add it into /etc/fstab.</p><p>After that it's just a simple command as this to set default permissions for the directory and the files</p><pre class="ql-syntax" spellcheck="false">setfacl -R -d -m g::rwX -snip-grav-directory-
</pre><ul><li>setfacl - set acl command</li><li>-R - recursive</li><li>-d - target <strong>d</strong>efault permissions</li><li>-m - <strong>m</strong>odify</li><li>g::rwX - target <strong>g</strong>roup with <strong>r</strong>ead <strong>w</strong>rite and only directories should be set with e<strong>X</strong>ecutable</li></ul><p>A confirmation with getfacl:</p><pre class="ql-syntax" spellcheck="false">getfacl -snip-grav-directory-
  getfacl -snip-grav-directory-
  # file: -snip-grav-directory-
  # owner: -snip-grav-user-
  # group: -snip-grav-group-
  # flags: -s-
  user::rwx
  group::rwx
  other::r-x
  default:user::rwx
  default:group::rwx
  default:other::r-x
</pre><p>Once you see # flags: -s- and default:group::rwx, you are all set and future new directories and files inside will be set with the default permissions automatically.</p><p>You may want to run chmod -R g+X the directory after the above step to apply the permissions to the existing files.</p><h2>Pointers from mistakes I made</h2><ul><li>Do not forget to set the executable permission for directories, if it is missing the user will not be able to access it, note the pointer below when needing to fix this.</li><li>Do not ever run chmod -R g+x as it would set all files executables too which is a security risk, instead use chmod -R g+X with capital X which only sets directories with executable, same thing for acl.</li></ul><h2>Further reading and references</h2><ul><li><a href="https://wiki.archlinux.org/index.php/Access_Control_Lists" rel="noopener noreferrer" target="_blank">Archlinux Wikipedia article on ACL</a></li><li><a href="https://wiki.archlinux.org/index.php/File_permissions_and_attributes" rel="noopener noreferrer" target="_blank">Archlinux Wikipedia article on file permissions and attributes</a></li></ul>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-default-file-and-directory-permissions-11i9Zvy40NI/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Linux - 5.1 Surround Sound on 3 3.5mm Ports Motherboard</title>
    <guid>Cdz-vkxg5c2</guid>
    <pubDate>Sat, 06 Apr 2024 16:13:09 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p>I've recently purchased an <a href="https://www.msi.com/Motherboard/B450M-BAZOOKA-PLUS" rel="noopener noreferrer" target="_blank">MSI B450M BAZOOKA PLUS</a> motherboard that supports 7.1 surround sound on their measely 3 3.5mm ports.</p><p>I have a 5.1 surround sound setup with my <a href="https://www.logitech.com/en-sg/product/surround-sound-speakers-z506" rel="noopener noreferrer" target="_blank">Logitech Z506</a> set that was previously working just plugging in to my old motherboard <a href="https://www.asus.com/sg/Motherboards/P8P67_PRO/" rel="noopener noreferrer" target="_blank">Asus P8P67 PRO</a> which had 6 3.5mm ports.</p><h2>Instructions</h2><ol><li>Install the hdajackrestask tool on your system (Archlinux: pacman -S alsa-tools)</li><li>Run in privileged mode using sudo or polkit (GKSU: gksu hdajackretask)</li><li>Plug your 3 3.5mm audio jacks according to this image, colours should be roughly the same, alternatively take reference from the label (Mic, L-Out and L-In)</li><li>As root run:</li></ol><pre class="ql-syntax" spellcheck="false">echo "options snd-hda-intel patch=hda-jack-retask.fw,hda-jack-retask.fw,hda-jac
</pre>]]>
    </description>
    <link>https://kb.jarylchng.com/i/linux-51-surround-sound-on-3-35mm-ports-mother-Cdz-vkxg5c2/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>Java - Minimal Undertow RESTeasy and Weld CDI Setup</title>
    <guid>4lDipY61lXo</guid>
    <pubDate>Thu, 17 Jun 2021 11:00:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<h2>Before we start</h2><h3>What is Undertow?</h3><p>Quoted from the <a href="https://undertow.io/" rel="noopener noreferrer" target="_blank">project page</a>:</p><blockquote>Undertow is a flexible performant web server written in java, providing both blocking and non-blocking API’s based on NIO.</blockquote><blockquote>Undertow has a composition based architecture that allows you to build a web server by combining small single purpose handlers. The gives you the flexibility to choose between a full Java EE servlet 4.0 container, or a low level non-blocking handler, to anything in between.</blockquote><blockquote>Undertow is designed to be fully embeddable, with easy to use fluent builder APIs. Undertow’s lifecycle is completely controlled by the embedding application.</blockquote><blockquote>Undertow is sponsored by JBoss and is the default web server in the Wildfly Application Server.</blockquote><p>Alternatives include <a href="https://javaee.github.io/grizzly/" rel="noopener noreferrer" target="_blank">Oracle Grizzly2</a>, <a href="https://netty.io/" rel="noopener noreferrer" target="_blank">Netty</a>, <a href="https://www.eclipse.org/jetty/" rel="noopener noreferrer" target="_blank">Eclipse Jetty</a> and <a href="https://tomcat.apache.org/" rel="noopener noreferrer" target="_blank">Apache Tomcat (has non-embedded version)</a></p><p>In a nutshell, <strong>Undertow is a embedded web server used that supports both static and dynamic content delivery</strong>.</p><p>It is notable that in my tests done in a <a href="https://gitlab.com/jarylc/jax-rs-mongodb-reactivestreams-to-rxjava-examples/" rel="noopener noreferrer" target="_blank">side benchmarking project</a> that Undertow seems to perform much better than the alternatives mentioned for simple use especially combined with the RESTeasy JAX-RS framework. You could try to download the <a href="https://gitlab.com/jarylc/jax-rs-mongodb-reactivestreams-to-rxjava-examples/pipelines" rel="noopener noreferrer" target="_blank">latest artifacts</a> on the project to test it out yourself.</p><h3>What is RESTeasy?</h3><p>Quoted from the <a href="https://resteasy.github.io/" rel="noopener noreferrer" target="_blank">project page</a>:</p><blockquote>RESTEasy is a JBoss project that provides various frameworks to help you build RESTful Web Services and RESTful Java applications. It is a fully certified and portable implementation of the JAX-RS 2.1 specification, a JCP specification that provides a Java API for RESTful Web Services over the HTTP protocol.</blockquote><p>In a nutshell <strong>RESTeasy is a JAX-RS (Java API for RESTful Web Services) implementation that provides frameworks aiding in exposing REST (REpresentational State Transfer) endpoints</strong>, especially for CRUD (Create, Read, Update and Delete) operations.</p><p>Alternatives include <a href="https://jersey.github.io/" rel="noopener noreferrer" target="_blank">Oracle Jersey</a></p><p>RESTeasy is also developed by RedHat and has better "native" integration with Undertow than Jersey.</p><h3>What is Weld?</h3><h4>What is CDI?</h4><p>As quoted from another good read <a href="https://www.baeldung.com/java-ee-cdi" rel="noopener noreferrer" target="_blank">Baeldung: Java EE CDI</a>:</p><blockquote>It allows us to manage the lifecycle of stateful components via domain-specific lifecycle contexts and inject components (services) into client objects in a type-safe way.</blockquote><p>In layman terms, <strong>CDI something like a factory that manages the life of Java objects during the life of the application</strong>, this is especially important in a multi-threaded environment like a web server. Some examples include:</p><ul><li>Objects that only need one instance throughout the runtime of the application (database connections, etc.)</li><li>Objects that only need to be unique and persist in a session</li><li>Objects that only need to be unique and persist in a single request for web applications.</li></ul><p>It is possible to code CDI yourself as mentioned in the Baeldung article, but since implementations are already freely available, you are able to use them and skip boilerplate codes by using annotation interfaces.</p><h4>Weld</h4><p>Quoted from the <a href="https://weld.cdi-spec.org/" rel="noopener noreferrer" target="_blank">project page</a>:</p><blockquote>Weld is the reference implementation of CDI for the Java EE Platform - a JCP standard for dependency injection and contextual lifecycle management and one of the most important and popular parts of the Java EE. Weld is integrated into many Java EE application servers such as WildFly, JBoss Enterprise Application Platform, GlassFish, Oracle WebLogic Server, WebSphere Application Server and others. Weld can also be used in plain servlet containers (Tomcat, Jetty) or Java SE.</blockquote><h2>Base setup</h2><h3>Maven pom.xml</h3><p>We will be using Maven to manage our Java libraries that we will depend on, you can use Gradle if you wish.</p><pre class="ql-syntax" spellcheck="false">&lt;dependencies&gt;
    &lt;!-- UNDERTOW --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.undertow&lt;/groupId&gt;
        &lt;artifactId&gt;undertow-servlet&lt;/artifactId&gt;
        &lt;version&gt;2.0.23.Final&lt;/version&gt;
    &lt;/dependency&gt;

    &lt;!-- RESTEASY --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.jboss.resteasy&lt;/groupId&gt;
        &lt;artifactId&gt;resteasy-undertow&lt;/artifactId&gt;
        &lt;version&gt;4.1.1.Final&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.jboss.resteasy&lt;/groupId&gt;
        &lt;artifactId&gt;resteasy-cdi&lt;/artifactId&gt;
        &lt;version&gt;4.1.1.Final&lt;/version&gt;
    &lt;/dependency&gt;

    &lt;!-- WELD --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.jboss.weld.servlet&lt;/groupId&gt;
        &lt;artifactId&gt;weld-servlet-core&lt;/artifactId&gt;
        &lt;version&gt;3.1.1.Final&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.jboss.weld&lt;/groupId&gt;
        &lt;artifactId&gt;weld-core-impl&lt;/artifactId&gt;
        &lt;version&gt;3.1.1.Final&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.jboss.weld&lt;/groupId&gt;
        &lt;artifactId&gt;weld-api&lt;/artifactId&gt;
        &lt;version&gt;3.1.Final&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</pre><h3>/src/resources/META-INF/beans.xml</h3><p>This file located in the resources META-INF folder is required to configure Weld</p><pre class="ql-syntax" spellcheck="false">&lt;beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
    version="1.2" bean-discovery-mode="annotated"&gt;
&lt;/beans&gt;
</pre><p>You can change "annotated" to "all" if you wish for all Java classes to be managed. Note that it would impact startup time slightly.</p><h3>/src/main/java/.../MyResource.java</h3><p>Also known as a controller in PHP, defines routes.</p><pre class="ql-syntax" spellcheck="false">@RequestScoped
@Path("/")
public class MyResource {
    @Inject
    BeanManager manager;

    @Path("/")
    @GET
    public String getManager() {
        return manager.toString();
    }
}
</pre><h3>/src/main/java/.../MyApplication.java</h3><p>To specify which resources and middleware for web servers to deploy along with the base path.</p><pre class="ql-syntax" spellcheck="false">@ApplicationPath("/")
public class MyApplication extends Application {
    @Override
    public Set&lt;Class&lt;?&gt;&gt; getClasses() {
        Set&lt;Class&lt;?&gt;&gt; classes = new HashSet&lt;&gt;();
        classes.add(MyResource.class); // Resource implemented in the previous section
        return classes;
    }
}
</pre><h3>Main function</h3><pre class="ql-syntax" spellcheck="false">public static void main(String[] args) {
    UndertowJaxrsServer server = new UndertowJaxrsServer();

    ResteasyDeployment deployment = new ResteasyDeploymentImpl();
    deployment.setApplicationClass(MyApplication.class.getName()); // Application implemented in the previous section
    deployment.setInjectorFactoryClass("org.jboss.resteasy.cdi.CdiInjectorFactory"); // set CDI injector factory

    DeploymentInfo deploymentInfo = server.undertowDeployment(deployment, "/");
    deploymentInfo.setClassLoader(Application.class.getClassLoader());
    deploymentInfo.setDeploymentName("Minimal Undertow RESTeasy and Weld CDI Setup"); // set name of deployment
    deploymentInfo.setContextPath("/");
    deploymentInfo.addListener(Servlets.listener(Listener.class)); // add Weld listener to deployment

    server.deploy(deploymentInfo);

    Undertow.Builder builder = Undertow.builder()
            .addHttpListener(8080, "localhost"); // access the server on http://localhost:8080, note that "localhost" should be "0.0.0.0" if you wish for others in the network to connect.
    server.start(builder);
}
</pre><p>Since Undertow is an embedded server, you will just need to run the application and it will expose the server on the specified IP and port.</p><p>After running, if you attempt to access your web server at <a href="http://localhost:8080" rel="noopener noreferrer" target="_blank">http://localhost:8080</a> and it returns "Weld BeanManager" with a bean count above 0, you have successfully configured the setup.</p><h2>What's next?</h2><h3>Modify MyResource.java to play around with scoping</h3><pre class="ql-syntax" spellcheck="false">@Path("/")
@RequestScoped // try changing this to "@ApplicationScoped" along with the commented modifications below
public class MyResource {
    @Inject
    BeanManager manager;

    private int count = 0; // add a counter variable

    @Path("/")
    @GET
    public String getManager() {
        count++; // increment counter
        return manager + ". Current count: " + count; // show current count ;
    }
}
</pre><p>When it is application scoped, the count should increase every time you refresh the endpoint, vice versa for request scoped.</p><p>This is because application scoped will re-initialize the count to 0 every request, whereas request scoped will not.</p><p>You can research more about scoping in Weld in the <a href="https://docs.jboss.org/weld/reference/latest/en-US/html/scopescontexts.html" rel="noopener noreferrer" target="_blank">documentation</a></p><h3>Further reading</h3><p>Do read the documentations on <a href="https://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html" rel="noopener noreferrer" target="_blank">Undertow</a>, <a href="https://docs.jboss.org/resteasy/docs/4.2.0.Final/userguide/html_single/index.html" rel="noopener noreferrer" target="_blank">RESTeasy</a> and <a href="https://docs.jboss.org/weld/reference/latest/en-US/html_single/" rel="noopener noreferrer" target="_blank">Weld</a>.</p><h3>Carry on with your own projects</h3><p>Now that you are equipped with the basic knowledge on how to implement Weld CDI on Undertow RESTeasy, go ahead and code your own projects! Be sure to update the Maven configuration if you are using them and the versions used are outdated.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/java-minimal-undertow-resteasy-and-weld-cdi-setu-4lDipY61lXo/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
  <item>
    <title>N8N - Carousell.sg - Automatic Reply Bot</title>
    <guid>k95O_GJ6Sdf</guid>
    <pubDate>Thu, 17 Jun 2021 10:02:00 GMT</pubDate>
    <itunes:explicit>false</itunes:explicit>
    <description>
      <![CDATA[<p><img src="https://kb-static.jarylchng.com/kb-jarylchng-com/production/media/rich-editor/items/k95O_GJ6Sdf/image-11ef9b231e071e02933af945d3bd0829.png"></p><h1>This has been superceded by <a href="https://gitlab.com/jarylc/carousell-gobot" rel="noopener noreferrer" target="_blank">Carousell GoBot</a> project.</h1><h2>Context</h2><p>I was looking for ideas to try out <a href="http://n8n.io" rel="noopener noreferrer" target="_blank">n8n.io</a>'s capabilities and picked this as the first big idea to try out.</p><p>Basically, I would like to <a href="http://n8n.io" rel="noopener noreferrer" target="_blank">n8n.io</a> to handle chat messages and offers from <a href="https://carousell.sg" rel="noopener noreferrer" target="_blank">Carousell</a>, a Singaporean marketplace platform, similar to Facebook Marketplace.</p><p>Right now I would only like it to:</p><ul><li>Reply a standard response template on new messages and offers</li><li>Forward to my personal Telegram bot to notify me via one additional channel</li><li>Detect any low-balling (offering a price much lower than the listed price)</li></ul><p>The problem was that Carousell does not have any public documentation on their APIs and I have to resort to inspecting the network traffic manually to achieve my goals.</p><p>Docker image: <a href="https://hub.docker.com/r/jarylc/n8n" rel="noopener noreferrer" target="_blank">jarylc/n8n</a></p><h2>Process</h2><h3>Automatic Triggers</h3><p>There are 3 ways to this workflow is automatically triggered:</p><p>-&gt; <strong>Webhook</strong> - Using Tasker on my Android device, it will send a GET request to this webhook everytime a notification from Carousell app arrives.</p><p>-&gt; <strong>IMAP</strong> - Reads a dedicated inbox for e-mails from Carousell and trigger if so, acts as fallback for the above.</p><p>-&gt; <strong>Cron</strong> - Every 30 minutes as fallback in case all of the above doesn't work. Note that there is a better to do this if standalone, you could just connect to the chat WebSocket and wait for messages.</p><h3>Retrieve Chat Data and Preliminary Checks</h3><p>When meddling around <a href="https://www.carousell.sg/inbox/received/" rel="noopener noreferrer" target="_blank">https://www.carousell.sg/inbox/received/</a> and inspecting the network trace, I found out that Carousell has an undocumented API to retreive that I could tap on.</p><p>This API call allows me to retrieve the list of messages and offers that I have recieved as a seller which I put into good use as the first step after the trigger.</p><p>It seems that only the Cookie header is required to authenticate myself.</p><p>The preliminary checks consists of checking if the chat list is empty, or the very last message contains my message signature.</p><p>Now this comes the hard part for this workflow, upon inspecting, Carousell uses SendBird for their chat platform which uses WebSockets instead of API calls.</p><p>I had to customize my n8n.io installation to include ws Node dependency and add it to NODE_FUNCTION_ALLOW_EXTERNAL environment variable to be used in the vm2 environment. This was made easy with my own Docker image on Docker hub using the environment variable ADDITIONAL_MODULES, jarylc/n8n.</p><p>After which, I had to mimic the WebSocket calls when chatting and came up with this final JavaScript node.</p><h3>Custom function codes</h3><p><strong>Check &amp; Split Checks</strong></p><pre class="ql-syntax" spellcheck="false">const fs = require("fs")

const seen = JSON.parse(fs.readFileSync('/data/carousell.json', 'utf8'))

const split = []
let save = {}
for (const item of $node["HTTP Request"].json["data"]["offers"]) {
  if (item['state'] === 'A')
    continue

  let old_price = seen[item['id']] || 0.0
  let latest_price = parseFloat(item['latest_price'].replace(',', ''))

  let message_price_search = item['latest_price_message'].match(/^(\d{1,5}\.?\d{0,2})$|(\d+\.?\d{0,2}((?&lt;=(\$|offer|quote|can|please|pls|quick|fast|sell).*)|(?=.*(\$|offer|quote|can|please|pls|quick|fast|bucks|ok|\?))))/gi)
  if (message_price_search != null) {
    let message_price = parseFloat(message_price_search[message_price_search.length - 1])
    if (message_price &gt; old_price &amp;&amp; message_price &gt; latest_price) {
      latest_price = message_price
      item['latest_price_formatted'] = (''+latest_price.toFixed(2)).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    }
  }

  save[item['id']] = latest_price
  if (item['id'] in seen &amp;&amp; old_price &gt;= latest_price)
    continue

  item['price_changed'] = old_price !== 0.0
  item['low_balled'] = latest_price !== 0 &amp;&amp; latest_price &lt;= parseFloat(item['product']['price']) * 0.85

  split.push({json: item})
}

if (split.length &gt; 0) {
  const fs = require("fs")
  fs.writeFileSync('/data/carousell.json', JSON.stringify(save))
}

return split
</pre><p><strong>Send Reply</strong></p><pre class="ql-syntax" spellcheck="false">const WebSocket = require('ws')

return await new Promise((resolve, reject) =&gt; {
  let sent = false
  const sendbird_subdomain = items[0].json['channel_url'].slice(0, items[0].json['channel_url'].indexOf('-carousell')).toLowerCase()
  const client = new WebSocket('wss://ws-' + sendbird_subdomain + '.sendbird.com/?p=JS&amp;pv=Mozilla%2F5.0%20(X11%3B%20Linux%20x86_64%3B%20rv%3A90.0)%20Gecko%2F20100101%20Firefox%2F90.0&amp;sv=3.0.149&amp;ai=F3CB6187-CB42-4CD1-95FC-1C46F8856006&amp;user_id=344194&amp;access_token=' + $node['Get Chat Token'].json['data']['token'] + '&amp;active=1&amp;SB-User-Agent=JS%2Fc3.0.149%2F%2F&amp;Request-Sent-Timestamp=' + Date.now() + '&amp;include_extra_data=premium_feature_list%2Cfile_upload_size_limit%2Capplication_attributes%2Cemoji_hash', {
    perMessageDeflate: true
  })
  client.on('message', (data) =&gt; {
    if (data.startsWith('LOGI') &amp;&amp; !sent) {
      sent = true
      for (const i in items) {
        try {
          const item = items[i].json
          let text = 'Hello @' + item['user']['username'] + '!\n\n' +
            'Thank you for your '
          if (item['latest_price_formatted'] !== '0') {
            text += (item['price_changed'] ? 'new ' : '') + 'offer of ' + item['currency_symbol'] + item['latest_price_formatted'] + ' on'
          } else {
            text += 'interest in'
          }
          text += ' my item: ' + item['product']['title'] + '.'
          if (!item['is_product_sold'] &amp;&amp; item['product']['status'] !== 'R') {
            if (item['low_balled'])
              text += '\n\nWARNING: Offer price is more than 15% below listing price, it is too low and may not get a future reply unless you increase it!'

            if (!item['price_changed']) {
              text += '\n\nFAQ:\n' +
                '» Where do I normally deal?\n' +
                'Choa Chu Kang or Bukit Panjang area if I am not in office, Bencoolen area if I am.\n' +
                '» What payment methods do I accept?\n' +
                'In order of preference: Google Pay, PayLah, PayNow, Cash, CarouPay, Bank Transfer\n' +
                '» What happens if I did not reply?\n' +
                'Very likely you offered too low-ball of a price' + (item['low_balled'] ? ' (which you probably did)' : '') + '. If not, feel free to message me again.'
            }
          } else {
            text += '\n\nHowever, this listing has already been ' + (item['is_product_sold'] ? 'sold' : 'reserved') + ' and not available anymore.'
          }
          text += '\n\n- @jarylc'

          const msg = {
            channel_url: '' + item['channel_url'],
            message: text,
            data: JSON.stringify({
              offer_id: '' + item['id'],
              source: 'web'
            }),
            mention_type: 'users',
            mentioned_user_ids: [],
            custom_type: 'MESSAGE',
            req_id: Date.now()
          }

          client.send('MESG' + JSON.stringify(msg) + '\n')
        } catch
          (err) {
          reject(err)
        }
      }
    } else if (data.startsWith('READ')) {
      client.terminate()
      resolve(true)
    }
  })
  client.on('error', (err) =&gt; {
    reject(err)
  })
  client.on('open', () =&gt; {
    setTimeout(() =&gt; {
      client.terminate()
      resolve(true)
    }, 10000)
  })
}).then(_ =&gt; {
  return []
}).catch(err =&gt; {
  throw err
})
</pre><h3>Telegram Notify Flow</h3><p>The other branch just forwards this to my personal Telegram bot so that I can be notified via another channel. Right now it will also tell me if the offer was a low-ball or not.</p>]]>
    </description>
    <link>https://kb.jarylchng.com/i/n8n-carousellsg-automatic-reply-bot-k95O_GJ6Sdf/</link>
    <itunes:episodeType>full</itunes:episodeType>
  </item>
</channel>
</rss>