caddypangolin

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

caddy-pangolin

Caddy plugin that lets a local Caddy instance mirror the resources of a remote Pangolin instance, so LAN clients can reach your services directly without hairpinning traffic through your VPS (see fosrl discussion #685).

It provides three modules:

  • http.reverse_proxy.upstreams.pangolin — a dynamic upstreams source that resolves the backend for each request by matching the request Host against Pangolin's resource fullDomains and their targets (ip:port).
  • http.matchers.pangolin_https_backend — a request matcher that matches hosts whose Pangolin target uses https, so you can route them through a TLS transport.
  • http.matchers.pangolin_remote — a request matcher that matches hosts that exist in Pangolin but have no locally reachable targets, so you can route them back through the public Pangolin instance (see Remote sites).

A background poller fetches GET /v1/org/{org}/resources (plus per-resource targets for the http/https method) from Pangolin's Integration API and caches the map in memory. If Pangolin becomes unreachable, the last-known map keeps serving. Pollers are shared between modules with identical config.

Build

xcaddy build \
  --with github.com/abs3ntdev/caddy-pangolin \
  --with github.com/caddy-dns/cloudflare

Pangolin setup

  1. Enable the Integration API (default port 3003) and expose it to your LAN host (e.g. proxy it or tunnel it — it is a separate listener from the main API).
  2. Create an API key with permission to list resources and targets, scoped to your org. The key is used as Authorization: Bearer <id>.<secret>.

Caddyfile example

{
	http_port 8080
	https_port 8443
}

(pangolin_cfg) {
	endpoint https://pangolin.example.com:3003
	api_key {env.PANGOLIN_API_KEY}
	org_id myorg
	refresh 60s
}

*.example.com, example.org {
	tls {
		dns cloudflare {env.CF_API_TOKEN}
	}

	# hosts whose Pangolin target method is https (e.g. unraid, nextcloud)
	@https_backend pangolin_https_backend {
		import pangolin_cfg
	}
	reverse_proxy @https_backend {
		dynamic pangolin {
			import pangolin_cfg
		}
		transport http {
			tls
			tls_insecure_skip_verify
		}
	}

	# everything else
	reverse_proxy {
		dynamic pangolin {
			import pangolin_cfg
		}
	}
}

Point your local DNS wildcard (*.example.com) at this Caddy host and LAN traffic stays local; external traffic still flows through Pangolin on the VPS.

Options

Option Required Description
endpoint yes Base URL of the Pangolin Integration API (https://host:3003)
api_key yes API key as <id>.<secret>; placeholders like {env.X} supported
org_id yes Pangolin organization ID
refresh no Poll interval (default 60s)
sites no Site names or niceIds whose targets are locally reachable; targets on other sites are treated as remote (default: all sites local)
resolvers no DNS server addresses (port 53 assumed) used to resolve the Pangolin endpoint instead of the system resolver — set this when split-horizon DNS would resolve the endpoint back to this Caddy instance
insecure_skip_verify no Skip TLS verification when talking to the Pangolin API

Remote sites

If your Pangolin org has resources on sites that are not on this LAN, their targets are not reachable by the local Caddy. Set sites to the site(s) local to this Caddy instance, and use the pangolin_remote matcher to send everything else back through the public Pangolin instance:

(pangolin_cfg) {
	endpoint https://pangolin-api.example.com
	api_key {env.PANGOLIN_API_KEY}
	org_id default
	sites Home
}

*.example.com {
	@remote pangolin_remote {
		import pangolin_cfg
	}
	reverse_proxy @remote https://<vps-ip> {
		transport http {
			tls
			tls_server_name {http.request.host}
		}
	}

	reverse_proxy {
		dynamic pangolin {
			import pangolin_cfg
		}
	}
}

pangolin_remote matches hosts that exist in Pangolin but have no locally reachable targets. Requests are proxied to Pangolin's edge with SNI set to the original host so traefik routes and terminates them normally (auth rules included, since the request goes through the real Pangolin path).

Offline resilience

Every successful sync is persisted to disk under Caddy's data directory ($XDG_DATA_HOME/pangolin/<hash>.json, i.e. /config/pangolin/ on hotio). On startup the last snapshot is loaded from disk before any network call, so once the plugin has synced at least once:

  • Caddy restarts serve immediately from cache (no 503 window)
  • if the WAN or the Pangolin instance is down, local routing keeps working indefinitely with the last-known resource map
  • the poller keeps retrying in the background and replaces the cache as soon as Pangolin is reachable again

The cache is keyed by endpoint + org + sites, so config changes get a fresh cache rather than reusing a stale one.

Behavior notes

  • Only enabled resources with enabled targets are mapped; disabled ones 404 through your normal fallback.
  • Pangolin health checks are respected: targets marked unhealthy are removed from rotation while at least one healthy target remains. If every target of a resource is unhealthy, they are all kept (failing loudly beats hiding the resource).
  • Polling is cheap in steady state: the per-resource target details (http vs https method) are only refetched when the resource/target topology changes, so an idle org costs one API request per refresh interval.
  • Refreshes only log at INFO level when the resource map actually changed; unchanged polls log at DEBUG.
  • Wildcard resources (*.example.com) match any single-level subdomain.
  • Multiple enabled targets become multiple upstreams and use the reverse_proxy load balancing policy you configure.
  • Pangolin auth rules (SSO, pins, passwords) are NOT enforced by this plugin — local traffic bypasses Pangolin entirely. Gate the site block yourself (e.g. @block not remote_ip private_ranges + abort @block).

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	Endpoint           string
	APIKey             string
	OrgID              string
	Refresh            time.Duration
	InsecureSkipVerify bool
	// Sites restricts which Pangolin sites' targets are considered locally
	// reachable. Matches site name or niceId, case-insensitive. Empty means
	// all sites are considered local.
	Sites []string
	// Resolvers are DNS server addresses used to resolve the Pangolin
	// endpoint, bypassing the system resolver (useful with split-horizon
	// DNS where the endpoint hostname would resolve back to this Caddy).
	// Port 53 is assumed if not specified. Empty means system resolver.
	Resolvers []string
}

