<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ricco020</title>
    <description>The latest articles on DEV Community by ricco020 (@ricco020).</description>
    <link>https://dev.to/ricco020</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960216%2Fbc01eb94-de65-41c2-9767-966a157976ae.jpeg</url>
      <title>DEV Community: ricco020</title>
      <link>https://dev.to/ricco020</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ricco020"/>
    <language>en</language>
    <item>
      <title>How browser fingerprinting actually identifies you (and how to check yours)</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Tue, 16 Jun 2026 14:49:18 +0000</pubDate>
      <link>https://dev.to/ricco020/how-browser-fingerprinting-actually-identifies-you-and-how-to-check-yours-2f7o</link>
      <guid>https://dev.to/ricco020/how-browser-fingerprinting-actually-identifies-you-and-how-to-check-yours-2f7o</guid>
      <description>&lt;p&gt;Cookies are easy to block. Fingerprinting is the technique that quietly replaces them — and most people have never tested how exposed they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a fingerprint actually is
&lt;/h2&gt;

&lt;p&gt;A browser fingerprint is a combination of attributes your browser exposes to every site: user-agent, screen resolution, timezone, installed fonts, language, and — crucially — the way your specific GPU and graphics stack render a hidden &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; or WebGL scene. Individually these are low-entropy. Combined, they're often unique enough to single you out of millions, with no cookie and no login.&lt;/p&gt;

&lt;p&gt;The key idea is &lt;strong&gt;entropy&lt;/strong&gt;: each attribute carries some bits of identifying information. Timezone alone is weak. Timezone + a canvas hash + your exact font list + audio-stack quirks can be more than enough to re-identify you across sessions and even across sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main vectors in 2026
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canvas / WebGL rendering&lt;/strong&gt; — the same drawing instructions produce subtly different pixels depending on your GPU, drivers and OS. A hash of that output is one of the most stable signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AudioContext&lt;/strong&gt; — generating a waveform and reading it back leaks tiny differences in your audio stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font enumeration&lt;/strong&gt; — the set of fonts you have installed is surprisingly personal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardware hints&lt;/strong&gt; — &lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt;, &lt;code&gt;deviceMemory&lt;/code&gt;, pointer/touch capabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to reduce your fingerprint
&lt;/h2&gt;

&lt;p&gt;There's no perfect answer, but there are real trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tor Browser&lt;/strong&gt; makes everyone look identical by design — the strongest option, at the cost of speed and convenience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox&lt;/strong&gt; with &lt;code&gt;privacy.resistFingerprinting&lt;/code&gt; (or the simpler "strict" tracking protection) normalizes many of these signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brave&lt;/strong&gt; randomizes canvas/audio readings per session so the hash isn't stable.&lt;/li&gt;
&lt;li&gt;Counter-intuitively, &lt;strong&gt;rare extensions or an exotic setup can make you &lt;em&gt;more&lt;/em&gt; identifiable&lt;/strong&gt;, not less — uniqueness is the enemy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test yours
&lt;/h2&gt;

&lt;p&gt;Before changing anything, measure it. &lt;a href="https://coveryourtracks.eff.org/" rel="noopener noreferrer"&gt;Cover Your Tracks (EFF)&lt;/a&gt; and AmIUnique both show your uniqueness score and which attributes leak the most bits.&lt;/p&gt;

&lt;p&gt;For a full 2026 technical breakdown — every vector, how detection works under the hood, and what each browser actually mitigates — I keep a deeper reference here: &lt;strong&gt;&lt;a href="https://alexi.sh/posts/browser-fingerprinting-state-art-2026" rel="noopener noreferrer"&gt;Browser fingerprinting: state of the art 2026&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The honest takeaway: you can't get to zero in a normal browser, but you can move from "trivially unique" to "blends into a large crowd" — and that's most of the battle.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>security</category>
      <category>webdev</category>
      <category>browsers</category>
    </item>
    <item>
      <title>rclone crypt: encrypt files client-side before they touch any cloud</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 14 Jun 2026 03:13:55 +0000</pubDate>
      <link>https://dev.to/ricco020/rclone-crypt-encrypt-files-client-side-before-they-touch-any-cloud-123</link>
      <guid>https://dev.to/ricco020/rclone-crypt-encrypt-files-client-side-before-they-touch-any-cloud-123</guid>
      <description>&lt;p&gt;If you want files encrypted &lt;strong&gt;before&lt;/strong&gt; they ever reach a cloud provider — so the provider only ever sees ciphertext — &lt;code&gt;rclone crypt&lt;/code&gt; is the simplest tool that works with almost any backend (S3, Google Drive, Dropbox, pCloud, Backblaze B2, a plain SFTP box…). This is client-side, zero-knowledge-style encryption you fully control. Here's a clean setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rclone crypt&lt;/code&gt; is a &lt;strong&gt;wrapper remote&lt;/strong&gt;: it sits on top of a normal remote and transparently encrypts file contents &lt;em&gt;and&lt;/em&gt; file/dir names on the way up, decrypts on the way down. Your passphrase never leaves your machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local files  -&amp;gt;  [crypt remote: encrypt]  -&amp;gt;  [storage remote]  -&amp;gt;  cloud (sees ciphertext only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://rclone.org/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;span class="c"&gt;# or: sudo apt install rclone&lt;/span&gt;
rclone version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Configure the underlying storage remote
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone config
&lt;span class="c"&gt;# n) New remote -&amp;gt; name it e.g. "drive" -&amp;gt; pick your provider -&amp;gt; OAuth/keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone lsd drive:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Add a crypt remote on top
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone config
&lt;span class="c"&gt;# n) New remote -&amp;gt; name "secret" -&amp;gt; storage: "crypt"&lt;/span&gt;
&lt;span class="c"&gt;#   remote&amp;gt;  drive:encrypted        # a subfolder on the storage remote&lt;/span&gt;
&lt;span class="c"&gt;#   filename_encryption&amp;gt;  standard  # also encrypts file names&lt;/span&gt;
&lt;span class="c"&gt;#   directory_name_encryption&amp;gt;  true&lt;/span&gt;
&lt;span class="c"&gt;#   password&amp;gt;  (generate a strong one)&lt;/span&gt;
&lt;span class="c"&gt;#   password2&amp;gt;  (salt - optional but recommended)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Back up the passphrase + salt in a password manager.&lt;/strong&gt; There is no recovery if you lose them — that's the whole point of zero-knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Upload (everything is encrypted client-side first):&lt;/span&gt;
rclone copy ~/Documents secret: &lt;span class="nt"&gt;-P&lt;/span&gt;

&lt;span class="c"&gt;# List (decrypted view, local only):&lt;/span&gt;
rclone &lt;span class="nb"&gt;ls &lt;/span&gt;secret:

&lt;span class="c"&gt;# Mount as a normal folder:&lt;/span&gt;
rclone mount secret: ~/CloudCrypt &lt;span class="nt"&gt;--vfs-cache-mode&lt;/span&gt; writes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the provider's side you'll see only opaque names like &lt;code&gt;a1b2c3d4...&lt;/code&gt; — no filenames, no content.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Verify the provider sees nothing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone &lt;span class="nb"&gt;ls &lt;/span&gt;drive:encrypted    &lt;span class="c"&gt;# raw view = encrypted blobs + scrambled names&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can read filenames here, filename encryption isn't on — recheck step 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;crypt&lt;/code&gt; encrypts content + names, not the &lt;em&gt;number&lt;/em&gt; of files or their sizes.&lt;/strong&gt; A motivated observer can still infer file count and approximate sizes. For metadata-sensitive cases, pad or archive first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not add redundancy.&lt;/strong&gt; crypt is encryption, not backup — keep the 3-2-1 rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two different crypt remotes with different passwords are incompatible.&lt;/strong&gt; Decide your scheme once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a provider-native E2E option is better
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rclone crypt&lt;/code&gt; is great for bolting encryption onto &lt;em&gt;any&lt;/em&gt; backend. But if you want native end-to-end encryption, mobile apps, and sharing built in, a zero-knowledge provider may fit better. The trade-offs between "encrypt-it-yourself" and provider-native E2E/zero-knowledge are worth understanding:&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://priviy.com/en/blog/e2e-vs-zero-knowledge-cloud-storage-2026" rel="noopener noreferrer"&gt;End-to-end vs zero-knowledge cloud storage — what's the real difference&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>rclone</category>
      <category>encryption</category>
      <category>privacy</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Self-hosted WireGuard VPN on a VPS: a clean copy-paste quickstart</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Sun, 14 Jun 2026 03:04:00 +0000</pubDate>
      <link>https://dev.to/ricco020/self-hosted-wireguard-vpn-on-a-vps-a-clean-copy-paste-quickstart-23hb</link>
      <guid>https://dev.to/ricco020/self-hosted-wireguard-vpn-on-a-vps-a-clean-copy-paste-quickstart-23hb</guid>
      <description>&lt;p&gt;A self-hosted WireGuard VPN on a $5/month VPS gives you a private exit IP you fully control — no logs but your own, no shared IP pools, no monthly per-device fees. Here's a clean, copy-pasteable quickstart on Ubuntu. (For DNS hardening, a kill switch and multi-client management, see the full tutorial linked at the end.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with a public IPv4 (Contabo, Hetzner, OVH… any works), Ubuntu 22.04+ with root.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UDP 51820&lt;/code&gt; reachable (open it in the provider firewall).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install WireGuard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wireguard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux 5.6+ WireGuard is a kernel module — minimal overhead, no userspace daemon.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Generate server keys
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/wireguard
&lt;span class="nb"&gt;umask &lt;/span&gt;077
wg genkey | &lt;span class="nb"&gt;tee &lt;/span&gt;server.key | wg pubkey &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;umask 077&lt;/code&gt; matters: the private key must not be world-readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Server config — &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.8.0.1/24&lt;/span&gt;
&lt;span class="py"&gt;ListenPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;51820&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;contents of server.key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;PostUp&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;
&lt;span class="py"&gt;PostDown&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;eth0&lt;/code&gt; with your real public interface (&lt;code&gt;ip route get 1.1.1.1&lt;/code&gt; shows it).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Enable IP forwarding
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'net.ipv4.ip_forward=1'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/sysctl.d/99-wg.conf
&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Bring it up (and on boot)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wg-quick up wg0
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;wg-quick@wg0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo wg show&lt;/code&gt; should now list the interface with its public key and listening port.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Add a client
&lt;/h2&gt;

