Reference

Custom obfs-https front site

Serve your own website (or mirror a public one) on the reality / obfs-https port — the config, and what a browser sees.

When you enable the REALITY / obfs-https transport, the node opens a TLS port that behaves as a gateway. Real MeshHold peers — whose TLS handshake carries an auth marker — get the encrypted mesh. Every other visitor (a browser, a port scanner, an ISP probe) is transparently forwarded to whatever dest points at.

So dest is the front site the outside world sees on that port. You can point it at any real HTTPS server: an existing public site, or your own. This page covers serving your own site; enabling the transport in the first place is in The Private Mesh VPN.

Serve your own site

1. Run an HTTPS server for the site

Run it on a local port the node can reach — e.g. 127.0.0.1:4443. It must terminate TLS with a certificate valid for your domain. MeshHold does a raw TCP relay and does not terminate TLS, so the certificate and private key live on this server, never in MeshHold.

Any server that speaks TLS 1.3 works. Caddy is the least effort because it obtains a Let's Encrypt certificate on its own:

# Caddyfile
yourdomain.com:4443 {
    root * /var/www/yoursite
    file_server
}

nginx, a Go http.ServeTLS, or anything else works too, as long as it presents a CA-trusted certificate for yourdomain.com.

2. Point dest at it

node:
  obfs:
    reality:
      enabled: true
      port: 443              # public HTTPS port visitors hit
      dest: "127.0.0.1:4443" # your front server
    order: ["plain", "reality", "ssh"]

Restart the node after editing config.yaml.

3. Point your domain at the node

Add a DNS A / AAAA record for yourdomain.com to the node's public IP, so browsers reach it by name and the certificate matches.

4. Verify

# dest reachable + TLS-1.3 / X25519 / ALPN compatible
meshhold check-reality-dest 127.0.0.1:4443

# the full path, exactly as a browser would take it
curl -I --resolve yourdomain.com:443:<node-public-ip> https://yourdomain.com/

The curl line should return HTTP/… 200 with no certificate error. Then open https://yourdomain.com/ in a browser — your site loads with a valid padlock.

What a visitor sees

  • By domain (https://yourdomain.com/, DNS → node) → the site loads with a valid padlock. The TLS session is end-to-end between the browser and your front server; the node only relays bytes.
  • By raw IP (https://<node-ip>/) → a certificate name-mismatch warning. That is normal for any HTTPS site hit by IP instead of by its hostname — the certificate is issued for a domain, not an address.
  • MeshHold peers on the same port still get the encrypted mesh. A browser can never reach it: without the peer auth marker it is always forwarded to dest.

Requirements & gotchas

  • The front server must terminate TLS itself with a CA-trusted certificate (Let's Encrypt, etc.). A plain-HTTP server won't work; a self-signed certificate shows a browser warning.
  • SNI passes through untouched — the front server picks its virtual host and certificate from the SNI the client sends.
  • The public port reality binds (e.g. 443) and the front server's port (e.g. 4443) must differ.
  • dest can also point at an external site — dest: "example.com:443" — instead of a local one. The node then relays visitors straight to that site, with no local server to run. Run meshhold check-reality-dest <host:port> on any candidate first.