Introduction

This book covers some tech notes / guides / howtos etc. that sometimes I refer to.

Also documenting here in public so I can easily share the reference with others (as opposed to e.g. a gist)

Running bitcoin-core on a VPS

We will install and run bitcoin-core on a VPS to support the bitcoin network.

Since a VPS would typically not have enough resources for the whole 300GB+ blockchain, we will use the pruned blockchain which is ~7GB.

Downloading & installing

Find the download link for the latest bitcoin-core from https://bitcoin.org/en/download

Then, download it to a tmp directory, extract it, and install it:

cd /tmp
wget https://bitcoin.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz
tar xvf bitcoin-25.0-x86_64-linux-gnu.tar.gz
sudo install -m 0755 -o root -g root -t /usr/local/bin bitcoin-25.0/bin/*

Pruning

Before running the bitcoin daemon, setup the bitcoin config to prune the chain:

mkdir -p ~/.bitcoin
echo "prune=550" >> ~/.bitcoin/bitcoin.conf

Running the node

For simplicity, in just a screen, you can run bitcoind

TODO: Service or something

Running a monero node (pruned) on a VPS

Note: The pruned node still needs ~50GB!

Downloading & installing

Monero distributes a tard set of binaries directly, which we will save into /opt.

In a root terminal:

mkdir -p /opt/monero
chown -R ubuntu:ubuntu /opt/monero

Then as regular user:

cd /tmp
wget https://downloads.getmonero.org/linux64
tar -xjvf linux64 --strip-components=1 -C /opt/monero

Run the node in prune mode:

(cd into /opt/monero first...)

./monerod --prune-blockchain --detach

You can check progress by tailing the log:

tail -f ~/.bitmonero/bitmonero.log

Connecting a monero wallet to a node over tor

This assumes you've tor already setup on your machine.

To connect the wallet CLI to the monero network over tor, you can do:

./monero-wallet-cli --proxy 127.0.0.1:9050 --daemon-address lrtrju7tz72422sjmwakygfu7xgskaawiqmfulmssfzx7aofatfkmvid.onion:18089

Here --proxy 127.0.0.1:9050 assumes tor is listening on the default port, and --daemon-address is the remote onion node.

If you're not running your own node, you can find a list at monero.fail

Exposting a local webservice over the internet via Cloudflare Tunnel

Go to Cloudflare Dashboard -> Zero Trust -> Networks -> Tunnels

Create a tunnel via GUI, it will give command to run to install cloudflared and setup the tunnel on the host machine. This step can sometimes take 60-120 seconds! You'll get something like:

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100 16.9M  100 16.9M    0     0  13.2M      0  0:00:01  0:00:01 --:--:-- 38.2M
Selecting previously unselected package cloudflared.
(Reading database ... 106916 files and directories currently installed.)
Preparing to unpack cloudflared.deb ...
Unpacking cloudflared (2024.2.0) ...
Setting up cloudflared (2024.2.0) ...
Processing triggers for man-db (2.10.2-1) ...
2024-02-19T12:13:16Z INF Using Systemd
2024-02-19T12:15:05Z INF Linux service for cloudflared installed successfully

Then, again in the Cloudflare Dashboard, just attach it to a subdomain and setup the local port, ezpz!

ntopng

ntopng is a pretty cool tool to view network traffic in real time.

It also performs DPI to extract some metadata about connections you can observe, such as SNI (for TLS), or the infohash (for BitTorrent).

It is especially interesting when run on a firewall / router of sorts.

Installation

We will be using Ubuntu 22.04 LTS in this guide.

1. Add the ntop repository into ubuntu

Inside a sudo shell:

apt-get install software-properties-common wget 
add-apt-repository universe
wget https://packages.ntop.org/apt-stable/22.04/all/apt-ntop-stable.deb 
apt install ./apt-ntop-stable.deb 

2. Install ntop

apt-get clean all 
apt-get update 
apt-get install ntopng

References

  • https://www.ntop.org/guides/ntopng/what_is_ntopng.html#installing-on-linux
  • https://packages.ntop.org/apt-stable/

nginx

Running HTTP/3 enabled nginx

For simiplicity, everything here will assume its being done in a root shell.

Building

Installing dependencies

apt-get update
apt-get install build-essential libpcre3 zlib1g libpcre3-dev 

Build LibreSSL

cd /tmp
wget https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.9.0.tar.gz
tar xvzf libressl-3.9.0.tar.gz
cd libressl-3.9.0
./configure
make check
make install

Building nginx

cd /tmp
wget http://nginx.org/download/nginx-1.25.4.tar.gz
tar xvzf nginx-1.25.4.tar.gz
cd nginx-1.25.4
./configure \
    --with-debug \
    --with-http_v3_module \
    --with-cc-opt="-I../libressl/build/include" \
    --with-ld-opt="-L../libressl/build/lib"
make

Making the config dirs

cd /usr/local
mkdir nginx
cd nginx
mkdir logs
mkdir conf

References

  • https://nginx.org/en/docs/quic.html
  • https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/
  • https://github.com/libressl/portable?tab=readme-ov-file#steps-that-apply-to-all-builds

Setting up TLS and Config

We will now generate our TLS keys and nginx config

Generate TLS key & cert

cd /usr/local/nginx
openssl req -newkey rsa:2048 -keyout domain.key -x509 -days 365 -out domain.crt

Setup the nginx config

Write this to /usr/local/nginx/conf/nginx.conf

events{}

http {
    log_format quic '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" "$http3"';

    access_log logs/access.log quic;

    server {
        # for better compatibility it's recommended
        # to use the same port for quic and https
        listen 443 quic reuseport;
        listen 443 ssl;

        ssl_certificate     /usr/local/nginx/domain.crt;
        ssl_certificate_key /usr/local/nginx/domain.key;

        location / {
            # required for browsers to direct them to quic port
            add_header Alt-Svc 'h3=":443"; ma=86400';
        }
    }
}

Make a dummy page

cd /var
mkdir -p www
cd www
mkdir -p html
cd html
echo "Hi" > index.html

Run nginx!

Now you can run nginx and serve your website over HTTP/3:

LD_LIBRARY_PATH=/usr/local/lib/ /tmp/nginx-1.25.4/objs/nginx

TODO

Make tutorial less hacky (LD library preload for LibreSSL, non standard nginx config dir etc.)

Running ECH enabled nginx

ECH is a good way to hide the SNI of the website being connected to.

However, since ECH is not yet an official RFC (March 2024) and not many libs support it (especially on the server side), we need to do some hacky stuff to deploy it.

Specifically, we will use openssl & nginx forks by the great people at defo.ie to deploy ECH support.

Building

Building the OpenSSL fork

This fork adds support for ECH into openssl.

cd ~
mkdir -p code
cd code
git clone https://github.com/sftcd/openssl.git openssl-for-nginx
cd openssl-for-nginx
git checkout ECH-draft-13c
./config -d
make

Building the nginx fork

This fork uses the forked OpenSSL along with Nginx to support incoming ECH TLS handshakes.

cd ~/code
git clone https://github.com/sftcd/nginx.git
cd nginx
git checkout ECH-experimental
./auto/configure --with-debug --prefix=nginx --with-http_ssl_module --with-openssl=$HOME/code/openssl-for-nginx  --with-openssl-opt="--debug"
make

Now nginx should be compiled with the ECH compatible OpenSSL!

Some nginx dirs

We need to create some directories which nginx expects for logs and stuff.

cd ~/code/openssl-for-nginx/esnistuff
mkdir nginx
cd nginx
mkdir logs
mkdir www
mkdir echkeydir

Deploying

To deploy a website with ECH support behind nginx, we need to generate ECH keys, update our DNS, and then configure nginx to use ECH.

The steps are outlined below:

Generate the ECH keys

Put whatever public_name here you want snoopers to think you're connecting to (via SNI)!

cd ~/code/openssl-for-nginx/esnistuff
../apps/openssl ech -public_name example.com -pemout ./nginx/echkeydir/example.pem.ech

Setup DNS records for ECHConfig

Once you generate the ECH keys, the contents will look something like this:

-----BEGIN PRIVATE KEY-----
[REDACTED]
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AD7+DQA6hAAgACAmYD8cK8laATn/iKI1jt79RGkFzoppTl0LVpshC/Q1VQAEAAEAAQALZXhhbXBsZS5jb20AAA==
-----END ECHCONFIG-----

The base64 text is what you need to add to your domains "HTTPS" DNS record.

For example, if your domain name (whatever value you used for public_name in the keygen step doesn't matter) is rfc5746.mywaifu.best , then you need to add a HTTPS record in your DNS, as such:

Cloudflare DNS example for ECH HTTPS

Note: the value should be: ech="THE BASE 64 ECHCONFIG"

Configure nginx

A sample configuration file could look like this: (nginx-ech.conf):

worker_processes  1;
error_log  logs/error.log  info;

events {
    worker_connections  1024;
}

http {
    access_log          logs/access.log combined;
    ssl_echkeydir        echkeydir;
    server {
        listen              443 default_server ssl;
        ssl_certificate     cadir/domain.crt;
        ssl_certificate_key cadir/domain.key;
        ssl_protocols       TLSv1.3;
        server_name         rfc5746.mywaifu.best;
        location / {
            root   www;
            index  index.html index.htm;
        }
    }
}

Replace server_name with whatever your real server (domain) name is, e.g. for me it is rfc5746.mywaifu.best). Now put this config in ~/code/openssl-for-nginx/esnistuff/nginx/nginx-ech.conf .

Run nginx

cd ~/code/openssl-for-nginx/esnistuff
../../nginx/objs/nginx -c nginx-ech.conf

Bonus: Multiple ECHConfigs with different SNIs

If you configure nginx to listen on multiple ports, you can advertise separate ECHConfigs for each port, with their own SNI. Specifically, the ECHConfig advertised in the HTTPS RR follows "Port Prefix Naming" as per Section 2.3 of RFC9460.

Generating a new ECHConfig

Let's say, now instead of example.com, we want to use the SNI cia.gov. Let's first generate an ECHConfig for this domain:

cd ~/code/openssl-for-nginx/esnistuff
../apps/openssl ech -public_name cia.gov -pemout ./nginx/echkeydir/cia.gov.pem.ech

Tell NGINX to listen on this port

If we want port 4443 to advertise this ECHConfig and be connectable, we just add a listen directive for this port in our NGINX config:

    listen 4443 default_server ssl;  

Finally, we need a new DNS HTTPS record for specifically for this port, based on "Port Prefix Naming". So add a new HTTPS record for your domain, with the target as:

_4443._https.rfc5746.mywaifu.best  

Note: The new part is _4443._https.! This will tell browsers (or well, more generally ECH capable clients) the ECHConfig to use on this port.

Reference

  • https://github.com/sftcd/openssl/blob/9e66beb759d274f3069e19cc96c793712e83122c/esnistuff/nginx.md?plain=1#L172
  • https://github.com/sftcd/openssl/issues/26
  • https://guardianproject.info/2023/11/10/quick-set-up-guide-for-encrypted-client-hello-ech/

CA Signed TLS certificates

Sometimes it can be useful to get TLS certificates signed by a CA. To do this for free, you need a domain name.

Usually ACME clients like certbot will auto-detect a webserver and use that to "serve" the challenge.

However this required pointing the domain to an IP address, which can be persisted in DNS history records. Another way to do it, from ANY machine, is via the DNS challenge.

ACME DNS Challenge

We will use certbot for this. Assuming the domain you want a certificate for is example.com, then the following command will suffice:

certbot -d example.com --manual --preferred-challenges dns certonly

This will ask you to create a DNS TXT record with a certain value, to prove you control the domain name. Once that is done, you will have the signed certificate and private key on your machine, to do whatever you want with.

Wireguard

Client

Creating a client config for routing all traffic "via VPN"

In a temporary directory:

wg genkey | tee private.key
cat private.key | wg pubkey | tee public.key

Then, make a config file e.g. wireguard_config.conf :

[Interface]
PrivateKey = [private key just generated]
Address = 10.10.0.3/24 (an IP on the subnet of the server's wireguard interface)

[Peer]
PublicKey = [server public key]
AllowedIPs = 0.0.0.0/0 (all traffic to be routed through this tunnel)
Endpoint = [server public IP address:port]

You then need to add the client as authorized to connect to the server. On the server, run:

wg set wg1 peer [public key just generated] allowed-ips 10.10.0.3

Replace 10.10.0.3 with whatever IP was chosen in the config file for the client above.

To add the config to something like a smartphone, you can qrencode it via:

qrencode -t ansiutf8 < wireguard_config.conf

Installing tor on ubuntu

Basic steps

All these should be done in a root terminal.

1. Install https transport for apt

apt install apt-transport-https

2. Add the tor distribution file in apt sources (i.e. /etc/apt/sources.list.d/tor.list)

First get your distribution name:

$ lsb_release -c
Codename:	jammy

Use this in the file:

deb     [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org jammy main
deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org jammy main

3. Get the tor GPG key

wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null

4. Install tor!

apt update
apt install tor deb.torproject.org-keyring

5. Verification

curl --proxy socks5h://localhost:9050 icanhazip.com

You can then put the IP into AbuseDB or check on the Tor Relay Metrics page.

You can also view the tor logs via:

journalctl -f -u tor

References

Mostly everything is from the official tor docs.

Installing some programming languages

Some tips on how to setup a VPS or similar with programming languages

Prerequisites

Usually a good idea to have build-essential & pkg-config:

sudo apt-get install build-essential pkg-config

Installing Node.JS

Official website: https://nodejs.org/en/download/package-manager/current

Quick install via n

Github Repo: https://github.com/tj/n

curl -L https://bit.ly/n-install | bash

Installing Rust

Official website: https://www.rust-lang.org/tools/install

Quick install via Rustup

Website: https://rustup.rs/

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Crypto Stuff

Cryptographic references for PGP, GPG, OpenSSL etc.

DISCLAIMER: This primarily aimed as a reference for myself. It may not be the most secure or best way to do this.

PGP

PGP is great for signing and encryption. Signing is an especially valuable concept, since it has stronger guarantees on the author as opposed to any kind of e.g. social media profile, which by nature have backdoors accessible by staff.

Keygen

We will use keys based on Curve25519. For signing, this means Ed25519, and for encryption X25519.

We will first generate a master ceritification key. The password for this guy should be very strong. For the subsequent subkeys we will change the password later.

gpg --quick-generate-key 'Your Name <email@domain.com>' ed25519 cert never

You should get something like:

pub   ed25519 2024-03-25 [C]
      224245002A2A0003CD7CF693FFC081E4E8D5FA41
uid                      Your Name <email@domain.com>

The long hex string is the key fingerprint. We will save it in a temporary variable for generating the subkeys:

export KEYFP=224245002A2A0003CD7CF693FFC081E4E8D5FA41

Now we will generate the subkeys for signing and encryption. Here we will use a 3 year expiration, you can change it to something else if you'd like:

gpg --quick-add-key $KEYFP ed25519 sign 3y
gpg --quick-add-key $KEYFP cv25519 encr 3y

Our keyring should now look like:

$ gpg -K --with-keygrip

sec   ed25519 2024-03-25 [C]
      224245002A2A0003CD7CF693FFC081E4E8D5FA41
      Keygrip = FCC73C8DA31BEB61ECF3B2316131CBF9AFE02675
uid           [ultimate] Your Name <email@domain.com>
ssb   ed25519 2024-03-25 [S] [expires: 2027-03-25]
      Keygrip = 5C407E3653C2BC7C1E4F06BB1179A9A317A88F52
ssb   cv25519 2024-03-25 [E] [expires: 2027-03-25]
      Keygrip = EBA000BD362AE3CFC512730544B133E7BBA7E217

Now that we generated the subkeys for signing and encryption, we can delete the master key. But we need to first save it, for when we need to generate new keys (e.g. after the current ones expire).

Back up the master key via

$ gpg --armor --export-secret-keys $KEYFP > gpg_master_key.asc

Now, we will delete the master key from our keyring. Go to your private key directory (usually ~/.gnupg), and delete the file matching the keygrip of the sec key. In our case it is FCC73C8DA31BEB61ECF3B2316131CBF9AFE02675.key.

Now when you view your keyring, GPG will print sec# indicating the secret key is not available:

sec#  ed25519 2024-03-25 [C]
      224245002A2A0003CD7CF693FFC081E4E8D5FA41
      Keygrip = FCC73C8DA31BEB61ECF3B2316131CBF9AFE02675
uid           [ultimate] Your Name <email@domain.com>
ssb   ed25519 2024-03-25 [S] [expires: 2027-03-25]
      Keygrip = 5C407E3653C2BC7C1E4F06BB1179A9A317A88F52
ssb   cv25519 2024-03-25 [E] [expires: 2027-03-25]
      Keygrip = EBA000BD362AE3CFC512730544B133E7BBA7E217

Make sure to save gpg_master_key.asc somewhere encrypted for when you need it!

Updating the password

Now with the master key deleted from your keyring, we can edit the password for the subkeys.

Run gpg -K --keyid-format 0xLONG , and copy the KEYID of your master key. It would look something like:

sec#  ed25519/0xFFC081E4E8D5FA41 2024-03-25 [C]

Tell GPG you want to edit the key

gpg --edit-key 0xFFC081E4E8D5FA41

At the gpg> prompt, type passwd . This will prompt you for the current password, which would be the strong password set when generating the master key.

After entering that, it will ask you for a new password. Put in whatever you want.

Finally, at the gpg> prompt, type quit to finish.

References

  • https://github.com/drduh/YubiKey-Guide
  • https://musigma.blog/2021/05/09/gpg-ssh-ed25519.html

OpenSSL

Some tips and tricks for using OpenSSL.

For more indepth into some patterns of how e.g. ECDH, RSA work, my repo cli-crypto may be more useful.

Generating TLS certificates

RSA

First, we generate and RSA key:

openssl genrsa -out example.com.key 4096

Generate the certificate signing request:

openssl req -key example.com.key -new -out example.com.csr

Self-signing the certificate:

openssl x509 -signkey example.com.key -in example.com.csr -req -days 3650 -out example.com.crt

Elliptic Curve

Basically the same, but we will generate an Ed25519 key instead

openssl genpkey -algorithm ed25519 -out example.com.key

Wireshark

Wireshark is a great tool for inspecting network traffic, with support for a lot of filters and such.

Remote Wireshark

In many cases, it can be useful to view the traffic of a remote machine locally via Wireshark.

Although tcpdump via SSH in the terminal is ok for some initial poking around, the convenicne of Wireshark w/ a GUI, deeper protocol analysis etc. is quite nifty.

There are two ways to do so, depending on tools available. In both, the rudimentary filter not tcp port 22 is used to not show SSH traffic, which otherwise would lead to a positive feedback loop.

using dumpcap

If the remote machine has dumpcap installed, then it can be launched using via the SSH command with output redirected to stdout, which is then piped to Wireshark locally:

wireshark -k -i <(ssh HOSTNAME "dumpcap -P -w - -f 'not tcp port 22'")

using tcpdump

If the remote machine has tcpdump installed, then it can be launched via the following command:

wireshark -k -i <(ssh HOSTNAME "sudo tcpdump -w - -f 'not tcp port 22'")

AV1 encoding for BluRays

Assuming you've ffmpeg built with SVT-AV1 already. (TODO: Write a guide for this).

These are my settings. They may not work for you, but generically they seem to get the job done for me.

Figuring out the crop

This is especially important for movies, since often they have black bars on the top and bottom. Cropping them out will make encoding faster and waste less space.

Note: Some movies have changing aspect ratios! For instance: The Dark Knight (2008). So be careful.

Using ffmpeg, you can get the cropdetect as such:

ffmpeg -i Filename.mkv -vf cropdetect,metadata=mode=print -f null -

Which will print a lot of messages, looking like:

[Parsed_cropdetect_0 @ 0x69ed680] x1:0 x2:1919 y1:138 y2:941 w:1920 h:800 x:0 y:140 pts:76743 t:76.743000 limit:0.094118 crop=1920:800:0:140
[Parsed_metadata_1 @ 0x69f3f80] frame:1840 pts:76743   pts_time:76.743
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.x1=0
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.x2=1919
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.y1=138
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.y2=941
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.w=1920
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.h=800
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.x=0
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.y=140
[Parsed_metadata_1 @ 0x69f3f80] lavfi.cropdetect.limit=0.094118

Here, crop=1920:800:0:140 is what you want.

Encoding the video

Since I've built ffav1 as ffmpeg with (and only with) SVT-AV1, I will encode the video independently from audio / subs etc.

Add in the correct cropdetect, or leave it out if there's no cropping.

ffav1 -hide_banner -y -i Filename.mkv -vf crop=1920:800:0:140 -map 0:v:0 -c:v:0 libsvtav1 -preset 6 -svtav1-params tune=0:enable-overlays=1:scm=0 -pix_fmt yuv420p10le -g 120 -b:v:0 5000k -pass 1 -an -f null /dev/null && \
ffav1 -hide_banner -y -i Filename.mkv -vf crop=1920:800:0:140 -map 0:v:0 -c:v:0 libsvtav1 -preset 6 -svtav1-params tune=0:enable-overlays=1:scm=0 -pix_fmt yuv420p10le -g 120 -b:v:0 5000k -pass 2 -an Filename_AV1.mkv

Encoding the audio

First, determine the audio formats present on the source media, using something like mediainfo filename.mkv , which let's say, gives:

Audio #1
ID                                       : 2
Format                                   : MLP FBA 16-ch
Format/Info                              : Meridian Lossless Packing FBA with 16-channel presentation
Commercial name                          : Dolby TrueHD with Dolby Atmos
Codec ID                                 : A_TRUEHD
Duration                                 : 2 h 35 min
Bit rate mode                            : Variable
Bit rate                                 : 3 243 kb/s
Maximum bit rate                         : 5 472 kb/s
Channel(s)                               : 8 channels
Channel layout                           : L R C LFE Ls Rs Lb Rb
Sampling rate                            : 48.0 kHz
Frame rate                               : 1 200.000 FPS (40 SPF)
Compression mode                         : Lossless
Stream size                              : 3.52 GiB (12%)
Title                                    : Dolby TrueHD/Atmos Audio / 7.1 / 48 kHz / 3243 kbps / 24-bit
Language                                 : English
Default                                  : Yes
Forced                                   : No
Number of dynamic objects                : 11
Bed channel count                        : 1 channel
Bed channel configuration                : LFE

Audio #2
ID                                       : 3
Format                                   : AC-3
Format/Info                              : Audio Coding 3
Commercial name                          : Dolby Digital
Format settings                          : Dolby Surround EX
Codec ID                                 : A_AC3
Duration                                 : 2 h 35 min
Bit rate mode                            : Constant
Bit rate                                 : 448 kb/s
Channel(s)                               : 6 channels
Channel layout                           : L R C LFE Ls Rs
Sampling rate                            : 48.0 kHz
Frame rate                               : 31.250 FPS (1536 SPF)
Compression mode                         : Lossy
Stream size                              : 498 MiB (2%)
Title                                    : Compatibility Track / Dolby Digital EX Audio / 5.1-EX / 48 kHz / 448 kbps
Language                                 : English
Service kind                             : Complete Main
Default                                  : No
Forced                                   : No

Extracting an existing audio track

In this example, there is already a decent 5.1ch Doby Digital track (AC3) with a decent bitrate (448kb/s). So we can identify it with mkvmerge and then extract it:

$ mkvmerge -i Filename.mkv
File 'Filename.mkv': container: Matroska
Track ID 0: video (AVC/H.264/MPEG-4p10)
Track ID 1: audio (TrueHD Atmos)
Track ID 2: audio (AC-3 Dolby Surround EX)
Track ID 3: subtitles (HDMV PGS)
Track ID 4: subtitles (HDMV PGS)
Track ID 5: subtitles (HDMV PGS)
Track ID 6: subtitles (HDMV PGS)
Track ID 7: subtitles (HDMV PGS)
Track ID 8: subtitles (HDMV PGS)
Track ID 9: subtitles (HDMV PGS)
Track ID 10: subtitles (HDMV PGS)
Track ID 11: subtitles (HDMV PGS)
Track ID 12: subtitles (HDMV PGS)
Track ID 13: subtitles (HDMV PGS)
Chapters: 18 entries
Global tags: 2 entries

And extract the track we want with

$ mkvextract Filename.mkv tracks 2:Filename_audio.ac3
Extracting track 2 with the CodecID 'A_AC3' to the file 'Filename_audio.ac3'. Container format: Dolby Digital (AC-3)
Progress: 100%

Encoding an existing audio track

If the only audio was some form of raw audio (e.g. Dolby TrueHD / DTS-HD), we can encode it to a compressed format (such as DD5.1) using:

ffmpeg -hide_banner Filename.mkv -map 0:a:0 -c:a:0 ac3 -b:a:0 640k Filename_audio.ac3

Extracting the chapters

If the original source had chapters, you can extract them to merge them into the final encode, via:

$ mkvextract Filename.mkv chapters Filename_Chapters.xml

Merging the files together

Once you have the video, audio, subtitles, you can merge them together using something like:

mkvmerge --title "Movie Name (Year)" \
--track-name "0:" Filename_AV1.mkv \
--track-name "0:English (DD5.1)" --language 0:eng Filename_audio.ac3 \
--track-name "0:English (SDH)" --language 0:eng Subtitles.srt \
-o Movie.Year.1080p.BluRay.DD5.1.AV1-GROUP.mkv

Encoding ogg files for iPod

If you use DownOnSpot to download songs, they are encoded with Vorbis and in an OGG container.

iTunes cannot import these (and the iPod can't play them back), so we need to convert them first.

Conversion

tl;dr

ffmpeg -i song.ogg -map_metadata 0:s:a:0 -acodec aac -b:a 320k -aac_pns 0 -movflags +faststart -vn song.m4a

Explanation

We need to encode them with AAC, and store them in an M4A container, for iTunes / iPod to be able to play them back.

The most vanilla way to do this is:

ffmpeg -i song.ogg -c:a aac song.m4a

However, this has a couple of problems:

  • Won't get the metadata (e.g. song/album/artist title etc.)
  • Regular weird high pitch noises when playing back on an iPod

Metadata

The metadata in the downloaded OGG tracks is stored at the stream level:

$ ffprobe -hide_banner Angels.ogg
Input #0, ogg, from 'Angels.ogg':
  Duration: 00:04:25.00, start: 0.000000, bitrate: 328 kb/s
  Stream #0:0: Audio: vorbis, 44100 Hz, stereo, fltp, 320 kb/s
    Metadata:
      title           : Angels
      album           : Life Thru A Lens
      artist          : Robbie Williams
      album_artist    : Robbie Williams
      track           : 4
      disc            : 1
      label           : Universal-Island Records Ltd.
      date            : 1997-01-01

However, the AAC encoder in ffmpeg can only store the metadata at the top level (global metadata). -map_metadata 0:s:a:0 will do this for us, so it is available to iTunes.

AAC Perceptual Noise Substitution

Most iPods do not support the PNS feature in their AAC decoder, which ffmpeg by default uses. This results in tracks that sound fine on all devices except after syncing to the iPod!

To fix this, we need -aac_pns 0 .

References

  • https://wiki.archlinux.org/title/IPod
  • https://www.reddit.com/r/ipod/comments/r2cti3/squeaking_noises_only_through_the_ipod_and_only/hry0ys5/

Miscellaneous ffmpeg goodies

Encoding Instant Replay to share online

Nvidia has a feature called instant replay that saves x minutes of gameplay. This is saved at a fairly high bitrate. If you want to share a part of it with a friend online, then the file is way too big. Additionally, when sharing online, it is possible to get the context at a much lower resolution (e.g. 720p vs 1440p). Also when gaming on an ultrawide, if only the 16:9 part is relevant, then a crop also helps.

General formula

Ignoring scaling down to 720p and cropping, there are two main codecs that should be used:

  • H.264 + AAC, good combo for sharing on Whatsapp etc.
  • VP9 + Opus, good combo for sharing on Discord etc.

VP9 is more efficient and will give better results compared to H.264, but slightly less compatibility.

H.264

The general recipe I like to use is:

ffmpeg -i source.mp4 -c:v libx264 -b:v 2000k -c:a aac -b:a 96k clip.mp4

VP9

The general recipe I like to use is:

ffmpeg -i source.mp4 -c:v libvpx-vp9 -b:v 2000k -c:a libopus -b:a 96k clip.webm

Additional filters

To crop from my 3440x1440 ultrawide down to 16:9 (useful when the overlay is ultrawide but actual content is 16:9), I use: -vf crop=2560:1440:440:0

To resize down to 720p, I use: vf scale=1280:-1

Usually I will combine the two, and encode at 1500kbps. If you need to seek to a particular start point, use the -ss flag. And if you only want the next n seconds, use the -t flag. This example starts from the 4 minute mark and takes the next 10 seconds:

ffmpeg -ss 00:04:00 -i "source.mp4"  -c:v libvpx-vp9 -b:v 1500k -vf "crop=2560:1440:440:0,scale=1280:-1" -preset veryslow -c:a libopus -b:a 96k -t 10 output.webm