zrok with the Power of Caddy

Michael QuigleyMichael Quigley
7 min read

It's that time again... we've just dropped a new zrok release! This time the marquee features are based on Caddy (https://caddyserver.com/). Caddy is a powerful Swiss Army Knife for serving and proxying HTTP resources, and we've integrated that power into the zrok ecosystem.

The Open-ended Caddyfile

zrok provides a new caddy backend mode, which allows you to utilize the full capabilities of Caddy in the zrok environment. This power is exposed through a Caddy feature called a Caddyfile. A Caddyfile is an expressive, powerful mechanism for configuring the different features available in Caddy.

Let's look at an example of a Caddyfile:

http:// {
    bind {{ .ZrokBindAddress }}
    reverse_proxy 127.0.0.1:3000
}

This example Caddyfile provides a simple reverse proxy implementation that works very much like the proxy backend mode. zrok will automatically rewrite the {{ .ZrokBindAddress }} token in the bind statement with the correct address for your share. This happens automatically when zrok loads the Caddyfile you specify as the target for the caddy backend mode.

This example Caddyfile is located at etc/caddy/simple_reverse_proxy.Caddyfile in the zrok repository on GitHub (https://github.com/openziti/zrok). We can run this example from an enabled zrok environment like this:


$ zrok share public --backend-mode caddy etc/caddy/simple_reverse_proxy.Caddyfile 
[   0.069]    INFO main.(*sharePublicCommand).run: access your zrok share at the following endpoints:
 https://3s9fddkilbi6.share.zrok.io
2023/09/06 18:12:43.582    INFO    admin    admin endpoint started    {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/09/06 18:12:43.583    WARN    http.auto_https    server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server    {"server_name": "srv0", "http_port": 80}
2023/09/06 18:12:43.583    INFO    tls.cache.maintenance    started background certificate maintenance    {"cache": "0xc000166c00"}
2023/09/06 18:12:43.584    INFO    tls    cleaning storage unit    {"description": "FileStorage:/home/michael/.local/share/caddy"}
2023/09/06 18:12:43.584    INFO    tls    finished cleaning storage units
2023/09/06 18:12:43.636    INFO    http.log    server running    {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2023/09/06 18:12:43.636    INFO    autosaved config (load with --resume flag)    {"file": "/home/michael/.config/caddy/autosave.json"}
[   0.127]    INFO sdk-golang/ziti.(*listenerManager).createSessionWithBackoff: {session token=[2a9ca2d1-21c2-4516-8b03-ffdd789283e6]} new service session

Notice that we've used --backend-mode caddy and the target of our share is the path to our Caddyfile.

zrok logs the ephemeral share URL: https://3s9fddkilbi6.share.zrok.io

If we access the URL with curl, we get the following result:

$ curl https://3s9fddkilbi6.share.zrok.io
Invalid Host header

In my environment, the "upstream" target specified in the Caddyfile is 127.0.0.1:3000, which corresponds to my local npm development server. Evidently this npm development server does not like to have a reverse proxy in front of it. If we rewrite the Host header hiding the fact that it's being proxied, we might get a different result.

Caddy supports a header_up directive nested within the reverse_proxy statement, which allows for "upstream" (traffic on its way to the proxied "backend") header rewriting. Let's try setting the Host header to make it look like the server is being accessed from localhost:3000:

http:// {
    bind {{ .ZrokBindAddress }}
    reverse_proxy 127.0.0.1:3000 {
        header_up Host localhost:3000
    }
}

If we restart our share and curl the new URL, we get:

$ curl http://e52lx23k7qgq.share.zrok.io
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="zrok ui"/>
...

This works! Now the npm development server thinks it's being accessed from localhost:3000.

Header rewriting is the first Caddy capability we've added that goes beyond the built-in --backend-mode proxy in zrok. But there are tons and tons of other things we can do with the caddy backend mode.

Let's look at a more complex example...

Multiple Upstream Backends

There are many reasons why you might want to utilize multiple upstream backends with a zrok share. You might want:

  • to distribute load across multiple backends, horizontally scaling the workload between multiple systems

  • to aggregate multiple microservice backends into a single combined API

  • to enforce additional security or performance constraints on the shared resources

  • to provide fault tolerance, allowing one or more backends to fail before the shared service becomes unavailable

Let's take a look at a simple example using the caddy backend mode to route traffic to two different backends:

http:// {
    # Bind to the zrok share
    bind {{ .ZrokBindAddress }}

    # Handle paths starting with `/zrok/*`
    # This will also strip the `/zrok/` from the path before sending to the backend
    handle_path /zrok/* {
        reverse_proxy https://zrok.io {
            header_up Host zrok.io
        }
    }

    # All other traffic goes to localhost:3000
    reverse_proxy /* 127.0.0.1:3000 {
        header_up Host localhost:3000
    }
}

Just like the first example, we bind our configuration to {{ .ZrokBindAddress }}. But in this case, we've added a second reverse_proxy statement. The new reverse_proxy statement is wrapped in a handle_path block. Wrapping the reverse_proxy statement with the handle_path causes Caddy to strip the /zrok path prefix before sending the request upstream to the backend. And like before, we're replacing the Host header to direct the traffic to the correct website instance (zrok.io).

We've qualified the existing reverse_proxy statement with /* to direct all other traffic to the backend at localhost:3000.

Let's see what happens when we send some traffic to this new Caddyfile. We'll share it using zrok share:

$ zrok share public --backend-mode caddy etc/caddy/multiple_upstream.Caddyfile 
[   0.296] WARNING zrok/endpoints/proxy.NewCaddyfileBackend: etc/caddy/multiple_upstream.Caddyfile [4] (): Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies
[   0.296]    INFO main.(*sharePublicCommand).run: access your zrok share at the following endpoints:
 https://u7cncngzhohb.share.zrok.io
2023/09/07 18:10:20.324    INFO    admin    admin endpoint started    {"address": "localhost:2019", "enforce_origin": false, "origins": ["//localhost:2019", "//[::1]:2019", "//127.0.0.1:2019"]}
2023/09/07 18:10:20.324    WARN    http.auto_https    server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server    {"server_name": "srv0", "http_port": 80}
2023/09/07 18:10:20.325    INFO    tls.cache.maintenance    started background certificate maintenance    {"cache": "0xc0005ff880"}
2023/09/07 18:10:20.325    INFO    tls    cleaning storage unit    {"description": "FileStorage:/home/michael/.local/share/caddy"}
2023/09/07 18:10:20.325    INFO    tls    finished cleaning storage units
2023/09/07 18:10:20.549    INFO    http.log    server running    {"name": "srv0", "protocols": ["h1", "h2", "h3"]}
2023/09/07 18:10:20.550    INFO    autosaved config (load with --resume flag)    {"file": "/home/michael/.config/caddy/autosave.json"}
[   0.549]    INFO sdk-golang/ziti.(*listenerManager).createSessionWithBackoff: {session token=[c16288b6-401e-4e0c-bacf-1661c5badd59]} new service session

If we curl our new share URL, https://u7cncngzhohb.share.zrok.io, we get this:

$ curl https://u7cncngzhohb.share.zrok.io
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="zrok ui"/>
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
...

And we can see that this is a response from our development npm server running on localhost:3000.

If we access a path starting with /zrok, we get this:

$ curl https://u7cncngzhohb.share.zrok.io/zrok/
<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <meta id="siteViewport" name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="profile" href="https://gmpg.org/xfn/11">
    <meta name='robots' content='index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' />

    <!-- This site is optimized with the Yoast SEO plugin v20.12 - https://yoast.com/wordpress/plugins/seo/ -->
    <title>Home - zrok</title>
...

This is the HTML from the zrok.io website.

Be sure to check out the Caddy reference documentation for even more possibilities for the caddy backend mode!

Improved Web and File Sharing

zrok provides a web backend mode for sharing files. In this mode, zrok behaves as if it is a traditional web server hosting static content on a filesystem. You can use this mode to share arbitrary files like documents, or code, or binaries. When sharing a tree of files like this, zrok provides a handy user interface to navigate the tree and download files.

Alternatively, if the tree of files contains index.html in any of the folders, the built-in file browser is suppressed, and the index.html is returned instead. This mimics the behavior of a traditional web server like Apache or nginx. This means you can use --backend-mode web to serve arbitrary directories containing files, and you can also use it to host static websites.

In the new v0.4.6 release of zrok, the --backend-mode web sharing mode has been improved to utilize Caddy for this function.

Here's what it looks like:

Caddy gives zrok a much nicer-looking file server experience. Built-in searching. Better MIME types. All together this makes for a much smoother and nicer experience.

There's a longer format "office hours" video dedicated to this new integration with Caddy. Check it out for a more leisurely stroll through these features, along with a little zrok debugging:

Conclusion

Bringing Caddy into zrok is a powerful new capability enabling lots of new sharing features. The new web backend mode implementation is just the first of many user experience and quality-of-life improvements we have planned for zrok users.

Before we're done with the Caddy integration, we plan on enhancing the zrok access functionality to allow you to use the powerful HTTPS certificate management and request routing capabilities on the frontend side of zrok. Imagine aggregating multiple zrok shares into a single zrok access command with fault-tolerance and failover capabilities!

And stay tuned for the new "zrok Drives" features. We're pretty excited about the doors that are going to open when it's released!

0
Subscribe to my newsletter

Read articles from Michael Quigley directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Michael Quigley
Michael Quigley