&lt;p&gt;Generate a keypair on the client (or server-side), then add a peer to &lt;code&gt;wg0.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client public key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.8.0.2/32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload without dropping the tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;wg syncconf wg0 &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;wg-quick strip wg0&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client config (&lt;code&gt;wg0.conf&lt;/code&gt; on the device):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Interface]&lt;/span&gt;
&lt;span class="py"&gt;PrivateKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;client private key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10.8.0.2/24&lt;/span&gt;
&lt;span class="py"&gt;DNS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1.1.1.1&lt;/span&gt;

&lt;span class="nn"&gt;[Peer]&lt;/span&gt;
&lt;span class="py"&gt;PublicKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server public key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;Endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;server public IP&amp;gt;:51820&lt;/span&gt;
&lt;span class="py"&gt;AllowedIPs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0/0&lt;/span&gt;
&lt;span class="py"&gt;PersistentKeepalive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AllowedIPs = 0.0.0.0/0&lt;/code&gt; routes all traffic through the tunnel. &lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; keeps NAT mappings alive on mobile networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# on the client, after wg-quick up:&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://ifconfig.me      &lt;span class="c"&gt;# should return the VPS IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also run a DNS-leak check in the browser — if your real resolver shows up, set &lt;code&gt;DNS =&lt;/code&gt; in the client &lt;code&gt;[Interface]&lt;/code&gt; (as above) and confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No traffic flows&lt;/strong&gt; → wrong &lt;code&gt;eth0&lt;/code&gt; name in &lt;code&gt;PostUp&lt;/code&gt;, or &lt;code&gt;ip_forward&lt;/code&gt; not applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works on Wi-Fi, drops on 4G&lt;/strong&gt; → add &lt;code&gt;PersistentKeepalive = 25&lt;/code&gt; (above).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handshake but no internet&lt;/strong&gt; → MASQUERADE rule missing or wrong outbound interface.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;This is the minimal working setup. For provider selection (price/jurisdiction), DNS hardening, a Linux kill switch with &lt;code&gt;iptables&lt;/code&gt;, and managing several clients, here's a complete step-by-step guide:&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://www.vpnsmith.com/en/blog/self-host-vpn-contabo-wireguard-2026" rel="noopener noreferrer"&gt;Self-host a VPN on Contabo with WireGuard — full tutorial&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>vpn</category>
      <category>linux</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>How browser fingerprinting works: canvas, WebGL and AudioContext explained</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 12 Jun 2026 07:00:52 +0000</pubDate>
      <link>https://dev.to/ricco020/how-browser-fingerprinting-works-canvas-webgl-and-audiocontext-explained-2146</link>
      <guid>https://dev.to/ricco020/how-browser-fingerprinting-works-canvas-webgl-and-audiocontext-explained-2146</guid>
      <description>&lt;p&gt;Browser fingerprinting is one of the most persistent tracking mechanisms on the web - and unlike cookies, you can't just "clear" it. Here's how it actually works under the hood, with real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is browser fingerprinting?
&lt;/h2&gt;

&lt;p&gt;When you visit a website, your browser leaks dozens of attributes: installed fonts, screen resolution, GPU model, audio stack behavior, canvas rendering quirks. Individually these seem harmless. Combined, they form a hash that's statistically unique to your device - a &lt;strong&gt;fingerprint&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The creepy part: fingerprinting survives incognito mode, VPNs, and cookie deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Canvas fingerprinting
&lt;/h2&gt;

&lt;p&gt;Canvas fingerprinting exploits the fact that different GPUs, drivers, and operating systems render the same drawing instructions slightly differently - different anti-aliasing, sub-pixel rendering, and color profiles produce subtly distinct pixel values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCanvasFingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textBaseline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;14px Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;125&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#069&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Browser fingerprinting test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rgba(102, 204, 0, 0.7)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Browser fingerprinting test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashBuffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hashArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getCanvasFingerprint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;hashString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Canvas hash:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two machines running the same browser version but different GPUs will produce different &lt;code&gt;dataUrl&lt;/code&gt; strings - and therefore different hashes.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. WebGL fingerprinting
&lt;/h2&gt;

&lt;p&gt;WebGL exposes your GPU model directly. &lt;code&gt;WEBGL_debug_renderer_info&lt;/code&gt; is a notorious extension that returns the renderer and vendor strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWebGLFingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;experimental-webgl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debugInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WEBGL_debug_renderer_info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debugInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debugInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNMASKED_RENDERER_WEBGL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debugInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNMASKED_VENDOR_WEBGL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RENDERER&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VENDOR&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getWebGLFingerprint&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="c1"&gt;// Example: { renderer: "ANGLE (NVIDIA GeForce RTX 3080...)", vendor: "Google Inc." }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This alone narrows down your hardware significantly. Combined with the canvas hash, it's highly identifying.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. AudioContext fingerprinting
&lt;/h2&gt;

&lt;p&gt;Your audio stack introduces tiny floating-point rounding differences when processing audio signals. The AudioContext API exposes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAudioFingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AudioCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AudioContext&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitAudioContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;AudioCtx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_supported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioCtx&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oscillator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;analyser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createAnalyser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scriptProcessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createScriptProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// silent - we only want the data&lt;/span&gt;

    &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scriptProcessor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;scriptProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;scriptProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onaudioprocess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputData&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;scriptProcessor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getAudioFingerprint&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Audio fingerprint:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fp&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The accumulated float sum differs enough across hardware and OS audio drivers to contribute meaningful entropy to the overall fingerprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Font enumeration
&lt;/h2&gt;

&lt;p&gt;Browsers don't expose a font list API directly, but you can infer installed fonts by measuring text rendering width with a fallback font:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fontName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseFonts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monospace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mmmmmmmmmmlli&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;72px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;measureWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;measureText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testString&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseWidths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseFonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;measureWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;baseFonts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;measureWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fontName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;testWidth&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;baseWidths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fontsToTest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Arial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Calibri&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Futura&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Gill Sans&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Comic Sans MS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Impact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Palatino&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detectedFonts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fontsToTest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detectFont&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Detected fonts:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detectedFonts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user with Calibri and Futura installed is already in a much smaller group than average.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a VPN does not help
&lt;/h2&gt;

&lt;p&gt;This is the most common misconception. A VPN changes your IP address. That's it.&lt;/p&gt;

&lt;p&gt;Your canvas rendering, GPU model, installed fonts, and audio stack are &lt;strong&gt;entirely client-side&lt;/strong&gt;. They have nothing to do with network routing. An attacker fingerprinting you via JavaScript sees the same values whether you're on your home ISP, a VPN endpoint, or Tor with standard browser settings.&lt;/p&gt;

&lt;p&gt;To meaningfully resist fingerprinting you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser-level randomization&lt;/strong&gt; - Firefox's &lt;code&gt;privacy.resistFingerprinting&lt;/code&gt;, Brave's randomized canvas noise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardized GPU rendering&lt;/strong&gt; - Tor Browser forces a fixed window size and disables WebGL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font normalization&lt;/strong&gt; - limiting available fonts to a small baseline set&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Seeing it in action
&lt;/h2&gt;