type HTTPSBackendMatcher

type HTTPSBackendMatcher struct {
	ModuleConfig
}

HTTPSBackendMatcher matches requests whose Pangolin resource has at least one locally reachable target with method "https". Use it to route those hosts through a reverse_proxy with an HTTPS transport.

func (HTTPSBackendMatcher) CaddyModule

func (HTTPSBackendMatcher) CaddyModule() caddy.ModuleInfo

func (*HTTPSBackendMatcher) Cleanup

func (m *HTTPSBackendMatcher) Cleanup() error

func (*HTTPSBackendMatcher) Match

func (m *HTTPSBackendMatcher) Match(r *http.Request) bool

func (*HTTPSBackendMatcher) MatchWithError

func (m *HTTPSBackendMatcher) MatchWithError(r *http.Request) (bool, error)

func (*HTTPSBackendMatcher) Provision

func (m *HTTPSBackendMatcher) Provision(ctx caddy.Context) error

func (*HTTPSBackendMatcher) UnmarshalCaddyfile

func (m *HTTPSBackendMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

type ModuleConfig

type ModuleConfig struct {
	// Base URL of the Pangolin integration API, e.g. https://pangolin-api.example.com
	Endpoint string `json:"endpoint,omitempty"`

	// API key in the form "<id>.<secret>" (sent as Authorization: Bearer).
	APIKey string `json:"api_key,omitempty"`

	// The Pangolin organization ID to list resources from.
	OrgID string `json:"org_id,omitempty"`

	// How often to refresh the resource map. Default: 60s.
	Refresh caddy.Duration `json:"refresh,omitempty"`

	// Skip TLS verification when talking to the Pangolin API.
	InsecureSkipVerify bool `json:"insecure_skip_verify,omitempty"`

	// Sites whose targets are locally reachable (name or niceId,
	// case-insensitive). Targets on other sites are treated as remote.
	// Empty means all sites are local.
	Sites []string `json:"sites,omitempty"`

	// Resolvers are DNS server addresses (port 53 assumed) used to resolve
	// the Pangolin endpoint instead of the system resolver. Set this when
	// split-horizon DNS would resolve the endpoint back to this Caddy.
	Resolvers []string `json:"resolvers,omitempty"`
	// contains filtered or unexported fields
}

ModuleConfig holds the user-facing configuration shared by all caddy-pangolin modules.

type RemoteMatcher

type RemoteMatcher struct {
	ModuleConfig
}

RemoteMatcher matches requests whose Pangolin resource exists but has no locally reachable targets (all its targets live on sites excluded by the `sites` filter). Use it to route those hosts back through the public Pangolin instance.

func (RemoteMatcher) CaddyModule

func (RemoteMatcher) CaddyModule() caddy.ModuleInfo

func (*RemoteMatcher) Cleanup

func (m *RemoteMatcher) Cleanup() error

func (*RemoteMatcher) Match

func (m *RemoteMatcher) Match(r *http.Request) bool

func (*RemoteMatcher) MatchWithError

func (m *RemoteMatcher) MatchWithError(r *http.Request) (bool, error)

func (*RemoteMatcher) Provision

func (m *RemoteMatcher) Provision(ctx caddy.Context) error

func (*RemoteMatcher) UnmarshalCaddyfile

func (m *RemoteMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

type Upstreams

type Upstreams struct {
	ModuleConfig
}

Upstreams is a dynamic upstream source that resolves the backend for a request by matching its Host against Pangolin resources.

func (Upstreams) CaddyModule

func (Upstreams) CaddyModule() caddy.ModuleInfo

func (*Upstreams) Cleanup

func (u *Upstreams) Cleanup() error

func (*Upstreams) GetUpstreams

func (u *Upstreams) GetUpstreams(r *http.Request) ([]*reverseproxy.Upstream, error)

func (*Upstreams) Provision

func (u *Upstreams) Provision(ctx caddy.Context) error

func (*Upstreams) UnmarshalCaddyfile

func (u *Upstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL