All posts by sufehmi

On Hoaxes / Mis-Disinformation

I’ve been interviewed by many researchers, and everyone pretty much always asked this particular question:

“Why fact-checking is not working?” or “Is it enough to solve the mis/disinformation problem by doing fact-checking?”

By which my answer is always the same: doing fact-checking, alone, will NEVER solve this problem.

There is no silver bullet for this monster.

We need to do so much in order to be able to combat hoax, our umbrella term for the whole spectrum of mis/disinformation, effectively.

We need to educate the public. We need to advocate the governments, so they can develop the right regulations & policies. We need to work together with academics and researchers, so they can pinpoint the right courses of actions. We need to work with journalists, so people will always have trusted sources of information. And of course we need to do hoax busting / fact-checking as well.

It’s a massive scope of work which require massive amount of efforts & resources.

Therefore I’m always so grateful to Mafindo’s volunteers. Due to their sheer numbers and depth & breadth of their skills capacity & capabilities, Mafindo has been able to do all of the above.

Indonesia is so lucky to have them. I hope we will always be able to support their efforts effectively.

Isometric Exercise – olahraga yang paling sehat

“Isometric Exercise” adalah olah raga yang membebani otot, namun tidak bergerak / minim gerakan. Contoh: planking, yoga, pilates, dll. Dan penelitian terbaru menemukan bahwa ini adalah jenis olah raga yang paling sehat.

Kelebihan isometric exercise daripada olahraga lainnya adalah (1) tidak membutuhkan ruang yang luas (2) bisa dilakukan tanpa alat, sehingga (3) jadi bisa dilakukan kapan saja

Beberapa contoh isometric exercises yang mudah dilakukan adalah sbb:

Overhead Hold

Otot yang terdampak : core, triceps, shoulder girdle, upper trapezius

Alat yang diperlukan : beban ringan, seperti barbel 2 kg, atau bahkan kaleng makanan.

  1. Angkat tangan ke atas, dan tahan. Pastikan otot core / tubuh terasa turut bereaksi.
  2. Pastikan tangan dianggkat lurus ke atas. Karena jika bengkok, maka otot yang terdampak akan beda (hanya biceps dan triceps)
  3. Tahan selama 20-30 detik (tapi jangan segan turunkan sebelum itu jika dirasa beban bisa terlepas dari tangan)
  4. Istirahat sejenak, lalu ulangi lagi dari poin 1
  5. Ulangi sebanyak 2 atau 3 kali

Variasi : lakukan dengan berdiri di satu kaki.

High plank

Alat yang diperlukan : tidak ada

Otot yang terdampak : abdominals, quadriceps, glutes, semua otot tangan, dada, pundak

  1. Start dengan posisi seperti push-up, dengan bertumpu di lutut
  2. Tegakkan kedua tangan, dan lalu luruskan kaki. Sehingga tubuh Anda jadi seperti di posisi saat naik di push-up. Pastikan tangan sama rata dengan pundak, kaki lurus, dan seluruh otot core terasa aktif.
  3. Tahan selama bisa, istirahat sejenak, lalu ulang 2 kali.

Variasi : bertumpu di lengan

Side Plank

Alat yang diperlukan : tidak ada

Otot yang terdampak : obliques (otot samping perut), spinal stabilizers, quadriceps, glutes, serratus anterior, shoulder stabilizers, hip abductors

  1. Mulai dengan berbaring menyamping.
  2. Bertumpu pada lengan, dan naikkan badan.
  3. Tangan yang satu lagi bisa di samping, atau diluruskan ke atas
  4. Pastikan postur tubuh lurus dari ujung kaki sampai kepala
  5. Tahan selama mungkin, istirahat sejenak, lalu ganti sisi.

Variasi : tangan tumpuan diluruskan.

Bahaya fenomena “Pakar Medsos”

Dunia medsos jadi memungkinkan siapa saja untuk tampil di panggung. Ini seperti pisau bermata dua – ketika yang muncul adalah yang baik, maka jadi banyak yang bisa mendapatkan manfaatnya.

Namun jika yang muncul adalah yang buruk, atau jahat – maka juga jadi banyak yang bisa dirugikan, atau terzalimi.

Salah satu fenomena yang sudah cukup lama marak adalah para pakar palsu. Mengandalkan penampilan yang memukau, dan kata-kata yang manis – mereka menipu & mengecoh banyak orang.

Salah satunya adalah Edy Nurhan, dengan akun @edynurhan di berbagai medsos- videonya sedang viral di WhatsApp, karena dianggap menyampaikan info yang bagus tentang diabetes.

Padahal sebenarnya banyak yang salah, dan bahkan menyesatkan.

Beberapa yang langsung jelas misalnya adalah:

1/ Fungsi insulin ketika ada gula yang berlebih adalah mengubahnya menjadi lemak.

Karena itu kelebihan gula / karbohidrat menyebabkan kegemukan.

2/ Menyarankan kentang daripada french fries = SALAH, keduanya tetap saja sama-sama karbohidrat.

Bagi yang sedang musti diet karbohidrat, klaim-klaim seperti ini bisa berdampak fatal.

3/ Menyarankan jagung daripada popcorn = SALAH, keduanya tetap saja sama-sama karbohidrat.

Bagi yang sedang musti diet karbohidrat, klaim-klaim seperti ini bisa berdampak fatal.

4/ Klaim bahwa “olahraga adalah yang paling penting” – SALAH, yang paling penting adalah kualitas asupan makanan.

Ini sudah menjadi pemahaman umum di kalangan pakar kebugaran/ fitness.Olahraga sekeras apapun, tapi makanan / asupannya tidak diubah – maka dampaknya akan minim.

5/ Anggap diabetes lebih berbahaya daripada covid19 = lebih bodoh daripada orang awam, bisa menyesatkan, dan bisa menyebabkan korban nyawa.

Kedua-duanya sama = berbahaya.

Tidak boleh malah pakai salah satunya untuk remehkan yang lainnya.Itu adalah kelakuan yang amat serampangan.

Semoga para soothsayer seperti ini, orang bodoh tapi manis mulutnya – segera musnah dari medsos Indonesia ; sehingga tidak lagi bisa menyesatkan masyarakat awam.

ref: video ybs yang sedang menyebar di WhatsApp:

Travel & Dietary Requirements of Harry Sufehmi

I’ve been traveling quite a lot lately, and in turn have been asked the same questions many times by the event organizer. And being rather forgetful – sometimes I forgot some of it, and had to trouble the EO at the last minutes.

So to avoid that from happening again, here they are:

  1. Allergy : I’m allergic to (human skin) dust.

    Please ask the hotel staff to clean the room’s AC filter before my arrival.
    Otherwise my throat will inflame, and I’ll have trouble breathing.

    I’d prefer hotel with “split AC” / dedicated AC unit (because the filter can be cleaned).
    I’ve had many problems with hotel with Central AC facility – in many cases I fell sick, probably because its AC filter can only be cleaned by its vendor / technician.

    Also if there’s carpet / sofa in the hotel room – please ask the hotel staff to vacuum it, extra clean.
    My house does not have carpet because it keeps triggering this allergy. So if the hotel room have carpet, it’s alright, as long as it’s clean.
  2. Accommodation preference : Non-smoking, high floor, breakfast included.
  3. Food / dietary requirement : Halal, or vegetarian, or vegan.

    I have very, very low alcohol tolerance. Just a little bit of it is enough to make me nauseous.
    Please ask the caterer to ensure that there’s no alcohol in my food & drink.

    I’m not a picky eater, so don’t worry, I’ll appreciate anything that’s provided.
  4. Airplane : short flight = window seat. long flight (> 3 hours) = aisle seat.
  5. Train : preferably single seat / not having someone else next to me.
  6. Bus : sleeper seat if possible, otherwise anything is fine.
  7. Venue / Event : strictly non-smoking please – I suffer from asthma, cigarette’s smoke will cause me breathing problems.