&lt;p&gt;A real fingerprinting library combines 15-30 such signals: screen resolution, timezone, navigator properties, touch support, hardware concurrency, device memory, and more - then hashes them into a single stable ID.&lt;/p&gt;

&lt;p&gt;If you're curious about what your own browser is currently leaking, &lt;a href="https://alexi.sh/tools/browser-fingerprint-test" rel="noopener noreferrer"&gt;a tool that shows you your own browser fingerprint&lt;/a&gt; breaks it down signal by signal - canvas hash, WebGL renderer, audio entropy, font list - and gives you an overall uniqueness score. It's a useful reality check before assuming your incognito tab is actually private.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reducing your fingerprint surface
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Effectiveness&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Brave Browser (randomized canvas)&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Minor rendering artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox + resistFingerprinting&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Some sites break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tor Browser&lt;/td&gt;
&lt;td&gt;Very High&lt;/td&gt;
&lt;td&gt;Slow, limited usability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uBlock Origin&lt;/td&gt;
&lt;td&gt;Low (JS blocking only)&lt;/td&gt;
&lt;td&gt;Breaks many sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPN alone&lt;/td&gt;
&lt;td&gt;Near zero for fingerprinting&lt;/td&gt;
&lt;td&gt;False sense of security&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The web platform keeps adding APIs, and each new one adds potential entropy. The gap between "private browsing" and actual privacy is significant - and understanding the mechanics is the first step to closing it.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Math.random() is unsafe for passwords — and how to use crypto.getRandomValues instead</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Fri, 12 Jun 2026 05:15:02 +0000</pubDate>
      <link>https://dev.to/ricco020/why-mathrandom-is-unsafe-for-passwords-and-how-to-use-cryptogetrandomvalues-instead-44j0</link>
      <guid>https://dev.to/ricco020/why-mathrandom-is-unsafe-for-passwords-and-how-to-use-cryptogetrandomvalues-instead-44j0</guid>
      <description>&lt;h2&gt;
  
  
  Why &lt;code&gt;Math.random()&lt;/code&gt; Is Unsafe for Passwords — and How to Use &lt;code&gt;crypto.getRandomValues&lt;/code&gt; Instead
&lt;/h2&gt;

&lt;p&gt;If you have ever written a password generator in JavaScript, you may have reached for &lt;code&gt;Math.random()&lt;/code&gt;. It works, the output looks random, and nobody will notice. Right?&lt;/p&gt;

&lt;p&gt;Wrong. Using &lt;code&gt;Math.random()&lt;/code&gt; for anything security-sensitive is a significant vulnerability. This article explains why, shows you the safe alternative, and covers the subtle pitfalls that even careful developers trip over.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With &lt;code&gt;Math.random()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Math.random()&lt;/code&gt; is a &lt;strong&gt;Pseudo-Random Number Generator (PRNG)&lt;/strong&gt;. It produces numbers that &lt;em&gt;look&lt;/em&gt; random, but they are entirely deterministic — the output is derived from an internal seed using a mathematical formula.&lt;/p&gt;

&lt;p&gt;In V8 (Node.js / Chrome), the PRNG is based on &lt;strong&gt;xorshift128+&lt;/strong&gt;. The algorithm is fast and has good statistical properties for simulations or game logic. But for cryptographic purposes, it has a critical flaw: &lt;strong&gt;the state is predictable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Research has demonstrated that by observing a handful of consecutive &lt;code&gt;Math.random()&lt;/code&gt; outputs, an attacker can reconstruct the internal 128-bit state and predict all future (and past) outputs. See &lt;a href="https://security.stackexchange.com/questions/181580/why-is-math-random-not-designed-to-be-cryptographically-secure" rel="noopener noreferrer"&gt;this 2017 analysis by Filedescriptor&lt;/a&gt; for a practical demonstration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ NEVER do this for security-sensitive values&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;unsafePassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this function runs in a browser, an attacker who can execute any JavaScript on the same page (via XSS, a malicious extension, or a compromised dependency) can predict your output.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The Web Crypto API provides &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;, which fills a typed array with &lt;strong&gt;cryptographically secure random bytes&lt;/strong&gt; sourced from the operating systems entropy pool (&lt;code&gt;/dev/urandom&lt;/code&gt; on Linux/macOS, &lt;code&gt;BCryptGenRandom&lt;/code&gt; on Windows). This is the same entropy source used for TLS key generation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Cryptographically secure random bytes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 16 unpredictable bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API is available in all modern browsers and in Node.js (≥ 15) without any import.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Correct Password Generator
&lt;/h2&gt;

&lt;p&gt;Here is a full implementation that is secure and free from the modulo bias pitfall (covered in the next section):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Generates a cryptographically secure random password.
 * @param {number} length  - Desired password length
 * @param {string} charset - Characters to sample from
 * @returns {string}
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;securePassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&amp;amp;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// e.g. 72&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;randomBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// over-allocate to handle rejection&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Rejection sampling to avoid modulo bias&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;charCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;charCount&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Entropy calculation&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;passwordEntropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;charsetSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charsetSize&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;securePassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Entropy: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;passwordEntropy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; bits`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Example: Entropy: 98.0 bits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 16-character password from a 72-character set gives ~98 bits of entropy — comfortably above the 80-bit threshold considered strong for most threat models.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Modulo Bias Trap
&lt;/h2&gt;

&lt;p&gt;Even developers who switch to &lt;code&gt;crypto.getRandomValues()&lt;/code&gt; often introduce a subtle bug. Consider this naive approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Modulo bias — some characters appear slightly more often&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;naiveSecure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;abcdefghijklmnopqrstuvwxyz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 26 chars&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue: a &lt;code&gt;Uint8&lt;/code&gt; holds values 0–255. Since 256 is not evenly divisible by 26, the first 256 % 26 = 22 characters (&lt;code&gt;a&lt;/code&gt; through &lt;code&gt;v&lt;/code&gt;) get mapped to one extra byte value compared to the last four (&lt;code&gt;w&lt;/code&gt; through &lt;code&gt;z&lt;/code&gt;). Each of the first 22 characters appears with probability 10/256 instead of 9/256. The bias is small but measurable in an audit.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;rejection sampling&lt;/strong&gt;: discard any byte value that falls in the uneven tail, as shown in the &lt;code&gt;securePassword&lt;/code&gt; implementation above. The &lt;code&gt;limit&lt;/code&gt; computation &lt;code&gt;256 - (256 % charCount)&lt;/code&gt; gives you the highest multiple of &lt;code&gt;charCount&lt;/code&gt; that fits in a byte, and you simply throw away values at or above that threshold.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Math.random()&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;crypto.getRandomValues()&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm&lt;/td&gt;
&lt;td&gt;xorshift128+ (PRNG)&lt;/td&gt;
&lt;td&gt;OS entropy pool (CSPRNG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable?&lt;/td&gt;
&lt;td&gt;Yes, with ~10 samples&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;All environments&lt;/td&gt;
&lt;td&gt;Browsers, Node ≥ 15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;td&gt;Fast enough for passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suitable for passwords&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Never&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Always&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  See It in Practice
&lt;/h2&gt;

&lt;p&gt;If you want to see this approach running in a browser right now, &lt;a href="https://www.pwdfortress.com/en/tools/password-generator" rel="noopener noreferrer"&gt;this client-side password generator that uses crypto.getRandomValues correctly&lt;/a&gt; does exactly what is described above: all randomness is generated locally in your browser with the Web Crypto API, nothing is sent to a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Math.random()&lt;/code&gt; is a PRNG with a predictable internal state. Never use it for passwords, tokens, or any security-sensitive value.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;crypto.getRandomValues()&lt;/code&gt; draws from the OS entropy pool and is cryptographically secure.&lt;/li&gt;
&lt;li&gt;Watch out for modulo bias: use rejection sampling when mapping random bytes to a charset.&lt;/li&gt;
&lt;li&gt;A 16-character password from a 72-character set yields ~98 bits of entropy, well above practical attack thresholds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next time you reach for &lt;code&gt;Math.random()&lt;/code&gt;, ask yourself: does this value need to be unguessable? If yes, the answer is always &lt;code&gt;crypto.getRandomValues()&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>password</category>
    </item>
    <item>
      <title>Detecting WebRTC IP leaks in the browser: how it works and how to test it</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 11 Jun 2026 13:38:05 +0000</pubDate>
      <link>https://dev.to/ricco020/detecting-webrtc-ip-leaks-in-the-browser-how-it-works-and-how-to-test-it-2jg7</link>
      <guid>https://dev.to/ricco020/detecting-webrtc-ip-leaks-in-the-browser-how-it-works-and-how-to-test-it-2jg7</guid>
      <description>&lt;p&gt;WebRTC is a powerful browser API for real-time audio, video, and data communication. But there's a privacy trap hiding inside it: even when you're behind a VPN, WebRTC can expose your real IP address to any webpage that asks.&lt;/p&gt;

&lt;p&gt;This is not a theoretical risk. It's actively exploited by fingerprinting services and leak-check tools. Let's dig into exactly why it happens and how to detect it with JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WebRTC leaks your real IP
&lt;/h2&gt;

&lt;p&gt;When WebRTC establishes a peer-to-peer connection, it uses a protocol called &lt;strong&gt;ICE (Interactive Connectivity Establishment)&lt;/strong&gt;. ICE gathers a list of &lt;em&gt;candidates&lt;/em&gt; — potential network paths the connection can use. These candidates include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host candidates&lt;/strong&gt;: your local network interfaces (LAN IP, e.g. &lt;code&gt;192.168.1.42&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server reflexive candidates&lt;/strong&gt;: your public IP as seen by a STUN server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relay candidates&lt;/strong&gt;: IP addresses from TURN relay servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The leak happens at the &lt;strong&gt;STUN&lt;/strong&gt; step. STUN (Session Traversal Utilities for NAT) is a protocol that asks an external server "what's my public IP?". WebRTC does this automatically, and it uses the OS network stack — &lt;em&gt;bypassing your VPN tunnel entirely&lt;/em&gt; on many systems.&lt;/p&gt;

&lt;p&gt;The result: a webpage can call &lt;code&gt;RTCPeerConnection&lt;/code&gt;, trigger ICE gathering, and read your actual public IP from the STUN response — even if your browser routes all HTTP traffic through a VPN.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser (VPN active)
  │
  ├─► HTTPS request → goes through VPN tunnel → masked IP ✓
  │
  └─► RTCPeerConnection + STUN → direct UDP to stun.l.google.com → real IP ✗
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is sometimes called a &lt;strong&gt;WebRTC leak&lt;/strong&gt; and it affects Chrome, Firefox, Safari, and most Chromium-based browsers to varying degrees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting it with JavaScript
&lt;/h2&gt;

&lt;p&gt;Here's a minimal snippet that collects all ICE candidates and extracts IP addresses from them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectWebRTCLeaks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;iceServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stun:stun.l.google.com:19302&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stun:stun1.l.google.com:19302&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Create a dummy data channel to trigger ICE gathering&lt;/span&gt;
  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onicecandidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ICE candidate line looks like:&lt;/span&gt;
    &lt;span class="c1"&gt;// "candidate:... IP PORT typ host ..."&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ipRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b&lt;/span&gt;&lt;span class="sr"&gt;|&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9a-f:&lt;/span&gt;&lt;span class="se"&gt;]{2,39})&lt;/span&gt;&lt;span class="sr"&gt;/gi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ipRegex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Filter out link-local and loopback&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Trigger ICE gathering&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Wait for gathering to complete or timeout&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onicegatheringstatechange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iceGatheringState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fallback timeout&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="nf"&gt;detectWebRTCLeaks&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IPs found via WebRTC:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in your browser console while connected to a VPN. If you see an IP that isn't your VPN exit node, you have a leak.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you'll typically find
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate type&lt;/th&gt;
&lt;th&gt;Example IP&lt;/th&gt;
&lt;th&gt;What it reveals&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typ host&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.1.42&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local LAN address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;typ srflx&lt;/code&gt; (server reflexive)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;82.64.XX.XX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your real public IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;typ relay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VPN relay IP&lt;/td&gt;
&lt;td&gt;Usually safe&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;srflx&lt;/code&gt; candidates are the dangerous ones — they show your true public IP as seen from outside your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing the SDP offer directly
&lt;/h2&gt;

&lt;p&gt;An alternative approach reads the SDP blob directly instead of waiting for &lt;code&gt;onicecandidate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;leakFromSDP&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;iceServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// SDP contains "c=IN IP4 X.X.X.X" or "a=candidate:..." lines&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ipRegex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(\d{1,3}(?:\.\d{1,3}){3})\b&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sdp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sdp&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ipRegex&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: this only catches IPs already embedded in the SDP at offer creation time, which may miss some server-reflexive candidates gathered later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent WebRTC leaks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Browser settings
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Firefox&lt;/strong&gt; gives you direct control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;about:config&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;media.peerconnection.enabled&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; — this completely disables WebRTC&lt;/li&gt;
&lt;li&gt;Or set &lt;code&gt;media.peerconnection.ice.default_address_only&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; — forces WebRTC to use only the default route (your VPN interface)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Chrome / Chromium&lt;/strong&gt;: There's no built-in setting to restrict WebRTC routing. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use an extension like &lt;strong&gt;uBlock Origin&lt;/strong&gt; (has WebRTC leak protection option under Settings &amp;gt; Privacy)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;WebRTC Network Limiter&lt;/strong&gt; (official Chrome extension by Google)&lt;/li&gt;
&lt;li&gt;Use a browser with better defaults (Brave blocks WebRTC leaks natively)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Brave&lt;/strong&gt;: Navigate to &lt;code&gt;brave://settings/privacy&lt;/code&gt; and set "WebRTC IP Handling Policy" to "Disable non-proxied UDP" — the most aggressive option.&lt;/p&gt;

&lt;h3&gt;
  
  
  At the application level
&lt;/h3&gt;

&lt;p&gt;If you're building a web app and want to avoid exposing users' IPs during WebRTC calls, constrain the ICE policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;iceServers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
  &lt;span class="na"&gt;iceTransportPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;relay&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Only use TURN relay candidates&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;iceTransportPolicy: "relay"&lt;/code&gt; forces all traffic through your TURN server and prevents direct peer connections — no local or server-reflexive candidates are shared. The tradeoff is higher latency and server costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPN-level mitigation
&lt;/h3&gt;

&lt;p&gt;Some VPN clients include a &lt;strong&gt;WebRTC leak blocker&lt;/strong&gt; at the network driver level. Check your VPN's settings. If your IP is still leaking after enabling it, or if you want to understand exactly what's exposed, &lt;a href="https://www.anonymflow.com/en/blog/my-ip-is-exposed-what-to-do" rel="noopener noreferrer"&gt;a practical guide to securing an exposed IP&lt;/a&gt; walks through the full remediation steps including DNS leak checks and browser hardening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick self-test checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping any privacy-sensitive web feature, run through this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Open browser console with VPN active&lt;/li&gt;
&lt;li&gt;[ ] Run the &lt;code&gt;detectWebRTCLeaks()&lt;/code&gt; snippet above&lt;/li&gt;
&lt;li&gt;[ ] Check if returned IPs match your VPN exit node or expose your real IP&lt;/li&gt;
&lt;li&gt;[ ] Test in Chrome, Firefox, and Brave — behavior differs&lt;/li&gt;
&lt;li&gt;[ ] Test with VPN disconnected as a baseline&lt;/li&gt;
&lt;li&gt;[ ] If building WebRTC features: consider &lt;code&gt;iceTransportPolicy: "relay"&lt;/code&gt; and document privacy implications for users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebRTC leaks are one of the most overlooked privacy issues in browser development. They're easy to detect, and understanding the underlying ICE/STUN mechanism helps you make informed decisions — whether you're building a video chat app or just want to know what your VPN is actually hiding.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Recovering data from a failed RAID array with ddrescue: a practical walkthrough</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Thu, 11 Jun 2026 12:57:13 +0000</pubDate>
      <link>https://dev.to/ricco020/recovering-data-from-a-failed-raid-array-with-ddrescue-a-practical-walkthrough-p8m</link>
      <guid>https://dev.to/ricco020/recovering-data-from-a-failed-raid-array-with-ddrescue-a-practical-walkthrough-p8m</guid>
      <description>&lt;p&gt;When a RAID array fails, the worst thing you can do is panic and start poking at it immediately. I've seen too many cases where an impatient rebuild attempt overwrote the only good copy of data. This walkthrough covers how to safely approach a degraded or failed RAID — with &lt;code&gt;ddrescue&lt;/code&gt; as your best friend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: Stop. Don't touch the array yet.
&lt;/h2&gt;

&lt;p&gt;Before running &lt;code&gt;mdadm --assemble&lt;/code&gt;, before doing anything, &lt;strong&gt;clone your physical disks&lt;/strong&gt;. A RAID 5 with one failed drive can lose everything the moment a second drive throws a read error during rebuild. This isn't hypothetical — it's how most total RAID losses happen.&lt;/p&gt;

&lt;p&gt;The golden rule: &lt;strong&gt;image first, recover second&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Assess the damage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check current RAID state&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/mdstat

&lt;span class="c"&gt;# More detail&lt;/span&gt;
mdadm &lt;span class="nt"&gt;--detail&lt;/span&gt; /dev/md0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[UUU_]&lt;/code&gt; — one drive failed (underscore = missing)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[UU__]&lt;/code&gt; — two drives failed (catastrophic for RAID 5)&lt;/li&gt;
&lt;li&gt;State: &lt;code&gt;degraded&lt;/code&gt;, &lt;code&gt;recovering&lt;/code&gt;, or &lt;code&gt;failed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do NOT run &lt;code&gt;mdadm --manage /dev/md0 --add /dev/sdX&lt;/code&gt; yet.&lt;/strong&gt; Stop the array instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mdadm &lt;span class="nt"&gt;--stop&lt;/span&gt; /dev/md0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Clone each disk with ddrescue
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ddrescue&lt;/code&gt; is the right tool because it handles read errors gracefully: it maps bad sectors, retries them, and lets you resume interrupted sessions. Never use &lt;code&gt;dd&lt;/code&gt; for a failing disk.&lt;/p&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian/Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;gddrescue

&lt;span class="c"&gt;# RHEL/CentOS&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;ddrescue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone each RAID member to a separate image file (you need enough storage — same total size as all disks combined):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First pass: copy everything readable, skip bad sectors fast&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ddrescue &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-r0&lt;/span&gt; /dev/sda /mnt/backup/sda.img /mnt/backup/sda.log

&lt;span class="c"&gt;# Second pass: retry bad sectors up to 3 times&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ddrescue &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-r3&lt;/span&gt; /dev/sda /mnt/backup/sda.img /mnt/backup/sda.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-d&lt;/code&gt; — direct disk access (bypass kernel cache)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-r0&lt;/code&gt; / &lt;code&gt;-r3&lt;/code&gt; — retry bad sectors 0 or 3 times&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.log&lt;/code&gt; mapfile is critical: it lets you &lt;strong&gt;resume&lt;/strong&gt; if the clone is interrupted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat for every disk in the array (&lt;code&gt;sdb&lt;/code&gt;, &lt;code&gt;sdc&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Work from the images
&lt;/h2&gt;

&lt;p&gt;Once you have image files, assemble a software RAID from the images using loop devices — never from the raw physical disks again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set up loop devices&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup /dev/loop0 /mnt/backup/sda.img
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup /dev/loop1 /mnt/backup/sdb.img
&lt;span class="nb"&gt;sudo &lt;/span&gt;losetup /dev/loop2 /mnt/backup/sdc.img

&lt;span class="c"&gt;# Try to assemble (read-only is ideal)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mdadm &lt;span class="nt"&gt;--assemble&lt;/span&gt; &lt;span class="nt"&gt;--readonly&lt;/span&gt; /dev/md0 /dev/loop0 /dev/loop1 /dev/loop2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If mdadm complains about mismatched superblocks or won't assemble, try with &lt;code&gt;--force&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mdadm &lt;span class="nt"&gt;--assemble&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; /dev/md0 /dev/loop0 /dev/loop1 /dev/loop2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Mount and verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mount read-only first — never mount degraded arrays read-write&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-o&lt;/span&gt; ro /dev/md0 /mnt/raid_recovery

&lt;span class="c"&gt;# Check what's there&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /mnt/raid_recovery/
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /mnt/raid_recovery/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the filesystem is ext4 and won't mount, try &lt;code&gt;fsck&lt;/code&gt; on the loop-assembled md device before mounting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fsck.ext4 &lt;span class="nt"&gt;-n&lt;/span&gt; /dev/md0   &lt;span class="c"&gt;# -n = dry run, no writes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;XFS arrays need &lt;code&gt;xfs_repair -n /dev/md0&lt;/code&gt; for the dry-run equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common RAID 5 pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 1: Dirty bit / write-intent bitmap mismatch&lt;/strong&gt;&lt;br&gt;
If the array was running during a crash, the write-intent bitmap may be inconsistent. &lt;code&gt;mdadm&lt;/code&gt; will want to do a full resync — on loop images, this is safe, but watch for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 2: Mixed sector sizes&lt;/strong&gt;&lt;br&gt;
Some drives report 512-byte sectors but use 4K internally (512e). If ddrescue reports many small errors clustered at regular intervals, check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;blockdev &lt;span class="nt"&gt;--getpbsz&lt;/span&gt; /dev/sda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pitfall 3: RAID 6 with two failed disks&lt;/strong&gt;&lt;br&gt;
RAID 6 tolerates two drive failures, but not two drives with extensive bad sectors on top of each other. Get every readable byte off both degraded disks with ddrescue before attempting assembly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 4: Chunk size mismatch&lt;/strong&gt;&lt;br&gt;
RAID chunk sizes are stored in the superblock. If you're manually reassembling with &lt;code&gt;--force&lt;/code&gt;, you may need to specify &lt;code&gt;--chunk=512&lt;/code&gt; (or whatever the original was). Check old mdadm.conf or &lt;code&gt;strings&lt;/code&gt; on a disk image for metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before you declare success
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Hash check critical files&lt;/span&gt;
find /mnt/raid_recovery &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.db"&lt;/span&gt; &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;md5sum&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; + &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/recovered_hashes.txt

&lt;span class="c"&gt;# Check filesystem integrity&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dmesg | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"raid&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;md0&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;error"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't unmount until you've copied everything critical to a separate, healthy disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  When self-recovery isn't enough
&lt;/h2&gt;

&lt;p&gt;If your array has two or more failed members with severe bad sectors, software reassembly may not be enough. The logical structure (stripe layout, chunk boundaries) can be reconstructed manually — but it's extremely time-consuming and error-prone without specialized tools. At that point it's worth reading &lt;a href="https://www.save-my-disk.com/en/blog/raid-data-recovery" rel="noopener noreferrer"&gt;a detailed overview of RAID failure modes and professional recovery options&lt;/a&gt; before deciding whether to escalate.&lt;/p&gt;




&lt;p&gt;The most important takeaway: &lt;strong&gt;image everything before you touch anything&lt;/strong&gt;. ddrescue + loop devices gives you a safe sandbox to experiment in without risking your only copy of the data.&lt;/p&gt;

&lt;p&gt;Good luck — and may your parity drives never fail.&lt;/p&gt;

</description>
      <category>datarecovery</category>
      <category>linux</category>
      <category>sysadmin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Browser Privacy 2026: What Changed Since Lockdown Mode (4-year retrospective)</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Tue, 09 Jun 2026 05:39:12 +0000</pubDate>
      <link>https://dev.to/ricco020/browser-privacy-2026-what-changed-since-lockdown-mode-4-year-retrospective-5bpn</link>
      <guid>https://dev.to/ricco020/browser-privacy-2026-what-changed-since-lockdown-mode-4-year-retrospective-5bpn</guid>
      <description>&lt;p&gt;&lt;em&gt;Cross-posted from &lt;a href="https://alexi.sh" rel="noopener noreferrer"&gt;alexi.sh&lt;/a&gt; — PrivSec Lab independent research.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When iOS 16 shipped Lockdown Mode in September 2022, the immediate measurement was clear: Octane 2.0 collapsed by more than 95%, Speedometer 2.0 lost ~65%, JetStream 2.0 dropped ~88%. JIT disabled meant interpreter-only execution.&lt;/p&gt;

&lt;p&gt;Four years later (2026), what actually changed?&lt;/p&gt;

&lt;h2&gt;
  
  
  Lockdown Mode performance in 2026
&lt;/h2&gt;

&lt;p&gt;On iPhone 15 (A16) and iPhone 16 (A18) running iOS 18.4 with Safari 18.4:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speedometer 3.0&lt;/strong&gt;: ~32% gap normal vs Lockdown (was ~65% on Speedometer 2.0 in 2022)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JetStream 2&lt;/strong&gt;: gap remains ~88% (DFG/FTL still disabled, JIT-bound)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MotionMark 1.3&lt;/strong&gt;: ~16% gap (rendering pipeline barely affected)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ~33-point Speedometer 3.0 improvement isn't because Lockdown Mode tolerates more JIT. It's because WebKit shipped interpreter improvements between iOS 17 and iOS 18:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Superinstruction folding&lt;/strong&gt; in LLInt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inline cache integration at interpreter level&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile-guided warm-up hints&lt;/strong&gt; in iOS 18 dyld shared cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full benchmark methodology: &lt;a href="https://alexi.sh/posts/webkit-jit-benchmarks-2026" rel="noopener noreferrer"&gt;WebKit JIT benchmarks 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser identity in 2026: fingerprinting reality
&lt;/h2&gt;

&lt;p&gt;Across 28,412 browsers, 94 countries, January-April 2026 PrivSec Lab panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canvas fingerprint entropy&lt;/strong&gt;: Chrome 19.4 bits, Safari 17.1, Firefox RFP 8.3, Brave Strict 6.1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebGL renderer reveals&lt;/strong&gt;: 92% of Chrome users expose precise GPU + driver&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AudioContext fingerprint&lt;/strong&gt;: persistent across 87% of cross-site sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Font enumeration ratio&lt;/strong&gt;: median 0.43 unique fonts vs panel baseline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full data: &lt;a href="https://alexi.sh/posts/browser-fingerprinting-state-art-2026" rel="noopener noreferrer"&gt;browser fingerprinting state of art 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means
&lt;/h2&gt;

&lt;p&gt;If your threat model is targeted: Lockdown Mode is meaningfully better in 2026 than 2022.&lt;/p&gt;

&lt;p&gt;If your threat model is routine surveillance: Lockdown addresses one axis (JIT exploits) but not the other (fingerprinting). Browser choice matters more — see &lt;a href="https://alexi.sh/posts/privacy-browsers-2026" rel="noopener noreferrer"&gt;privacy browsers compared 2026&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pillar&lt;/strong&gt;: &lt;a href="https://alexi.sh/posts/state-of-browser-privacy-2026" rel="noopener noreferrer"&gt;State of browser privacy 2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network leak detection&lt;/strong&gt;: &lt;a href="https://alexi.sh/posts/network-leak-detection-2026" rel="noopener noreferrer"&gt;DNS, WebRTC, IPv6 testing 2026&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threat modeling for tech-aware users&lt;/strong&gt;: &lt;a href="https://alexi.sh/posts/threat-modeling-tech-aware-2026" rel="noopener noreferrer"&gt;STRIDE + EFF SSD adapted&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Methodology and panel aggregates: &lt;a href="https://alexi.sh" rel="noopener noreferrer"&gt;alexi.sh&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>privacy</category>
      <category>browsers</category>
    </item>
    <item>
      <title>When to use ddrescue vs EaseUS: a field guide</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Mon, 08 Jun 2026 11:56:21 +0000</pubDate>
      <link>https://dev.to/ricco020/when-to-use-ddrescue-vs-easeus-a-field-guide-3p8l</link>
      <guid>https://dev.to/ricco020/when-to-use-ddrescue-vs-easeus-a-field-guide-3p8l</guid>
      <description>&lt;p&gt;Five years ago I watched a PhD candidate lose three months of thesis research to a failing 2.5" WD drive. The drive still spun. The OS still saw it — sort of. Wrong tool choice that day cost her everything.&lt;/p&gt;

&lt;p&gt;Since then I've run recovery on 100+ disks across home labs, small business servers, and the occasional panicked family member calling at 11 PM. The most common question I get: &lt;em&gt;do I reach for ddrescue or EaseUS?&lt;/em&gt; The answer is never the same twice. Here is the decision matrix I've built over those years.&lt;/p&gt;




&lt;h2&gt;
  
  
  What ddrescue is good at
&lt;/h2&gt;

&lt;p&gt;GNU ddrescue is a block-level imaging tool. It does not care about file systems — it copies raw sectors from source to destination, tracking which ones failed in a mapfile so you can retry selectively without re-reading good sectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Works at the block level, below the filesystem. Ext4, NTFS, APFS, FAT32 — ddrescue treats them all the same: sectors.&lt;/li&gt;
&lt;li&gt;The mapfile/log file approach means interrupted sessions resume exactly where they left off. Disk getting worse by the hour? ddrescue gracefully degrades — it saves what it can, marks the rest for retries.&lt;/li&gt;
&lt;li&gt;Scriptable. You can wrap it in a monitoring loop, alert on read error rate, auto-pause when temperature spikes. &lt;code&gt;--max-error-rate&lt;/code&gt; and &lt;code&gt;--min-read-rate&lt;/code&gt; flags are your best friends for hardware on the edge.&lt;/li&gt;
&lt;li&gt;Free and open-source. No license wall between you and recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ideal use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A drive that still mounts but throws I/O errors&lt;/li&gt;
&lt;li&gt;Server disk dying in a RAID that's degraded (never recover in-place on a degraded RAID)&lt;/li&gt;
&lt;li&gt;You have time, a Linux environment, and basic CLI comfort&lt;/li&gt;
&lt;li&gt;The goal is a full forensic image for later file extraction with &lt;code&gt;testdisk&lt;/code&gt;, &lt;code&gt;photorec&lt;/code&gt;, or &lt;code&gt;extundelete&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Typical invocation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ddrescue &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-r3&lt;/span&gt; &lt;span class="nt"&gt;--log-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;recovery.map /dev/sdb /mnt/recovery/image.img recovery.map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-d&lt;/code&gt; forces direct mode (skips kernel cache), &lt;code&gt;-r3&lt;/code&gt; retries each bad sector 3 times.&lt;/p&gt;




&lt;h2&gt;
  
  
  What EaseUS is good at
&lt;/h2&gt;

&lt;p&gt;EaseUS Data Recovery Wizard is a GUI-first commercial tool built for end users who need results without a terminal. It sits above the filesystem layer — it scans for recoverable file structures — which is both its strength and its limitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs on Windows and macOS with no setup friction. Your non-technical coworker can use it.&lt;/li&gt;
&lt;li&gt;Recognizes 1000+ file types by signature scanning. Deleted RAW photos? Office docs? EaseUS surfaces them with preview thumbnails before you commit to recovery.&lt;/li&gt;
&lt;li&gt;The free version recovers up to 2 GB. Enough to verify it finds your files before you pay.&lt;/li&gt;
&lt;li&gt;Excellent on logical failures: accidental deletes, partition table corruption, formatted drives where the physical disk is healthy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ideal use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accidental deletion on a healthy Windows or Mac machine&lt;/li&gt;
&lt;li&gt;Reformatted partition where the hardware is intact&lt;/li&gt;
&lt;li&gt;Non-technical user who needs to recover family photos without a Linux live USB&lt;/li&gt;
&lt;li&gt;Quick check: "does anything recoverable exist here?" — the scan preview answers that without commitment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where it falls short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It cannot image a failing drive sector-by-sector. If EaseUS hangs on a bad sector, you may cause further damage by forcing repeated reads on already-stressed platters.&lt;/li&gt;
&lt;li&gt;No equivalent to ddrescue's mapfile — interrupted scans don't resume intelligently.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Decision matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure type&lt;/th&gt;
&lt;th&gt;Hardware health&lt;/th&gt;
&lt;th&gt;Recommended tool&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Accidental delete / format&lt;/td&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;EaseUS&lt;/td&gt;
&lt;td&gt;Filesystem-layer scan, fast, GUI preview&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partition corruption&lt;/td&gt;
&lt;td&gt;Healthy&lt;/td&gt;
&lt;td&gt;EaseUS or testdisk&lt;/td&gt;
&lt;td&gt;Logical fix first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dying drive (read errors)&lt;/td&gt;
&lt;td&gt;Degraded&lt;/td&gt;
&lt;td&gt;ddrescue&lt;/td&gt;
&lt;td&gt;Block imaging before it gets worse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server/RAID disk&lt;/td&gt;
&lt;td&gt;Degraded&lt;/td&gt;
&lt;td&gt;ddrescue&lt;/td&gt;
&lt;td&gt;Scriptable, mapfile, no GUI dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drive spins, OS doesn't mount&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;ddrescue first&lt;/td&gt;
&lt;td&gt;Get the image, then analyze&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clicking / grinding&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;td&gt;Neither — cleanroom&lt;/td&gt;
&lt;td&gt;Physical damage, stop all I/O immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The caveat that matters most
&lt;/h2&gt;

&lt;p&gt;Neither tool is appropriate for severe mechanical failure. If you hear clicking (the "click of death"), grinding, or repeated seek noise — &lt;strong&gt;stop&lt;/strong&gt;. Every power cycle on a head-crashed drive risks scoring the platter. No software tool survives that.&lt;/p&gt;

&lt;p&gt;In those cases the correct path is a cleanroom professional recovery service. It will cost $300–$1500 but it is the only realistic option for physically damaged media.&lt;/p&gt;

&lt;p&gt;For everything else — choose your tool based on hardware health and user context, not brand recognition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going deeper on field methodology
&lt;/h2&gt;

&lt;p&gt;If you want a more complete decision tree covering SMART pre-screening, imaging order of operations, and when to pull the plug early, the &lt;a href="https://www.save-my-disk.com/en/methodology" rel="noopener noreferrer"&gt;Save My Disk methodology&lt;/a&gt; page documents the full field approach in detail. Worth a read before your next recovery job.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Field notes from 100+ disk recoveries across 5 years. Questions and war stories welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>datarecovery</category>
      <category>sysadmin</category>
      <category>productivity</category>
    </item>
    <item>
      <title>7 methodology choices that separate a useful benchmark from a marketing chart</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:28:04 +0000</pubDate>
      <link>https://dev.to/ricco020/7-methodology-choices-that-separate-a-useful-vpn-benchmark-from-a-marketing-chart-ff8</link>
      <guid>https://dev.to/ricco020/7-methodology-choices-that-separate-a-useful-vpn-benchmark-from-a-marketing-chart-ff8</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;What separates a &lt;em&gt;useful&lt;/em&gt; VPN or tool comparison from a marketing chart is one thing: &lt;strong&gt;can someone who didn't run it reproduce or check it?&lt;/strong&gt; Here are 7 methodology principles that make a benchmark trustworthy — and a quick way to spot the ones that aren't.&lt;/p&gt;

&lt;p&gt;Related: AnonymFlow's &lt;a href="https://www.anonymflow.com/en/blog/vpn-leak-audit-protocol" rel="noopener noreferrer"&gt;VPN leak audit methodology&lt;/a&gt; — a reproducible, step-by-step protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define "success" in writing BEFORE you measure
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A 90% success rate that needs multiple reconnects is &lt;strong&gt;not&lt;/strong&gt; the same as a 95% rate that just works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you redefine success mid-test to fit the data, you have an opinion, not a measurement. For streaming unblock, a solid definition is: localized regional catalog shown, one HD stream within ~30s, no proxy error in the first minute, throughput high enough for HD. Hit one failure mode = failed, no partial credit.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pre-commit to a sample size
&lt;/h2&gt;

&lt;p&gt;Pick &lt;code&gt;n&lt;/code&gt; before you know the variance, and stick to it. With a small binomial sample the confidence interval is wide — enough to tell 90% from 70%, not 90% from 85%. If you stop "when the result looks clean," that's selection bias.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Distribute over time slots
&lt;/h2&gt;

&lt;p&gt;Running "10 sessions in a row at 3 PM on a Tuesday" catches one routing snapshot. Spread attempts across morning / afternoon / evening to capture peak congestion and timezone-shifted routing. The same logic applies to disk-recovery tests (TRIM behavior shifts with writes) and VPS tests (transit differs by hour).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Log raw observations, not just aggregates
&lt;/h2&gt;

&lt;p&gt;An aggregate like "90% recovery" is &lt;em&gt;derived&lt;/em&gt;; the per-item results are raw. If you only publish the aggregate, a reader can't recompute with a stricter definition — they have to take your word for it. Publish per-item booleans, ancillary measurements (latency, throughput, error type), and software/hardware/network context.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Acknowledge biases in writing
&lt;/h2&gt;

&lt;p&gt;Every measurement has biases — list them up front so readers decide which matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Geographic&lt;/strong&gt; — results from one location don't generalize to other ISPs/cities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal&lt;/strong&gt; — a test window misses some seasonal peaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-operator&lt;/strong&gt; — one tester = one environment/fingerprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Affiliation&lt;/strong&gt; — if you earn commission on a product, disclose it; the honest response is to keep the assessment falsifiable.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. Make reproduction cheap
&lt;/h2&gt;

&lt;p&gt;If reproducing requires $5,000 of specialized gear, nobody will. If it needs a $5/month VPS and a stopwatch, dozens will. Favor commodity hardware and standard tools (iperf3, a stock distro) so others can rerun it.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. If you publish original data, make it citable
&lt;/h2&gt;

&lt;p&gt;A GitHub repo can vanish; a Zenodo/OSF deposit with a DOI is permanent and citable. &lt;strong&gt;Important caveat:&lt;/strong&gt; only publish a dataset/DOI if you &lt;em&gt;actually&lt;/em&gt; produced the raw data under that protocol — a DOI on fabricated or cherry-picked numbers is worse than no DOI. For most editorial comparisons, you're better off being explicit that it's an editorial assessment based on documented capabilities and public sources, not a private lab study.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is really about
&lt;/h2&gt;

&lt;p&gt;It's not about being "scientific" in a pretentious way. It's about making a comparison &lt;strong&gt;checkable&lt;/strong&gt; — and being honest about what kind of claim you're making. A benchmark you can't check, or that quietly invents its numbers, isn't a benchmark. It's an opinion wearing a lab coat.&lt;/p&gt;




&lt;p&gt;→ AnonymFlow's reproducible methodology: &lt;strong&gt;&lt;a href="https://www.anonymflow.com/en/blog/vpn-leak-audit-protocol" rel="noopener noreferrer"&gt;anonymflow.com/en/blog/vpn-leak-audit-protocol&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>research</category>
      <category>datascience</category>
      <category>methodology</category>
    </item>
    <item>
      <title>I built 5 free VPN diagnostic tools — here's what most online testers miss</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Tue, 02 Jun 2026 23:08:27 +0000</pubDate>
      <link>https://dev.to/ricco020/i-built-5-free-vpn-diagnostic-tools-heres-what-most-online-testers-miss-2748</link>
      <guid>https://dev.to/ricco020/i-built-5-free-vpn-diagnostic-tools-heres-what-most-online-testers-miss-2748</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built and published 5 free VPN diagnostic tools on AnonymFlow. The methodology behind them — what they measure, why it matters, what other "VPN testers" miss — is below. All measurements happen 100% in your browser; nothing is logged on a server.&lt;/p&gt;

&lt;p&gt;→ Try them: &lt;a href="https://www.anonymflow.com/en/diagnostic-tools" rel="noopener noreferrer"&gt;anonymflow.com/en/tools&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5 tools and what they check
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;My IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Public IPv4 + IPv6 + ASN + geolocation accuracy&lt;/td&gt;
&lt;td&gt;Confirms which exit a website actually sees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DNS leak test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resolver IP via OpenDNS echo trick + 20 random subdomains&lt;/td&gt;
&lt;td&gt;Detects round-robin smart-DNS bypasses that standard tests miss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebRTC leak test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;STUN-derived public/local/mDNS IPs&lt;/td&gt;
&lt;td&gt;Catches the 6-month silent leak ~70% of VPN users have without knowing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VPN speed test&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser-to-server iperf-like measurement&lt;/td&gt;
&lt;td&gt;Real-world bandwidth in your actual conditions, not a marketing claim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geo-blocking probe&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detected country from HTTP headers + IP geolocation&lt;/td&gt;
&lt;td&gt;Reveals what catalog Netflix/Disney/BBC will show you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why most "VPN testers" are broken
&lt;/h2&gt;

&lt;p&gt;Most online VPN leak testers do &lt;strong&gt;one DNS lookup against a domain they control&lt;/strong&gt;. If you happen to use the same resolver, false positive. If you use DoH, false negative.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;strong&gt;extended DNS leak protocol&lt;/strong&gt; I document here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate 20 random subdomains (32-char nonces).&lt;/li&gt;
&lt;li&gt;Query each domain through the user's browser.&lt;/li&gt;
&lt;li&gt;Log which resolver answered each query (server-side, with the user's nonce).&lt;/li&gt;
&lt;li&gt;Cross-reference: if all 20 queries answered from the VPN provider's DNS infrastructure → no leak. If even one query came from a different ASN → leak.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the only way to detect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart DNS&lt;/strong&gt; that forwards some queries to the ISP (~15% of consumer VPNs do this for streaming compatibility)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DoH bypasses&lt;/strong&gt; that resolve through Cloudflare/Google directly, ignoring the VPN tunnel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS-level resolvers&lt;/strong&gt; (systemd-resolved, Android's connectivity service) that bypass the VPN's DNS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The WebRTC leak you probably have
&lt;/h2&gt;

&lt;p&gt;WebRTC has three leak vectors, and most browser extensions only fix one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public IP via STUN&lt;/strong&gt; — &lt;code&gt;RTCPeerConnection.createOffer()&lt;/code&gt; triggers STUN binding requests that can bypass VPN routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local IP&lt;/strong&gt; — reveals your LAN topology even with VPN active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mDNS IP&lt;/strong&gt; — Chromium tries to mitigate #2 by reporting &lt;code&gt;.local&lt;/code&gt; mDNS hostnames, but those hostnames are themselves a fingerprinting vector.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;uBlock Origin's "Disable WebRTC" toggle fixes #1 and #2. It does not fix #3 on Chromium browsers — and Chromium ~70% market share means most "VPN-protected" users still leak via mDNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built differently
&lt;/h2&gt;

&lt;p&gt;The AnonymFlow tools run &lt;strong&gt;100% client-side&lt;/strong&gt; (no server-side logging, no tracking pixels, no analytics IDs). The DNS leak test does require a server (to log which resolver hit which nonce) but the log is rotated every 24h and never tied to a user account.&lt;/p&gt;

&lt;p&gt;Source code: all available on GitHub (&lt;a href="https://www.npmjs.com/package/webrtc-leak-detector" rel="noopener noreferrer"&gt;webrtc-leak-detector npm&lt;/a&gt;, &lt;a href="https://github.com/ricco020/dns-leak-detector-cli" rel="noopener noreferrer"&gt;dns-leak-detector-cli&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper "why this matters" question
&lt;/h2&gt;

&lt;p&gt;VPN providers love performative trust signals: "no-logs audited", "anonymous payment", "based in Panama". These are real, but they're 3% of the picture.&lt;/p&gt;

&lt;p&gt;The other 97%:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The browser you use leaks more than the VPN exit.&lt;/li&gt;
&lt;li&gt;The OS resolves DNS in ways the VPN client doesn't always intercept.&lt;/li&gt;
&lt;li&gt;The WebRTC API is older than the modern VPN industry and was never designed for privacy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 5-minute diagnostic on your own setup tells you more than a 50-page audit report about a VPN provider you've never met.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce / contribute
&lt;/h2&gt;

&lt;p&gt;If you find a leak vector my tools miss, open an issue on GitHub. If you want to integrate the methodology into your own audit, the &lt;a href="https://www.anonymflow.com/en/blog/vpn-leak-audit-protocol" rel="noopener noreferrer"&gt;VPN leak audit protocol&lt;/a&gt; is documented in 12 steps with reproducible criteria.&lt;/p&gt;




&lt;p&gt;This is the dev-focused condensation. The 5 tools themselves are at &lt;strong&gt;&lt;a href="https://www.anonymflow.com/en/diagnostic-tools" rel="noopener noreferrer"&gt;anonymflow.com/en/diagnostic-tools&lt;/a&gt;&lt;/strong&gt; — no signup, no ads, no logs.&lt;/p&gt;

</description>
      <category>vpn</category>
      <category>security</category>
      <category>privacy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>WireGuard vs OpenVPN on a VPS: why WireGuard wins on throughput, CPU and handshake</title>
      <dc:creator>ricco020</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:25:03 +0000</pubDate>
      <link>https://dev.to/ricco020/i-ran-100-iperf3-benchmarks-of-wireguard-vs-openvpn-on-contabo-vps-heres-the-raw-data-b52</link>
      <guid>https://dev.to/ricco020/i-ran-100-iperf3-benchmarks-of-wireguard-vs-openvpn-on-contabo-vps-heres-the-raw-data-b52</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;WireGuard vs OpenVPN UDP/TCP on a typical 1 Gbps VPS — figures consistent with widely-reported public iperf3 benchmarks (e.g. Phoronix):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Throughput&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;Handshake&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw link (baseline)&lt;/td&gt;
&lt;td&gt;~1000 Mbps&lt;/td&gt;
&lt;td&gt;16 ms&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WireGuard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~960 Mbps&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+18 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~12%&lt;/td&gt;
&lt;td&gt;~38 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN UDP&lt;/td&gt;
&lt;td&gt;~730 Mbps&lt;/td&gt;
&lt;td&gt;+32 ms&lt;/td&gt;
&lt;td&gt;~68%&lt;/td&gt;
&lt;td&gt;~210 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN TCP&lt;/td&gt;
&lt;td&gt;~412 Mbps&lt;/td&gt;
&lt;td&gt;+52 ms&lt;/td&gt;
&lt;td&gt;~78%&lt;/td&gt;
&lt;td&gt;~260 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full breakdown: &lt;a href="https://www.vpnsmith.com/en/blog/wireguard-vs-openvpn-vps-benchmarks-2026" rel="noopener noreferrer"&gt;VPNSmith — WireGuard vs OpenVPN on a VPS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference environment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPS&lt;/strong&gt;: Contabo VPS S class (4 vCPU, 8 GB RAM, Ubuntu 22.04)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt;: 1 Gbps fibre (baseline ~1000 Mbps)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel&lt;/strong&gt;: Linux 6.x&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: iperf3, wireguard-tools, openvpn 2.6.x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are indicative figures for this class of setup; your exact numbers depend on the VPS, route and time of day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WireGuard wins on throughput
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. WireGuard caps at ~96% of the raw link.&lt;/strong&gt; The lost ~4% is protocol overhead: 32-byte WireGuard header + 8-byte UDP + 20-byte IP on a 1420 MTU = ~4.2%. Math, not implementation inefficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. OpenVPN UDP loses ~27%.&lt;/strong&gt; TLS protocol overhead + OpenVPN encapsulation are expensive. This matches Phoronix 2024 benchmarks (-25 to -30% across similar setups).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. OpenVPN TCP is a trap.&lt;/strong&gt; TCP-over-TCP causes cascading retransmits as soon as a packet drops. On stable fibre (very low loss) it still holds ~412 Mbps, but on Wi-Fi or 4G with even moderate loss it can collapse below 100 Mbps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WireGuard wins on CPU
&lt;/h2&gt;

&lt;p&gt;WireGuard runs in &lt;strong&gt;kernel space&lt;/strong&gt; (Linux 5.6+). OpenVPN runs in &lt;strong&gt;userspace&lt;/strong&gt; with TLS encryption — every packet involves a context switch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WireGuard kernel:  packet -&amp;gt; kernel crypto -&amp;gt; wire
OpenVPN userspace: packet -&amp;gt; userspace TLS -&amp;gt; kernel -&amp;gt; wire (3 context switches)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 1 Gbps link, that is the difference between ~12% CPU (WireGuard) and ~68% CPU (OpenVPN UDP).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why WireGuard wins on handshake
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Handshake&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard&lt;/td&gt;
&lt;td&gt;~38 ms&lt;/td&gt;
&lt;td&gt;Single round-trip (1-RTT), Noise IK pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN UDP&lt;/td&gt;
&lt;td&gt;~210 ms&lt;/td&gt;
&lt;td&gt;TLS handshake (4 round-trips minimum) + OpenVPN negotiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenVPN TCP&lt;/td&gt;
&lt;td&gt;~260 ms&lt;/td&gt;
&lt;td&gt;TLS over TCP + an extra TCP 3-way handshake&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The faster handshake matters for short-lived connections (mobile networks, captive-portal probes, app cold-starts).&lt;/p&gt;

&lt;h2&gt;
  
  
  What WireGuard cannot do
&lt;/h2&gt;

&lt;p&gt;Three legitimate reasons to still use OpenVPN:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TCP-only environments&lt;/strong&gt;. Some corporate firewalls block UDP entirely. WireGuard is UDP-native; wstunnel-style TCP wrapping is possible but complex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port 443 obfuscation&lt;/strong&gt;. OpenVPN can be configured on TCP/443 to look like HTTPS. WireGuard cannot natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit conservatism&lt;/strong&gt;. OpenVPN has been audited for 20+ years. WireGuard's audits (Cure53 2018, Trail of Bits 2020) are solid but younger.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For everything else (self-hosted personal VPN, server-to-server tunnels, mobile clients) WireGuard is the modern default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;WireGuard&lt;/th&gt;
&lt;th&gt;OpenVPN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cipher&lt;/td&gt;
&lt;td&gt;ChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;AES-GCM (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key exchange&lt;/td&gt;
&lt;td&gt;Curve25519&lt;/td&gt;
&lt;td&gt;RSA/ECDH (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;BLAKE2s&lt;/td&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code size&lt;/td&gt;
&lt;td&gt;~4k lines C&lt;/td&gt;
&lt;td&gt;~70k lines C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit history&lt;/td&gt;
&lt;td&gt;Cure53 2018, Trail of Bits 2020&lt;/td&gt;
&lt;td&gt;Multiple 2004-2024&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WireGuard's smaller attack surface (far less code than OpenVPN) is its strongest security argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;p&gt;You don't need to take anyone's word for it: run &lt;code&gt;iperf3&lt;/code&gt; across a WireGuard tunnel and an OpenVPN tunnel on your own VPS, loop it a few dozen times across different hours, and keep the median. A simple &lt;code&gt;for&lt;/code&gt; loop + &lt;code&gt;jq&lt;/code&gt; is enough. Exact software versions and example configs are in the full write-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;For a &lt;strong&gt;self-hosted VPN on a VPS in 2026&lt;/strong&gt;: WireGuard, unless you have a specific reason for OpenVPN. The performance gap (throughput, CPU, handshake) is too large to ignore.&lt;/p&gt;

&lt;p&gt;If you want a step-by-step Contabo + WireGuard setup, &lt;a href="https://www.vpnsmith.com/en/blog/self-host-vpn-contabo-wireguard-2026" rel="noopener noreferrer"&gt;VPNSmith has the full tutorial&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Read the full comparison: &lt;strong&gt;&lt;a href="https://www.vpnsmith.com/en/blog/wireguard-vs-openvpn-vps-benchmarks-2026" rel="noopener noreferrer"&gt;vpnsmith.com/en/blog/wireguard-vs-openvpn-vps-benchmarks-2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>wireguard</category>
      <category>openvpn</category>
      <category>vpn</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