Thank you.

Your WordPress Website is Slow? – perhaps you have WP-Statistics plugin installed?

A few days ago a client contacted me and said that their website is down. I checked the server, and indeed the server is very overloaded. In total it got 36 cores – and they’re all 100% utilized.

As usual I checked the whole stack, but today it’s something different – the MySQL / database server was the culprit. It was performing very slowly, and in turn caused the webserver to slow down as well.

In the slow query log, some queries kept showing up with crazy query times, in tens of seconds.
For comparison – all of the other queries finishes in less than a second.

MySQL’s slow query log is your friend – it enable you to find problematic queries very quickly.

And all those slow queries are in these tables:


I checked the currently running queries with “mysqladmin processlist“, and almost all (hundreds of them) queued queries are those involving those tables, looking for specific content in the field “ip”. They’re all looked like these:

SELECT location FROM wp_statistics_visitor WHERE ip = ''
SELECT * FROM wp_statistics_useronline WHERE ip = ''

On a hunch, I checked the structure of those tables. And right enough, there’s no index for “ip”

An index can increase a query’s performance by a, very, significant amount.

When the size of those tables are big enough, and you have a higher traffic than usual (they just published a very important information that’s of interest to a lot of people) – then suddenly these seemingly innocent queries were able to bring down a 36-core server to its knees.

Anyway, now we know the culprit, the solution is easy enough:

alter table wp_statistics_useronline add index (ip);
alter table wp_statistics_visitor add index (ip);

And voilà – in an instant, the website was up again, and the CPU utilization dropped to nearly zero.

Everyone’s happy, and I have also notified the developers about the issue as well.

Manajemen Finansial

For English (and other) speakers: this is a post about general financial management. Click on the “Translate” button on the right to have this article translated into your language.

Tidak sengaja saya membaca artikel tentang FI/RE (Financial Independence/Retiring Early). Di dunia yang ideal, semua orang punya akses ke UBI (Universal Basic Income) – yaitu dimana Pemerintah menjamin bahwa setiap bulan Anda akan selalu mendapatkan sejumlah uang.

Namun ketika ini belum ada, maka kita perlu melakukan berbagai usaha lainnya agar kondisi finansial keluarga kita selalu aman. Artikel ini adalah catatan pribadi saya tentang berbagai informasi terkait hal ini.

Disclaimer: artikel ini hanya catatan pribadi saya. Keuntungan/kerugian finansial yang terjadi karena membaca isi artikel ini bukan tanggung jawab saya. Dengan mengakses artikel ini, maka Anda telah setuju dengan disclaimer ini.

Mayoritas orang hidup dari gajian ke gajian. Jumlah tabungan minim. Atau kalaupun ada tabungan/investasi, kurang dikelola – sehingga kalah cepat dari besaran inflasi
= jumlah uang Anda malah berkurang setiap tahunnya ……

INFLASI – Target untuk dikalahkan

Selama 2010 s/d 2020, rata-rata inflasi per tahun adalah 4.48%

Artinya : nilai uang Anda berkurang sebesar 4.48% setiap tahun.

Maka, strategi investasi / tabungan Anda harus memberikan hasil yang lebih besar dari ini.
(setelah dipotong pajak, zakat, komisi, dst).

Mari kita lihat beberapa strategi investasi / tabungan yang ada:


Dengan rata-rata peningkatan nilai investasi sebesar 21,67% setiap tahun, emas jelas bisa mengalahkan gerogot inflasi pada harta Anda.



  1. Penyimpanan : Safe deposit bank : bukan jaminan aman, di luar negeri sudah sering terjadi kasus safe deposit box yang lenyap.
  2. Penyimpanan : Rumah : ukuran emas memang kecil sehingga mudah disembunyikan, namun jangan sampai lupa tempatnya. Keberadaan emas di rumah juga bisa membahayakan penghuni jika sampai diketahui oleh penjahat.


  1. Ukuran kecil, mudah disimpan.
  2. Cukup likuid / mudah dijual kembali, beda dengan misalnya perak.


  1. Resiko menyimpan benda berharga yang berbentuk fisik.
  2. Harganya cenderung agak mahal.
  3. Musti paham emas seperti apa yang harganya stabil – beberapa bentuk emas, seperti dinar dll, harganya bisa jatuh cukup banyak ketika dijual kembali.





  1. Jika tepat memilih lokasi, ROI (return on investment) bisa cukup tinggi.


  1. Sangat tergantung pada lokasi : jika salah pilih lokasi, malah bisa turun harga dan/atau sulit dijual kembali.
  2. Cenderung tidak likuid : susah dijual dalam waktu cepat, kecuali jika harganya diturunkan jauh di bawah harga pasar.
  3. Potensi resiko : mafia tanah : bukan sekali dua kali kejadian mendadak tanah sudah dikuasai oleh pihak lainnya, lengkap dengan sertifikat tanah asli.
  4. Modal tinggi : membutuhkan dana dalam jumlah besar untuk melakukan investasi jenis ini.



Tentu saja masih ada banyak sekali skema-skema tabungan & investasi lainnya. Misalnya di luar negeri ada berbagai institusi Fund Management yang bisa membantu mengelola dana Anda. Anda bisa turut bergabung dengan dana yang tidak besar dan cenderung aman.

Silakan jika ada saran / masukan, bisa disampaikan via Telegram atau Facebook di bawah ini.



Artikel ini masih belum selesai, karena saya masih terus mendalami soal ini.
Artikel ini akan terus diperbaharui setiap kali saya mendapatkan informasi baru.

Berbagai data & chart di artikel ini bisa dilihat di Google Sheet ini :

Performa Berbagai Investasi versus Inflasi Indonesia” =

Jika ada data yang ingin Anda sumbangkan, silakan kontak saya via Telegram di “sufehmi” (respons cepat), atau via Facebook (jarang saya cek).
Sumbangan data silakan dikirim dengan format CSV, dan lalu akan saya gabungkan ke dokumen di atas.

Jika ada kekeliruan / koreksi / masukan, jangan segan kontak saya via Telegram / Facebook di atas.

Demikian artikel ini, semoga bermanfaat bagi Anda.

Referensi / Bacaan tambahan


Cloud and DRC

In my years of experience as IT architect, it’s quite shocking to see how many institutions are slacking about their backup system once they moved to the cloud. Especially with their DRC (disaster recovery center). They thought that once they go “up” to the cloud, then it’s all right. No need to worry anymore with troublesome stuff such as backup.

As harsh as it may sound, my friend said that “cloud is other people’s computer”, and it’s a fact. And computer will fail. It’s just a matter of when, not if. And cloud did indeed fail from time to time.

When your organization does not have a solid backup system, then when the cloud fail – you are in for a very unpleasant experience.

“There’s no such thing as too much backup” – this is another principle that’s true. I have been in various data loss incidents, one of them were saved by the fifth (5th) backup mechanism. All other four failed.

But of course the implementation of the backup system will need to balance between levels of data safety and actual available resources.

A DRC can be of various shapes and sizes, customized to fit one’s system recovery needs versus available resources/budget. There are 8 levels of Disaster Preparedness, and we can choose the one that fits our needs & available resources.
But it simply has to exist. Any institution with data & systems considered important, need to have a working DRC facility.

And a DRC does not always have to be complex or expensive. There are ways to make a fully working DRC with minimum resources. And along time, it can be tweaked even further.

Moving to the cloud is not an excuse to avoid having a good backup strategy. We don’t need to be caught with our pants down.

Linux and Logitech MX Anywhere 3

This mouse feels good to touch. It just feels nice. The scroll wheel, called Magspeed Wheel, feels really good to use with its tactile feeling. However try pressing the black button in the middle – then it changed from Ratchet mode to Freespin mode , basically it flies. You can spin it really, really fast. Awesome.

However, Logitech does not provide its configuration software on Linux. But no worries, we can use LogiOps for that.

Copy-paste these lines to set it up ; these are for Ubuntu 20.04, if you’re using different Linux distro, you might need to change some of it.

sudo apt-get install -y install cmake libevdev-dev libudev-dev libconfig++-dev

cd /tmp ; wget ; mkdir tmp ; cd tmp ; unzip ../ ; cd logiops-master ; mkdir build ; cd build

cmake .. ; make ; sudo make install 

sudo nano /etc/logid.cfg

sudo systemctl enable --now logid

You may notice that we’re creating a configuration file name logid.cfg , there’s a [ guide to create it ], however some may find it confusing.

Therefore please find a sample logid.cfg for MX Anywhere 3 below. It will enable a reasonably nice usage of the device, and also enable you to change the mouse’s DPI by pressing the side buttons.


// Logiops (Linux driver) configuration for Logitech MX Master 3.
// Includes gestures, smartshift, DPI.
// Tested on logid v0.2.2-35-g1c209ed.

// File location: /etc/logid.cfg

devices: ({
  name: "MX Anywhere 3";

  smartshift: {
    on: true;
    threshold: 15;

  hiresscroll: {
    hires: true;
    invert: false;
    target: true;
       up: {
            mode: "Axis";
            axis: "REL_WHEEL_HI_RES";
            axis_multiplier: 1;
        down: {
            mode: "Axis";
            axis: "REL_WHEEL_HI_RES";
            axis_multiplier: -1;

  dpi: 1600; // max=4000

    buttons: (
            cid: 0x52;
            action =
                type: "Gestures";
                gestures: (
                        direction: "Left";
                        mode: "OnInterval";
			interval: 10;
                        action =
                            type: "Keypress";
                            keys: ["KEY_VOLUMEDOWN"];
                        direction: "Right";
                        mode: "OnInterval";
			interval: 10;
                        action =
                            type: "Keypress";
                            keys: ["KEY_VOLUMEUP"];
                        direction: "None"
                        mode: "OnRelease";
                        action =
			    type: "Keypress";
			    keys: ["BTN_MIDDLE"];
            cid: 0x53;
            action =
//                type: "Keypress";
//                keys: ["KEY_BACK"];
		type: "ChangeDPI";
              	inc: -1000;            
            cid: 0x56;
            action =
//                type: "Keypress";
//                keys: ["KEY_FORWARD"];
		type: "ChangeDPI";
              	inc: 1000;            

How to play Roblox on Linux

I used to play Roblox with my kids from my Linux-based (Ubuntu) laptop – by having a virtual machine set up (on VirtualBox) with Windows, and play there. Of course it’s slow, but I still CAN play.

However one day Roblox decided to disable playing from Virtual Machine. And there goes my little bit of happiness with my children.

One day I found this article about using Steam Link to enable remote access to a Windows desktop, and it got me an idea – can I use this trick to play Roblox again from my laptop?

So I setup Google Chrome to be streamable from my gaming computer on my house’s second floor – and voila, it showed up in my Steam account indeed !

By invoking Google Chrome on my Steam app on Linux – then I can browse to, and then play all the games there.

Mission accomplished !

Use Steam to Stream Your Desktop Instead of Your Games :

Setup OpenVPN Server on Proxmox LXC

I needed to do this, but all the tutorials that I could find are incomplete, or already outdated, such as this.

After hacking around for a while, here’s how to correctly setup OpenVPN server in a container on Proxmox:

(btw if you just need to setup an OpenVPN Server in a normal server / non-container, then just do the “in container” part below)


# create special device "tun" for OpenVPN
mkdir -p /devcontainer/net
mknod /devcontainer/net/tun c 10 200
chown 100000:100000 /devcontainer/net/tun

# enable your container to use that tun device
# change 124 into your container's number : pct list
echo "lxc.mount.entry: /devcontainer/net dev/net none bind,create=dir" >> /etc/pve/lxc/124.conf

# forward OpenVPN traffic to your container's IP address
# change to your container's IP address
iptables -t nat -A PREROUTING -i vmbr0 -p tcp -m tcp --dport 1194 -j DNAT --to-destination

iptables -t nat -A PREROUTING -i vmbr0 -p udp -m udp --dport 1194 -j DNAT --to-destination

iptables -t nat -A PREROUTING -i vmbr1 -p tcp -m tcp --dport 53 -j DNAT --to-destination

# save iptables's rule
iptables-save > /etc/iptables.rules


# execute the automated OpenVPN installation script 
mkdir /root/scripts
cd /root/scripts

wget --no-check-certificate -O ; chmod +x ; ./
# if you'd like to change the default IP address, do this :
# vi
# :%s/10.8.0/10.88.0/g

# setup NAT, so the OpenVPN clients can connect to the internet 
# while connected to this OpenVPN server
iptables -I POSTROUTING -t nat -s -j MASQUERADE

# save iptables's rule
iptables-save > /etc/iptables.rules

After executing the /root/scripts/ script , it will result in a file with ovpn extension

Download that to your computer / client,
install OpenVPN client,
and use that ovpn file as the configuration

Enjoy !

In case that very helpful OpenVPN Server install script suddenly disappear, here it is :

# Copyright (c) 2013 Nyr. Released under the MIT License.

# Detect Debian users running the script with "sh" instead of bash
if readlink /proc/$$/exe | grep -q "dash"; then
	echo 'This installer needs to be run with "bash", not "sh".'

# Discard stdin. Needed when running from an one-liner which includes a newline
read -N 999999 -t 0.001

# Detect OpenVZ 6
if [[ $(uname -r | cut -d "." -f 1) -eq 2 ]]; then
	echo "The system is running an old kernel, which is incompatible with this installer."

# Detect OS
# $os_version variables aren't always in use, but are kept here for convenience
if grep -qs "ubuntu" /etc/os-release; then
	os_version=$(grep 'VERSION_ID' /etc/os-release | cut -d '"' -f 2 | tr -d '.')
elif [[ -e /etc/debian_version ]]; then
	os_version=$(grep -oE '[0-9]+' /etc/debian_version | head -1)
elif [[ -e /etc/centos-release ]]; then
	os_version=$(grep -oE '[0-9]+' /etc/centos-release | head -1)
elif [[ -e /etc/fedora-release ]]; then
	os_version=$(grep -oE '[0-9]+' /etc/fedora-release | head -1)
	echo "This installer seems to be running on an unsupported distribution.
Supported distributions are Ubuntu, Debian, CentOS, and Fedora."

if [[ "$os" == "ubuntu" && "$os_version" -lt 1804 ]]; then
	echo "Ubuntu 18.04 or higher is required to use this installer.
This version of Ubuntu is too old and unsupported."

if [[ "$os" == "debian" && "$os_version" -lt 9 ]]; then
	echo "Debian 9 or higher is required to use this installer.
This version of Debian is too old and unsupported."

if [[ "$os" == "centos" && "$os_version" -lt 7 ]]; then
	echo "CentOS 7 or higher is required to use this installer.
This version of CentOS is too old and unsupported."

# Detect environments where $PATH does not include the sbin directories
if ! grep -q sbin <<< "$PATH"; then
	echo '$PATH does not include sbin. Try using "su -" instead of "su".'

if [[ "$EUID" -ne 0 ]]; then
	echo "This installer needs to be run with superuser privileges."

if [[ ! -e /dev/net/tun ]] || ! ( exec 7<>/dev/net/tun ) 2>/dev/null; then
	echo "The system does not have the TUN device available.
TUN needs to be enabled before running this installer."

new_client () {
	# Generates the custom client.ovpn
	cat /etc/openvpn/server/client-common.txt
	echo "<ca>"
	cat /etc/openvpn/server/easy-rsa/pki/ca.crt
	echo "</ca>"
	echo "<cert>"
	sed -ne '/BEGIN CERTIFICATE/,$ p' /etc/openvpn/server/easy-rsa/pki/issued/"$client".crt
	echo "</cert>"
	echo "<key>"
	cat /etc/openvpn/server/easy-rsa/pki/private/"$client".key
	echo "</key>"
	echo "<tls-crypt>"
	sed -ne '/BEGIN OpenVPN Static key/,$ p' /etc/openvpn/server/tc.key
	echo "</tls-crypt>"
	} > ~/"$client".ovpn

if [[ ! -e /etc/openvpn/server/server.conf ]]; then
	echo 'Welcome to this OpenVPN road warrior installer!'
	# If system has a single IPv4, it is selected automatically. Else, ask the user
	if [[ $(ip -4 addr | grep inet | grep -vEc '127(\.[0-9]{1,3}){3}') -eq 1 ]]; then
		ip=$(ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}')
		number_of_ip=$(ip -4 addr | grep inet | grep -vEc '127(\.[0-9]{1,3}){3}')
		echo "Which IPv4 address should be used?"
		ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | nl -s ') '
		read -p "IPv4 address [1]: " ip_number
		until [[ -z "$ip_number" || "$ip_number" =~ ^[0-9]+$ && "$ip_number" -le "$number_of_ip" ]]; do
			echo "$ip_number: invalid selection."
			read -p "IPv4 address [1]: " ip_number
		[[ -z "$ip_number" ]] && ip_number="1"
		ip=$(ip -4 addr | grep inet | grep -vE '127(\.[0-9]{1,3}){3}' | cut -d '/' -f 1 | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | sed -n "$ip_number"p)
	# If $ip is a private IP address, the server must be behind NAT
	if echo "$ip" | grep -qE '^(10\.|172\.1[6789]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168)'; then
		echo "This server is behind NAT. What is the public IPv4 address or hostname?"
		# Get public IP and sanitize with grep
		get_public_ip=$(grep -m 1 -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' <<< "$(wget -T 10 -t 1 -4qO- "" || curl -m 10 -4Ls "")")
		read -p "Public IPv4 address / hostname [$get_public_ip]: " public_ip
		# If the checkip service is unavailable and user didn't provide input, ask again
		until [[ -n "$get_public_ip" || -n "$public_ip" ]]; do
			echo "Invalid input."
			read -p "Public IPv4 address / hostname: " public_ip
		[[ -z "$public_ip" ]] && public_ip="$get_public_ip"
	# If system has a single IPv6, it is selected automatically
	if [[ $(ip -6 addr | grep -c 'inet6 [23]') -eq 1 ]]; then
		ip6=$(ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}')
	# If system has multiple IPv6, ask the user to select one
	if [[ $(ip -6 addr | grep -c 'inet6 [23]') -gt 1 ]]; then
		number_of_ip6=$(ip -6 addr | grep -c 'inet6 [23]')
		echo "Which IPv6 address should be used?"
		ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}' | nl -s ') '
		read -p "IPv6 address [1]: " ip6_number
		until [[ -z "$ip6_number" || "$ip6_number" =~ ^[0-9]+$ && "$ip6_number" -le "$number_of_ip6" ]]; do
			echo "$ip6_number: invalid selection."
			read -p "IPv6 address [1]: " ip6_number
		[[ -z "$ip6_number" ]] && ip6_number="1"
		ip6=$(ip -6 addr | grep 'inet6 [23]' | cut -d '/' -f 1 | grep -oE '([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}' | sed -n "$ip6_number"p)
	echo "Which protocol should OpenVPN use?"
	echo "   1) UDP (recommended)"
	echo "   2) TCP"
	read -p "Protocol [1]: " protocol
	until [[ -z "$protocol" || "$protocol" =~ ^[12]$ ]]; do
		echo "$protocol: invalid selection."
		read -p "Protocol [1]: " protocol
	case "$protocol" in
	echo "What port should OpenVPN listen to?"
	read -p "Port [1194]: " port
	until [[ -z "$port" || "$port" =~ ^[0-9]+$ && "$port" -le 65535 ]]; do
		echo "$port: invalid port."
		read -p "Port [1194]: " port
	[[ -z "$port" ]] && port="1194"
	echo "Select a DNS server for the clients:"
	echo "   1) Current system resolvers"
	echo "   2) Google"
	echo "   3)"
	echo "   4) OpenDNS"
	echo "   5) Quad9"
	echo "   6) AdGuard"
	read -p "DNS server [1]: " dns
	until [[ -z "$dns" || "$dns" =~ ^[1-6]$ ]]; do
		echo "$dns: invalid selection."
		read -p "DNS server [1]: " dns
	echo "Enter a name for the first client:"
	read -p "Name [client]: " unsanitized_client
	# Allow a limited set of characters to avoid conflicts
	client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
	[[ -z "$client" ]] && client="client"
	echo "OpenVPN installation is ready to begin."
	# Install a firewall in the rare case where one is not already available
	if ! systemctl is-active --quiet firewalld.service && ! hash iptables 2>/dev/null; then
		if [[ "$os" == "centos" || "$os" == "fedora" ]]; then
			# We don't want to silently enable firewalld, so we give a subtle warning
			# If the user continues, firewalld will be installed and enabled during setup
			echo "firewalld, which is required to manage routing tables, will also be installed."
		elif [[ "$os" == "debian" || "$os" == "ubuntu" ]]; then
			# iptables is way less invasive than firewalld so no warning is given
	read -n1 -r -p "Press any key to continue..."
	# If running inside a container, disable LimitNPROC to prevent conflicts
	if systemd-detect-virt -cq; then
		mkdir /etc/systemd/system/openvpn-server@server.service.d/ 2>/dev/null
		echo "[Service]
LimitNPROC=infinity" > /etc/systemd/system/openvpn-server@server.service.d/disable-limitnproc.conf
	if [[ "$os" = "debian" || "$os" = "ubuntu" ]]; then
		apt-get update
		apt-get install -y openvpn openssl ca-certificates $firewall
	elif [[ "$os" = "centos" ]]; then
		yum install -y epel-release
		yum install -y openvpn openssl ca-certificates tar $firewall
		# Else, OS must be Fedora
		dnf install -y openvpn openssl ca-certificates tar $firewall
	# If firewalld was just installed, enable it
	if [[ "$firewall" == "firewalld" ]]; then
		systemctl enable --now firewalld.service
	# Get easy-rsa
	mkdir -p /etc/openvpn/server/easy-rsa/
	{ wget -qO- "$easy_rsa_url" 2>/dev/null || curl -sL "$easy_rsa_url" ; } | tar xz -C /etc/openvpn/server/easy-rsa/ --strip-components 1
	chown -R root:root /etc/openvpn/server/easy-rsa/
	cd /etc/openvpn/server/easy-rsa/
	# Create the PKI, set up the CA and the server and client certificates
	./easyrsa init-pki
	./easyrsa --batch build-ca nopass
	EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-server-full server nopass
	EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full "$client" nopass
	EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl
	# Move the stuff we need
	cp pki/ca.crt pki/private/ca.key pki/issued/server.crt pki/private/server.key pki/crl.pem /etc/openvpn/server
	# CRL is read with each client connection, while OpenVPN is dropped to nobody
	chown nobody:"$group_name" /etc/openvpn/server/crl.pem
	# Without +x in the directory, OpenVPN can't run a stat() on the CRL file
	chmod o+x /etc/openvpn/server/
	# Generate key for tls-crypt
	openvpn --genkey --secret /etc/openvpn/server/tc.key
	# Create the DH parameters file using the predefined ffdhe2048 group
	echo '-----BEGIN DH PARAMETERS-----
-----END DH PARAMETERS-----' > /etc/openvpn/server/dh.pem
	# Generate server.conf
	echo "local $ip
port $port
proto $protocol
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
auth SHA512
tls-crypt tc.key
topology subnet
server" > /etc/openvpn/server/server.conf
	# IPv6
	if [[ -z "$ip6" ]]; then
		echo 'push "redirect-gateway def1 bypass-dhcp"' >> /etc/openvpn/server/server.conf
		echo 'server-ipv6 fddd:1194:1194:1194::/64' >> /etc/openvpn/server/server.conf
		echo 'push "redirect-gateway def1 ipv6 bypass-dhcp"' >> /etc/openvpn/server/server.conf
	echo 'ifconfig-pool-persist ipp.txt' >> /etc/openvpn/server/server.conf
	# DNS
	case "$dns" in
			# Locate the proper resolv.conf
			# Needed for systems running systemd-resolved
			if grep -q '^nameserver' "/etc/resolv.conf"; then
			# Obtain the resolvers from resolv.conf and use them for OpenVPN
			grep -v '^#\|^;' "$resolv_conf" | grep '^nameserver' | grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' | while read line; do
				echo "push \"dhcp-option DNS $line\"" >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
			echo 'push "dhcp-option DNS"' >> /etc/openvpn/server/server.conf
	echo "keepalive 10 120
cipher AES-256-CBC
user nobody
group $group_name
status openvpn-status.log
verb 3
crl-verify crl.pem" >> /etc/openvpn/server/server.conf
	if [[ "$protocol" = "udp" ]]; then
		echo "explicit-exit-notify" >> /etc/openvpn/server/server.conf
	# Enable net.ipv4.ip_forward for the system
	echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/30-openvpn-forward.conf
	# Enable without waiting for a reboot or service restart
	echo 1 > /proc/sys/net/ipv4/ip_forward
	if [[ -n "$ip6" ]]; then
		# Enable net.ipv6.conf.all.forwarding for the system
		echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.d/30-openvpn-forward.conf
		# Enable without waiting for a reboot or service restart
		echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
	if systemctl is-active --quiet firewalld.service; then
		# Using both permanent and not permanent rules to avoid a firewalld
		# reload.
		# We don't use --add-service=openvpn because that would only work with
		# the default port and protocol.
		firewall-cmd --add-port="$port"/"$protocol"
		firewall-cmd --zone=trusted --add-source=
		firewall-cmd --permanent --add-port="$port"/"$protocol"
		firewall-cmd --permanent --zone=trusted --add-source=
		# Set NAT for the VPN subnet
		firewall-cmd --direct --add-rule ipv4 nat POSTROUTING 0 -s ! -d -j SNAT --to "$ip"
		firewall-cmd --permanent --direct --add-rule ipv4 nat POSTROUTING 0 -s ! -d -j SNAT --to "$ip"
		if [[ -n "$ip6" ]]; then
			firewall-cmd --zone=trusted --add-source=fddd:1194:1194:1194::/64
			firewall-cmd --permanent --zone=trusted --add-source=fddd:1194:1194:1194::/64
			firewall-cmd --direct --add-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
			firewall-cmd --permanent --direct --add-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
		# Create a service to set up persistent iptables rules
		iptables_path=$(command -v iptables)
		ip6tables_path=$(command -v ip6tables)
		# nf_tables is not available as standard in OVZ kernels. So use iptables-legacy
		# if we are in OVZ, with a nf_tables backend and iptables-legacy is available.
		if [[ $(systemd-detect-virt) == "openvz" ]] && readlink -f "$(command -v iptables)" | grep -q "nft" && hash iptables-legacy 2>/dev/null; then
			iptables_path=$(command -v iptables-legacy)
			ip6tables_path=$(command -v ip6tables-legacy)
		echo "[Unit]
ExecStart=$iptables_path -t nat -A POSTROUTING -s ! -d -j SNAT --to $ip
ExecStart=$iptables_path -I INPUT -p $protocol --dport $port -j ACCEPT
ExecStart=$iptables_path -I FORWARD -s -j ACCEPT
ExecStart=$iptables_path -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
ExecStop=$iptables_path -t nat -D POSTROUTING -s ! -d -j SNAT --to $ip
ExecStop=$iptables_path -D INPUT -p $protocol --dport $port -j ACCEPT
ExecStop=$iptables_path -D FORWARD -s -j ACCEPT
ExecStop=$iptables_path -D FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT" > /etc/systemd/system/openvpn-iptables.service
		if [[ -n "$ip6" ]]; then
			echo "ExecStart=$ip6tables_path -t nat -A POSTROUTING -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to $ip6
ExecStart=$ip6tables_path -I FORWARD -s fddd:1194:1194:1194::/64 -j ACCEPT
ExecStart=$ip6tables_path -I FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
ExecStop=$ip6tables_path -t nat -D POSTROUTING -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to $ip6
ExecStop=$ip6tables_path -D FORWARD -s fddd:1194:1194:1194::/64 -j ACCEPT
ExecStop=$ip6tables_path -D FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT" >> /etc/systemd/system/openvpn-iptables.service
		echo "RemainAfterExit=yes
[Install]" >> /etc/systemd/system/openvpn-iptables.service
		systemctl enable --now openvpn-iptables.service
	# If SELinux is enabled and a custom port was selected, we need this
	if sestatus 2>/dev/null | grep "Current mode" | grep -q "enforcing" && [[ "$port" != 1194 ]]; then
		# Install semanage if not already present
		if ! hash semanage 2>/dev/null; then
			if [[ "$os_version" -eq 7 ]]; then
				# Centos 7
				yum install -y policycoreutils-python
				# CentOS 8 or Fedora
				dnf install -y policycoreutils-python-utils
		semanage port -a -t openvpn_port_t -p "$protocol" "$port"
	# If the server is behind NAT, use the correct IP address
	[[ -n "$public_ip" ]] && ip="$public_ip"
	# client-common.txt is created so we have a template to add further users later
	echo "client
dev tun
proto $protocol
remote $ip $port
resolv-retry infinite
remote-cert-tls server
auth SHA512
cipher AES-256-CBC
ignore-unknown-option block-outside-dns
verb 3" > /etc/openvpn/server/client-common.txt
	# Enable and start the OpenVPN service
	systemctl enable --now openvpn-server@server.service
	# Generates the custom client.ovpn
	echo "Finished!"
	echo "The client configuration is available in:" ~/"$client.ovpn"
	echo "New clients can be added by running this script again."
	echo "OpenVPN is already installed."
	echo "Select an option:"
	echo "   1) Add a new client"
	echo "   2) Revoke an existing client"
	echo "   3) Remove OpenVPN"
	echo "   4) Exit"
	read -p "Option: " option
	until [[ "$option" =~ ^[1-4]$ ]]; do
		echo "$option: invalid selection."
		read -p "Option: " option
	case "$option" in
			echo "Provide a name for the client:"
			read -p "Name: " unsanitized_client
			client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
			while [[ -z "$client" || -e /etc/openvpn/server/easy-rsa/pki/issued/"$client".crt ]]; do
				echo "$client: invalid name."
				read -p "Name: " unsanitized_client
				client=$(sed 's/[^0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-]/_/g' <<< "$unsanitized_client")
			cd /etc/openvpn/server/easy-rsa/
			EASYRSA_CERT_EXPIRE=3650 ./easyrsa build-client-full "$client" nopass
			# Generates the custom client.ovpn
			echo "$client added. Configuration available in:" ~/"$client.ovpn"
			# This option could be documented a bit better and maybe even be simplified
			# ...but what can I say, I want some sleep too
			number_of_clients=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep -c "^V")
			if [[ "$number_of_clients" = 0 ]]; then
				echo "There are no existing clients!"
			echo "Select the client to revoke:"
			tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | nl -s ') '
			read -p "Client: " client_number
			until [[ "$client_number" =~ ^[0-9]+$ && "$client_number" -le "$number_of_clients" ]]; do
				echo "$client_number: invalid selection."
				read -p "Client: " client_number
			client=$(tail -n +2 /etc/openvpn/server/easy-rsa/pki/index.txt | grep "^V" | cut -d '=' -f 2 | sed -n "$client_number"p)
			read -p "Confirm $client revocation? [y/N]: " revoke
			until [[ "$revoke" =~ ^[yYnN]*$ ]]; do
				echo "$revoke: invalid selection."
				read -p "Confirm $client revocation? [y/N]: " revoke
			if [[ "$revoke" =~ ^[yY]$ ]]; then
				cd /etc/openvpn/server/easy-rsa/
				./easyrsa --batch revoke "$client"
				EASYRSA_CRL_DAYS=3650 ./easyrsa gen-crl
				rm -f /etc/openvpn/server/crl.pem
				cp /etc/openvpn/server/easy-rsa/pki/crl.pem /etc/openvpn/server/crl.pem
				# CRL is read with each client connection, when OpenVPN is dropped to nobody
				chown nobody:"$group_name" /etc/openvpn/server/crl.pem
				echo "$client revoked!"
				echo "$client revocation aborted!"
			read -p "Confirm OpenVPN removal? [y/N]: " remove
			until [[ "$remove" =~ ^[yYnN]*$ ]]; do
				echo "$remove: invalid selection."
				read -p "Confirm OpenVPN removal? [y/N]: " remove
			if [[ "$remove" =~ ^[yY]$ ]]; then
				port=$(grep '^port ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
				protocol=$(grep '^proto ' /etc/openvpn/server/server.conf | cut -d " " -f 2)
				if systemctl is-active --quiet firewalld.service; then
					ip=$(firewall-cmd --direct --get-rules ipv4 nat POSTROUTING | grep '\-s '"'"'!'"'"' -d' | grep -oE '[^ ]+$')
					# Using both permanent and not permanent rules to avoid a firewalld reload.
					firewall-cmd --remove-port="$port"/"$protocol"
					firewall-cmd --zone=trusted --remove-source=
					firewall-cmd --permanent --remove-port="$port"/"$protocol"
					firewall-cmd --permanent --zone=trusted --remove-source=
					firewall-cmd --direct --remove-rule ipv4 nat POSTROUTING 0 -s ! -d -j SNAT --to "$ip"
					firewall-cmd --permanent --direct --remove-rule ipv4 nat POSTROUTING 0 -s ! -d -j SNAT --to "$ip"
					if grep -qs "server-ipv6" /etc/openvpn/server/server.conf; then
						ip6=$(firewall-cmd --direct --get-rules ipv6 nat POSTROUTING | grep '\-s fddd:1194:1194:1194::/64 '"'"'!'"'"' -d fddd:1194:1194:1194::/64' | grep -oE '[^ ]+$')
						firewall-cmd --zone=trusted --remove-source=fddd:1194:1194:1194::/64
						firewall-cmd --permanent --zone=trusted --remove-source=fddd:1194:1194:1194::/64
						firewall-cmd --direct --remove-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
						firewall-cmd --permanent --direct --remove-rule ipv6 nat POSTROUTING 0 -s fddd:1194:1194:1194::/64 ! -d fddd:1194:1194:1194::/64 -j SNAT --to "$ip6"
					systemctl disable --now openvpn-iptables.service
					rm -f /etc/systemd/system/openvpn-iptables.service
				if sestatus 2>/dev/null | grep "Current mode" | grep -q "enforcing" && [[ "$port" != 1194 ]]; then
					semanage port -d -t openvpn_port_t -p "$protocol" "$port"
				systemctl disable --now openvpn-server@server.service
				rm -rf /etc/openvpn/server
				rm -f /etc/systemd/system/openvpn-server@server.service.d/disable-limitnproc.conf
				rm -f /etc/sysctl.d/30-openvpn-forward.conf
				if [[ "$os" = "debian" || "$os" = "ubuntu" ]]; then
					apt-get remove --purge -y openvpn
					# Else, OS must be CentOS or Fedora
					yum remove -y openvpn
				echo "OpenVPN removed!"
				echo "OpenVPN removal aborted!"

OBS (Open Broadcasting Software) As Video source (for Zoom, Google Meet, Skype, etc) in Ubuntu 20.04

I used to use my Android smartphone as webcam for Zoom / Skype / Google Meet / etc because my laptop’s webcam is so bad. This is possible thanks to the Droidcam app.

But sometimes there are problems, like the wifi got interference so the video would slow down or freeze for a while. And for long conference / meeting, it got my phone pretty hot because sometimes I have to charge it. And of course I can’t use my phone while it’s being used as webcam.

So I bought Logitech C920 Pro webcam, and started using it instead. In Linux it’s recognized and can bs used straight away.
But you may need to tweak its image quality a bit using guvcview before being used for work.

The picture quality is not as good as my smartphone’s , because my smartphone is heavily processing the images, so it came out even with HDR quality, in real-time. But as a daily work webcam, this Logitech webcam is good enough.

Then I need to start using Green screen as well with this webcam. There’s one problem – my Green screen is not wide enough to cover the webcam’s wide angle.

With Droidcam, this is not a problem, there’s a “Zoom” feature. So I just Zoom-in, until the green screen fills the view.
But since Logitech does not provide any kind of software for this webcam on Linux, I use OBS instead.

Using OBS, I can set up green screen in it, so we don’t need to use Zoom’s green screen / Virtual Background feature. And also that means all other software (Google Meet, Skype, etc) will automatically got the already green screened video from OBS.

To zoom-in in OBS, I just enlarge the webcam’s image box, until the green screen fill the view.
To activate green screen, I use the Chroma key filter.

choose Tools – V4L2 Video Output to enable OBS as Video Source for other software
Make sure to tick the “Auto Start” option
(no green screen tho when I took this screenshot)

To make OBS become a video source, we’ll need to install obs-v4l2sink :

Turned out there are a few problems installing it in Ubuntu 20.04 , we’ll discuss here the workaround for those:

# Another possible solution is using Snap's version of OBS, 
# which already include v4l2loopback kernel module 
# & obs-v4l2sink plugin
# sudo snap install obs-studio.
# sudo modprobe v4l2loopback video_nr=10 card_label=”OBS Video Source” exclusive_caps=1

# If by any reason you can't use this Snap-based solution, 
# then continue : 

# download needed software for compilation
sudo apt-get update ; sudo apt-get install -y install obs-studio git cmake build-essential libobs-dev ffmpeg qtbase5-dev

cd /tmp ; mkdir myobscode ; cd myobscode

# get OBS' source code
git clone --recursive

# get plugin's source code
git clone

# compile the OBS plugin
cd ~/obs-v4l2sink
mkdir build && cd build
cmake -DLIBOBS_INCLUDE_DIR="../../obs-studio/libobs" -DCMAKE_INSTALL_PREFIX=/usr ..

make -j4
sudo make install
sudo cp /usr/lib/obs-plugins/
sudo cp /usr/lib/obs-plugins/ /usr/lib/x86_64-linux-gnu/obs-plugins/

# Turned out we need to compile and build v4l2loopback by ourselves - this is because the Ubuntu's version is too old
# Thanks to user jplandrain : 
cd ..
sudo apt-get remove v4l2loopback-dkms
git clone --branch v0.12.5
cd v4l2loopback
make && sudo make install

# make v4l2loopback automatically loaded by kernel after reboot
echo "v4l2loopback" >> /etc/modules-load.d/modules.conf
echo 'options v4l2loopback video_nr=2' >> /etc/modprobe.d/v4l2loopback.conf

echo 'options v4l2loopback card_label="VirtualCam"' >> /etc/modprobe.d/v4l2loopback.conf

echo 'options v4l2loopback exclusive_caps=1' >> /etc/modprobe.d/v4l2loopback.conf

# load the loopback module into kernel now
sudo modprobe v4l2loopback video_nr=10 card_label="OBS Video Source" exclusive_caps=1

# start OBS - now there should be a new menu : 
#     Tools - V4L2 Video Output
# also you'll need to tick "Autostart" option after choosing that menu
obs &

How to run Proxmox with only a single public IP address

IPv4 address is becoming rarer by each day. In some cases, it can be pretty hard to get multiple IPv4 address for your Proxmox server.

Thankfully, Proxmox is basically a Debian Linux OS with Proxmox layer on top of that. So that gives us quite a lot of flexibility.

This tutorial will help you to create a fully functional Proxmox server running multiple containers & virtual machines, using only a single IPv4 address.

These are the main steps :

  1. Create port forwarding rules
  2. Make sure it’s executed automatically everytime the server is restarted
  3. Setup a reverse-proxy server : to forward HTTP/S requests to the correct container / virtual machine
  4. Setup HTTPS

For CT (container) / VM (virtual machine) that contains webserver, point 3 is important – because there’s only one public IP address, so there’s only one port 80 and 443 that’s facing the Internet.

By forwarding port 80 and 443 to a reverse-proxy in a CT, then we’ll be able to forward incoming visitors, by hostname / domain name, to the correct CT/VM.


Modify the following to match your host’s interface name & CT/VM’s internal IP addresses, then copy-paste to terminal :

###### All HTTP/S traffic are forwarded to reverse proxy
iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 80 -j DNAT --to

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 443 -j DNAT --to

###### SSH ports to each existing CT/VM
iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22101 -j DNAT --to

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22102 -j DNAT --to

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22103 -j DNAT --to

iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport 22104 -j DNAT --to

Then we save it :

iptables-save > /etc/iptables.rules


Edit /etc/network/interfaces file, find your network interface name that’s facing the Internet (in my case, vmbr0) – then add the pre-up line as follows :

auto vmbr0
pre-up iptables-restore < /etc/iptables.rules


In a CT, install Nginx. Then for each domain, create a configuration file like this, for example: /etc/nginx/sites-available/ :

server {
listen 80;

location / {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;


To activate it (assuming you’re using Ubuntu) link it to /etc/nginx/sites-enabled/ , then restart Nginx :

ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/

/etc/init.d/nginx restart

note: as noted before, all HTTP/s traffic will have to go through this reverse-proxy. You may wish to tune this Nginx installation accordingly.


It’s very easy with Let’s Encrypt once you’ve done point 3 above. Do the following on the reverse-proxy CT :

sudo apt-get update ; sudo apt-get install -y certbot python3-certbot-nginx

sudo certbot --nginx

sudo /etc/init.d/nginx restart


Installing HTTrack on Ubuntu from Source

Today I needed to have the latest version of HTTrack installed to make a (static) mirror of a website that I managed

After a few attempts, this is how you compile & install HTTrack from source on Ubuntu :

wget ""

mv cserv.php3\?File\=httrack.tar.gz  httrack.tar.gz

tar xzvf httrack.tar.gz

cd httrack-3.49.2/

### the following is the key to a successful install
apt-get install zlib1g-dev libssl-dev build-essential

./configure && make && make install

Cutting a Table out of a mysqldump output file

I was restoring the backup of a MySQL 5.x server into MySQL 8.x server – and found out that it corrupt the MySQL 8.x ‘s mysql table

Which stores the usernames and passwords.

So I had to delete the mysql table from the backup, before trying to restore it again

Turn out it’s pretty easy, just will take some time since it’s a pretty big backup :

# search for beginning of 'mysql' table
cat backup.mysql | grep -n Current Database: `mysql`

# 155604:-- Current Database: `mysql`

# search for ending of 'mysql' table
tail -n +155604 backup.mysql | grep -n "Current Database"

# 1:  -- Current Database: `mysql`
# 916:-- Current Database: `phpmyadmin`

# cut that table out
head -155603 backup.mysql                > new.mysql
tail -n +$(( 155603+916 )) backup.mysql >> new.mysql

# voila !

Crontab runs on different timezone : here’s the fix

A few days ago I got reports that a server is running its cron jobs at strange times. Logged in, and indeed it was. A huge backup was running during peak hours. Saying that it disrupt people’s work is an understatement.

To my surprise, the explanation for this issue can not be found straightaway. Took some googling to find the cause. And even more time to find the correct solution.

So to cut the chase – /etc/localtime was a link to /usr/share/zoneinfo/America/NewYork

Changed it to /usr/share/zoneinfo/Asia/Jakarta – and voila, now the cronjobs are running at the correct times.

Hope it helps

XCTB – X Compression Tool Benchmarker

I deal with a lot of big files at work. While storage capacity is not infinite indeed. So it’s in my interest to keep the file sizes as low as possible.

One way to achieve that is by using compression. Especially when dealing with log files, or database archive, you can save a ton of space with the right compression tool.

But space saving is not the only consideration.

You also need to weighs in other factors. Such as :

  • File type : different tool will compress different type of file differently
  • CPU multi-core capabilities
  • Compression speed
  • Compression size
  • Decompression time

But there are so many great compression tools available in Unix / Linux. It can be really confusing to choose which one to use even for a seasoned expert.

So I created X Compression Tool Benchmarker to help with this.

Features :

  • Test any kind of file : just put the file’s name as the parameter when calling the script. Then it will be tested against all the specified compression tools.
  • Add more compression tool easily : just edit the compressor_list & ext_file variable, and that’s it
  • Fire and forget : just run the script, and forget it. It will run without needing any intervention
  • CSV output : ready to be opened with Libre Office / Excel, and made into graphs in seconds.

Here’s a sample result for a Database archive file (type MySQL dump) :

The bar chart on top of this article is based from this result.

As you can see, currently this script will benchmark the following compression tools automatically : pigz – gzip – bzip2 – pbzip2 – lrzip – rzip – zstd – pixz – plzip – xz

The result, for each different file types, may surprise you 🙂

For example ; I was surprised to see rzip beat lrzip – because lrzip is supposed to be the enhancement of rzip.

Then I was even more surprised to find out that :

  • I was testing Debian Buster’s version of rzip, which turned out to be pretty old – it does not even have multi-thread/core capability
  • But when I tested the latest version of rzip, which can use all the 16 cores in my server – it turned out to be slower than the old rzip from Debian Buster !
  • No, disk speed is not an issue – I made sure that all the benchmark was run from NVME SSD

So I was grinning at how Debian Buster packaged a very old version of rzip instead of the new one – turned out the joke’s on me : the old rzip perform better than the new one. Even without the multi-core capability.

Also it was amazing to see how really REALLY fast zstd is, while still giving decent compression size. When you absolutely need compression speed, this not so well known compression tool turned out to be the clear winner.

And so on, etc

Yes, indeed I had fun 🙂

I hope you will too. Enjoy !

UPDATE : My friend , Eko Juniarto, published his results here and have permitted me to publish it here as well – thanks. Very interesting, indeed.

BCA – daftar bank korespondensi di Amerika

Suatu hari saya ditanyakan hal ini (bank korespondensi BCA di Amerika) setelah selesai seminar di Hawaii, untuk mentransfer honorarium saya.

Ternyata info ini tidak ketemu dimana-mana.

Tanya via Call center BCA di 1500888, mereka juga tidak tahu.

Akhirnya ketika istri saya kebetulan ada perlu ke BCA, dia tanyakan sekalian. Dijawab bahwa musti saya sendiri yang datang menanyakan.

Istri saya marah besar 😀 hahahaha

Apa logikanya cuma menanya “informasi bank korespondensi BCA” dengan saya musti datang sendiri ke BCA 😀 ha ha ha

Kalau karena musti nasabah BCA – istri saya juga nasabah BCA, dia juga punya rekening di BCA.

Akhirnya customer service BCA menyerah, dan memberitahu informasi tsb, hahaha. Ada-ada saja.

Saya lampirkan informasi tsb disini. Maka moga yang membutuhkannya tidak perlu mengalami kekonyolan serupa & terbuang-buang waktunya juga.

NAMA BANK : Bank of New York

NAMA BANK : Bank of America

NAMA BANK : Wells Fargo Bank

NAMA BANK : JP Morgan Chase Bank

NAMA BANK : Citibank

NAMA BANK : Standard Chartered Bank

Instalasi w3af

w3af (Web Application Attack and Audit Framework) adalah software yang bisa Anda gunakan untuk memeriksa keamanan aplikasi / website Anda.

Cara instalasi & penggunaannya sangat mudah, silakan ikuti panduan ini :

sudo apt-get update ; sudo apt-get -y install python-pip git

git clone
cd w3af/
# install semua paket yang diminta, lalu


Maka kini w3af & semua paket software yang dibutuhkannya telah terpasang.

Lalu buat file bernama MyScript.w3af, dengan isi sbb :

(CATATAN : jangan gunakan dulu plugin “redos” – terakhir saya gunakan, plugin redos ini berjalan selama 2 hari dan menghabiskan disk space di server saya. Hati-hati)

# -----------------------------------------------------------------------------------------------------------
# -----------------------------------------------------------------------------------------------------------
#Configure HTTP settings
set timeout 30
#Configure scanner global behaviors
set timeout 20
set max_requests_per_second 100
set max_discovery_time 20
set fuzz_cookies True
set fuzz_form_files True
set fuzz_url_parts True
set fuzz_url_filenames True
#Configure entry point (CRAWLING) scanner
crawl web_spider
crawl config web_spider
set only_forward False
set ignore_regex (?i)(logout|disconnect|signout|exit)+
#Configure vulnerability scanners
##Specify list of AUDIT plugins type to use
audit blind_sqli, buffer_overflow, cors_origin, csrf, eval, file_upload, ldapi, lfi, os_commanding, phishing_vector, response_splitting, sqli, xpath, xss, xst
##Customize behavior of each audit plugin when needed
audit config file_upload
set extensions jsp,php,php2,php3,php4,php5,asp,aspx,pl,cfm,rb,py,sh,ksh,csh,bat,ps,exe
##Specify list of GREP plugins type to use (grep plugin is a type of plugin that can find also vulnerabilities or informations disclosure)
grep analyze_cookies, click_jacking, code_disclosure, cross_domain_js, csp, directory_indexing, dom_xss, error_500, error_pages,
html_comments, objects, path_disclosure, private_ip, strange_headers, strange_http_codes, strange_parameters, strange_reason, url_session, xss_protection_header
##Specify list of INFRASTRUCTURE plugins type to use (infrastructure plugin is a type of plugin that can find informations disclosure)
infrastructure server_header, server_status, domain_dot, dot_net_errors
#Configure target authentication
#Configure reporting in order to generate an HTML report
output console, html_file
output config html_file
set output_file /tmp/W3afReport.html
set verbose False
output config console
set verbose False
#Set target informations, do a cleanup and run the scan
set target
set target_os unix
set target_framework php

Simpan file tersebut, lalu jalankan perintah sbb :

./w3af_console ­-s MyScript.w3af

Kini tinggal Anda tunggu sampai selesai, dan setelah itu laporannya bisa dilihat di /tmp/W3afReport.html

Enjoy !

Setup Varnish on Port 80

Sometimes you need to quickly setup Varnish, usually in an emergency (like, your website got featured on Reddit’s frontpage 😀 ), to quickly absorb most of the hits hitting your website.

But the webserver is already using port 80.
Now what ?

Pretty easy actually :

  1. Setup Varnish on other port, say, 6081
  2. Run an iptables command : to forward incoming traffic from port 80 to port 6081
  3. Make sure Varnish uses as the backend

Presto – now all the traffic hits Varnish first – which will process them in lightning speed.

Alright, so here’s the gory detail, also available on :

Enjoy !


apt-get update ; apt-get -y install varnish

# Varnish should be already configured to list on port 6081
# if in doubt, check /etc/default/varnish,
# and look for the following line :
# DAEMON_OPTS="-a :6081

# edit varnish config
vi /etc/varnish/default.vcl

# make sure the .port line is set to 80, like this :
# .port = "80";
# then save & exit

# enable Apache's expires & headers module
a2enmod expires
a2enmod headers

# setup caching for static files
# via .htaccess file
echo "Header unset ETag" >> /var/www/.htaccess
echo "FileETag None" >> /var/www/.htaccess
echo "<ifmodule mod_expires.c>" >> /var/www/.htaccess
echo "<filesmatch \"(?i)^.*\\.(ico|flv|jpg|jpeg|png|gif|js|css)$\">" >> /var/www/.htaccess
echo "ExpiresActive On" >> /var/www/.htaccess
echo "ExpiresDefault \"access plus 2 minute\"" >> /var/www/.htaccess
echo "</filesmatch>" >> /var/www/.htaccess
echo "</ifmodule>" >> /var/www/.htaccess

# enable caching in php.ini
vi /etc/php/7.0/apache2/php.ini

# make sure session.cache_limiter = public
# save & exit

# restart Apache
/etc/init.d/apache2 restart

###### now let's start forwarding traffic to Varnish ######

# enable port forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
vi /etc/sysctl.conf

# add this line at the end of the file :
# net.ipv4.ip_forward = 1

# now here's the command that will actually forward the traffic from port 80 to Varnish
# change eth0 to your computer's network interface name
iptables -A PREROUTING -t nat -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 6081

# make sure this iptables setting will become permanent
apt-get -y install iptables-persistent

WordPress Auto-Backup via SSH

This script will enable you to backup your WordPress websites automatically. Just put it in a crontab / automatic scheduling software somewhere.

Also available on Pastebin :




# 1/ You can do SSH password-less login to the server
# How :
# 2/ You have created a correct ~/.my.cnf file
# How :

# <server address> <home directory> <backup directory>

# 1/ SSH to server
# 2/ read wp-config file & get details
# 3/ create backup_dir/web
# 4/ rsync home_dir backup_dir/web 
# 5/ backup database to backup_dir/web/today.mysql
# 6/ compress backup_dir/web to backup_dir/backup_today.bz2 

### choose backup retention
# backup retention: weekly
today=`date +%A`
# backup retention: monthly
#today=`date +%d`


#============= START BACKUP =======================

# to help making this code more readable

# get the variables
ssh $server "cat $home_dir/wp-config.php" > /tmp/$server$today.txt

db_name=`cat /tmp/$server$today.txt | grep DB_NAME | cut -d"'" -f 4`

db_user=`cat /tmp/$server$today.txt | grep DB_USER | cut -d"'" -f 4`

db_pass=`cat /tmp/$server$today.txt | grep DB_PASSWORD | cut -d"'" -f 4`

db_host=`cat /tmp/$server$today.txt | grep DB_HOST | cut -d"'" -f 4`

table_prefix=`cat /tmp/$server$today.txt | grep table_prefix | cut -d"'" -f 2`

# debug
#echo $db_name $db_user $db_pass $db_host $table_prefix

# delete temporary file
rm /tmp/$server$today.txt

# backup database
ssh $server "mysqldump -h $db_host -u $db_user --password=\"$db_pass\" $db_name \$(mysql -h $db_host -u $db_user --password=\"$db_pass\" -D $db_name -Bse \"show tables like '$table_prefix%'\") > $home_dir/db-$today.mysql"

# compress database dump
ssh $server "$compressor1 $home_dir/db-$today.mysql"

# download everything 
mkdir $backup_dir/web/
rsync -avuz $server:$home_dir/* $backup_dir/web/

# make backup file
tar cvf $backup_dir/$server-$today.tar $backup_dir/web/

$compressor2 $backup_dir/$server-$today.tar &

# clean up
ssh $server "rm $home_dir/db-$today.mysql.gz"

# done !