jkunz faa56e5b6e
Docker (tags) / release (push) Failing after 1s
Release / build-and-release (push) Failing after 2m31s
v18.0.0
2026-07-25 21:33:25 +00:00
2026-07-25 21:33:25 +00:00
2026-07-25 21:33:25 +00:00
2026-07-25 21:33:25 +00:00
2025-05-19 17:34:48 +00:00
2024-02-15 20:30:38 +01:00
2024-02-15 20:30:38 +01:00
2024-02-15 20:30:38 +01:00
2026-07-25 21:33:25 +00:00
2026-07-15 17:40:35 +00:00
2026-07-25 21:33:25 +00:00

@serve.zone/dcrouter

dcrouter is the serve.zone datacenter gateway runtime: a TypeScript control plane that brings HTTP/HTTPS/TCP routing, email ingress, authoritative DNS, RADIUS, VPN access control, remote ingress tunnels, certificate operations, metrics, and an Ops dashboard into one process.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Why It Exists

Modern infrastructure often has too many tiny edge tools: a proxy here, a DNS daemon there, a separate cert worker, another dashboard, and a tunnel process bolted on later. dcrouter is designed as a cohesive gateway layer for operators who want one audited place to define public routes, domains, edge tunnels, access policy, and operational state.

Highlights:

  • 🌐 SmartProxy-backed HTTP, HTTPS, TCP, TLS/SNI, source-policy rate limits/challenges, managed Special Forwards, and optional HTTP/3 route handling
  • 📬 SmartMTA-backed SMTP ingress, email-domain operations, and managed WorkApp SMTP credentials for gateway clients
  • 🧭 SmartDNS-backed authoritative DNS plus generated DNS-over-HTTPS routes
  • 🔐 ACME with managed-domain DNS-01 support, certificate state, API tokens, users, source profiles, and target profiles
  • 🛡️ RADIUS, VLAN assignment, VPN-protected routes, IP intelligence, block rules, and remote ingress firewall snapshots
  • 🖥️ Browser Ops dashboard and TypedRequest API served by the built-in OpsServer

Runtime Areas

Area What dcrouter manages
Proxying SmartProxy routes for HTTP, HTTPS, TCP, SNI, TLS termination, passthrough, backend forwarding, source policies, rate limits, and browser challenges
Route ownership Constructor routes, generated email/DNS routes, and API-created routes with explicit origins
DNS Delegation-verified authoritative zones, generated NS records, static DNS records, provider-backed domains, and DoH endpoints
Email UnifiedEmailServer startup, email-domain management, route-backed delivery actions, received mail operations, managed app address bindings, and outbound SMTP submission identities
Certificates ACME config, managed-domain DNS-01 challenges, HTTP-01 fallback, stored certificate metadata, provisioning backoff, and certificate status reporting
Edge access Remote ingress hub, edge registrations, derived edge ports, pushed firewall rules, VPN-only route access
Network auth RADIUS clients, MAC Authentication Bypass, VLAN mapping, and accounting sessions
Security policy DB-backed block rules, public-IP intelligence, compiled SmartProxy deny policy, RemoteIngress firewall snapshots, and audit events
Operations Dashboard views, TypedRequest handlers, metrics, logs, health, API tokens, users, and configuration views

Install

Install the CLI/runtime on a Linux gateway host with the released self-extracting binary:

curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash

The installer downloads dcrouter-linux-x64 or dcrouter-linux-arm64 from the latest Gitea release, installs it under /opt/dcrouter, and links /usr/local/bin/dcrouter. Use --version vX.Y.Z to pin a release, --install-dir /path to change the target directory, or --source to clone the tag and build the NodeNext package locally.

curl -sSL https://code.foss.global/serve.zone/dcrouter/raw/branch/main/install.sh | sudo bash -s -- --source

Use the package as a TypeScript library:

pnpm add @serve.zone/dcrouter

Quick Start

This starts the gateway on unprivileged ports and stores data under the default ~/.serve.zone/dcrouter base directory.

import { DcRouter } from '@serve.zone/dcrouter';

const router = new DcRouter({
  coreTrafficConfig: {
    routes: [
      {
        name: 'local-app',
        match: {
          domains: ['localhost'],
          ports: [18080],
        },
        action: {
          type: 'forward',
          targets: [{ host: '127.0.0.1', port: 3001 }],
        },
      },
    ],
  },
  dbConfig: {
    enabled: true,
  },
  opsServerPort: 3000,
});

await router.start();

After startup:

  • open the dashboard at http://localhost:3000
  • complete the first-admin bootstrap flow if no persisted admin account exists yet
  • send proxied traffic to http://localhost:18080
  • stop gracefully with await router.stop()

Initial Admin Bootstrap

When DB-backed persistence is enabled and no persisted admin exists, dcrouter does not auto-create an admin account. The Ops dashboard exposes a non-cancelable first-admin bootstrap flow that must be completed explicitly.

Bootstrap behavior:

  • getAdminBootstrapStatus reports whether persistence is ready and whether a first admin is required.
  • The temporary env/config admin identity is only used to authorize bootstrap access while no persisted admin exists.
  • createInitialAdminUser creates the first persisted admin with normalized email and local password authentication.
  • Optional idp.global authentication can be enabled for that local account. The hosted https://idp.global endpoint is used by default, adminAuth.idpGlobalUrl or DCROUTER_IDP_GLOBAL_URL only override it, and the local dcrouter role remains authoritative.
  • After a persisted admin exists, temporary bootstrap admin login is rejected and normal persisted-account authentication is used.

Configuration Model

DcRouter is configured with IDcRouterOptions from @serve.zone/dcrouter.

Option Purpose
baseDir Root directory for dcrouter runtime data. Defaults to ~/.serve.zone/dcrouter.
coreTrafficConfig Main CoreTraffic route configuration for HTTP/HTTPS/TCP/SNI traffic. smartProxyConfig remains accepted as a legacy alias.
emailConfig UnifiedEmailServer configuration: hostname, ports, domains, and mail routes.
emailOutboundMode Outbound SMTP mode. Defaults to direct; remoteIngress routes outbound SMTP through a RemoteIngress egress edge.
emailPortConfig External-to-internal email port mapping and received-email storage path.
tls Legacy/static TLS and ACME contact settings used to seed certificate config.
dnsNsDomains Nameserver hostnames used for generated NS records and DoH routes. A zone becomes authoritative by having its public NS records observed naming one of these.
dnsRecords Constructor-defined DNS records.
publicIp / proxyIps IPs used for generated A records and proxy-aware DNS exposure.
dbConfig Smartdata persistence via embedded LocalSmartDb or external MongoDB.
radiusConfig RADIUS authentication, accounting, and VLAN assignment.
remoteIngressConfig Remote ingress hub configuration for edge tunnel nodes.
vpnConfig VPN server/client definitions and VPN-only routing behavior.
http3 HTTP/3 augmentation settings for qualifying HTTPS routes.
opsServerPort Port for the Ops dashboard and /typedrequest API. Defaults to 3000.

Important runtime behavior:

  • dbConfig.enabled defaults to enabled. Without mongoDbUrl, dcrouter uses embedded LocalSmartDb.
  • If the DB is disabled, constructor-defined proxy traffic can still run, but persistent API routes, tokens, managed domains, and stored certificate state are unavailable. The embedded DNS server is also skipped entirely, because DNS authority is delegation-verified database state — the DoH routes are still generated, but nothing answers behind them.
  • Qualifying HTTPS forward routes on port 443 are HTTP/3-augmented unless http3.enabled === false or the route opts out.
  • DNS-over-HTTPS routes are generated on the first dnsNsDomains entry at /dns-query and /resolve.
  • Email listener ports can be remapped internally, for example public 25, 587, and 465 to unprivileged internal ports.
  • emailOutboundMode: 'remoteIngress' requires an enabled RemoteIngress hub and an eligible connected QUIC edge with SMTP egress enabled. If no eligible edge is available, outbound delivery fails or defers instead of silently falling back to direct SMTP from the hub.

Web Push Provider

Web Push is disabled by default and does not read its secret configuration while disabled. It requires DB-backed persistence and the 17.8.0 migration. Enable it only after all three secret values are present:

Environment variable Value
DCROUTER_WEB_PUSH_ENABLED true, 1, on, or yes starts the provider and worker. false, 0, off, no, unset, or empty disables it. Any other value fails startup.
DCROUTER_WEB_PUSH_MASTER_KEY_RING JSON or a base64Object: JSON value containing AES-256 keys used for encrypted VAPID and queued-delivery envelopes.
DCROUTER_WEB_PUSH_HMAC_KEY_RING The same key-ring shape, with different key material, used for credentials and opaque request/endpoint digests.
DCROUTER_WEB_PUSH_VAPID_SUBJECT An HTTPS URL or mailto: contact passed to push services.

Each key ring has the form {"currentKeyId":"key-2026-07","keys":{"key-2026-07":"<32-byte-unpadded-base64url>"}}, supports at most eight distinct keys, and must name a configured current key. Encryption and HMAC rings must never reuse key material.

Operational behavior:

  • Control-plane callers need gateway-client readWebPush or manageWebPush capability. App calls use the one-time binding credential returned by syncWebPushBinding; the active credential record stores only its HMAC verifier. To recover a lost rotation response, dcrouter may return the same issuance again during its 15-minute overlap window; that recovery secret is AES-GCM encrypted at rest and is removed when the window expires.
  • Every sync or delete carries a durable controller id, stable epoch, monotonically increasing generation, operation id, and intent. Sync accepts only enabled; delete accepts disabled or deleted, an exact owner, and an optional binding id. Exact same-generation retries resume cleanup and replay the durable outcome, while lower generations, changed controller epochs, or conflicting same-generation requests fail closed. An owner-reserving delete tombstone is created even when a timed-out create has not returned a binding id, so the late create cannot reactivate the provider.
  • A binding admits 120 new requests per minute and 5,000 per hour, atomically across workers and restarts. Matching idempotent replays do not consume admission. The nonterminal queue is capped at 10,000 items per binding.
  • Payloads are limited to 3,500 UTF-8 bytes, TTL must be from 1 through 86,400 seconds, delivery makes at most eight attempts, and each retry sends the remaining TTL. Terminal rows retain only non-secret delivery metadata for seven days.
  • Subscription endpoints must be public HTTPS hostnames on port 443. DNS is bounded and pinned for the request; IP literals, credentials, fragments, and any answer containing a non-global address are rejected.
  • App credential rotation has a 15-minute overlap. VAPID rotation retains retiring keys for 90 days and, if necessary, until old queued items have terminally drained, with four VAPID keys maximum. Rotate before reaching that bound.
  • For a key-ring rotation, add new unique key material, set currentKeyId, restart, and keep old encryption keys until every VAPID, delivery, and credential-recovery envelope using them has been rotated or purged. Keep each old HMAC key only until no current or overlap credential references its key id; terminal and idempotency retention does not require the old HMAC key. Remove old keys only after those conditions are true.
  • deleteWebPushBinding is a destructive tombstone operation: it revokes and removes all credential hashes, prior credentials, credential-recovery envelopes, and encrypted VAPID private keys; cancels and scrubs queued work; and clears admission state. Queued work is terminalized immediately, but an already sending request remains sending until the remote response wins or its lease expires. An expired lease after cancellation is recorded as a failed delivery with an unknown remote outcome, never as a confirmed cancellation. Delete/disable reports success only after that lifecycle drains; a bounded drain timeout returns a retryable failure, and the same controller operation resumes cleanup. Re-enabling the same owner creates a new lifecycle, credential, and VAPID key. Clients must recreate browser push subscriptions and must not reuse deleted credentials.
  • The worker performs bounded maintenance every cycle and at startup, repeatedly removing expired credential overlap/recovery state, drained retiring or retired VAPID private keys, stale admission state, and incomplete disabled/deleted lifecycle cleanup without materializing an unbounded collection.

For a staged rollout, deploy with DCROUTER_WEB_PUSH_ENABLED=false, confirm the migration and health of the rest of dcrouter, install the three secret values, enable one gateway-client binding, and exercise status/enqueue/delivery/cancellation before expanding. To roll back worker activity, set the feature flag to false and restart; durable unexpired items remain encrypted and resume after re-enable. Do not delete bindings as a rollback mechanism because deletion is intentionally irreversible.

Route Ownership

dcrouter keeps generated and operator-created routes separate so automation can reconcile safely.

Origin Source Mutability
config Constructor coreTrafficConfig.routes and seed data Toggle only
email Email listener and email-domain generated routes Toggle only
dns Generated DNS-over-HTTPS and DNS-related routes Toggle only
api Ops UI, specialized managed-route workflows, and gateway clients Operator routes use generic CRUD; managed routes use their specialized API; toggle remains available

System routes are persisted with stable systemKey values. Ordinary operator-created API routes are editable through generic route CRUD. Routes carrying managed ownership metadata, including Special Forwards and gateway-client routes, reject generic structural updates and deletion so their owning workflow keeps canonical match, priority, source-policy, and ownership fields intact.

DNS Authority

Which zones the embedded DNS server may answer for authoritatively is database state, not deployment configuration. There is no dnsScopes option.

A zone enters the authority set exactly one way: its public delegation must name one of dnsNsDomains, observed through a single DNS-over-HTTPS lookup against a public resolver. The system resolver is never used for this — on a dcrouter host it may be dcrouter itself, which would answer with the very NS records dcrouter generated and make the proof self-confirming. An ops-API caller cannot repoint somebody else's delegation, so writing the record is not the same as manufacturing the proof.

Operation Scope Effect
getDnsAuthority dns-authority:read The authority set, its evidence, and whether it was readable.
probeDnsAuthorityZone dns-authority:read Read-only delegation probe. Never mutates.
verifyDnsAuthorityZone dns-authority:write Claims a zone, only against a delegated verdict.
revokeDnsAuthorityZone dns-authority:write Drops a zone. Every zone is revocable.
getDnsAuthorityDrift dns-authority:read Advisory comparison of claimed authority against reality.

Writes accept an admin identity or an API token carrying dns-authority:write; a non-admin identity is refused. Probes return three verdicts, never two: delegated, not-delegated, and undeterminable. A timeout is not evidence that a zone is not ours, so a mutation refuses on undeterminable rather than assuming either way.

Claiming or revoking a zone takes effect in-process, with no restart: it moves the running DNS server's authoritative zone set, its generated apex NS records, route certificate warnings, and the private-route overlay together.

Cold start. A database with no authority document is a legitimate state, and it means dcrouter is authoritative for nothing and REFUSES every query. The DNS server still starts — so DoH keeps serving and a zone verified a moment later takes effect immediately — and the condition is logged at error alongside a startup drift audit listing every dcrouter-hosted zone delegated to us that is not being served. An authority document that cannot be read is treated differently: the set is unknown rather than empty, so the DNS services fail and retry instead of quietly revoking every zone. dcrouter itself still comes up — both services are optional — so the rest of the router keeps running while DNS stays deliberately down.

Route Source Bindings

API-created route records pass ordered metadata.sourceBindings[] alongside the SmartProxy route config to express source and path policy variants without duplicating whole routes by hand. Each binding points at a source profile id through sourceProfileRef. Dashboard presets resolve seeded profile names to ids before saving.

Source profiles store reusable defaults only. A source profile is not enforced globally by itself; dcrouter compiles it into SmartProxy routes only when a route references it through metadata.sourceBindings[].

Runtime behavior:

  • Source matching uses the referenced SourceProfile.security.ipAllowList.
  • Bindings are evaluated in order and the first matching source profile wins.
  • A matched binding that exceeds its configured rate or connection limit is terminal; dcrouter does not fall through to later bindings. Rate limits normally return 429, or trigger a browser challenge when rateLimit.onExceeded.type === 'challenge'.
  • Source-binding rate limits are always keyed by source IP; dcrouter ignores path and header keying on source-binding and path-policy overrides.
  • Source-binding and path-policy rateLimit and challenge fields use tri-state semantics: omitted means inherit, null means explicitly clear inherited protection, and an object means custom protection.
  • Effective protection precedence is path policy, then route binding, then source profile, then parent source profile, then no protection.
  • When sourceBindings[] are present, dcrouter compiles source-policy variants from source profile, binding, and path policy protection. Base route rateLimit and challenge values are not part of that inheritance chain; routes without source bindings use normal SmartProxy route security.
  • Direct challenge protection applies to matching HTTP-visible requests. rateLimit.onExceeded.type === 'challenge' configures a browser challenge for rate-limit exceed events.
  • Binding-level and path-policy onExceeded is only for 429 message handling. Browser challenge-on-exceeded behavior belongs on rateLimit.onExceeded.
  • Private-only binding lists are valid. dcrouter adds a same-match terminal deny fallback so unmatched sources fail closed.
  • A public or wildcard binding is optional. When present, it must be last and must use *, or both 0.0.0.0/0 and ::/0, in security.ipAllowList.
  • Create/update paths reject source bindings with missing source profiles, source profiles without source matches, or any all-source binding that shadows later bindings; persisted invalid bindings fail closed at compile time.
  • Server-side caps bound policy expansion to 16 source bindings, 12 path policies per binding, 64 path patterns per path policy, 256 characters and 8 wildcards per custom path pattern, 512 compiled SmartProxy route-port variants per stored route, and enough priority headroom above the stored route priority for generated source-binding variants.

Path policies let a source binding override rate limits, browser challenges, or connection limits for specific path classes. dcrouter currently ships Gitea-oriented classes: git-smart-http, static, normal-html, expensive-html, raw, and archive. Path-specific variants win over the same binding's fallback; if every path policy is path-specific, dcrouter adds a source-level fallback route for unmatched paths so normal browsing cannot fall through to a later source binding. The Gitea preset keeps git-smart-http high-limit and separate from HTML crawling paths so normal git clone, git fetch, git push, and Git LFS traffic are not subject to the lower HTML crawler limits.

const trustedProfileId = 'source-profile-id-trusted';
const publicProfileId = 'source-profile-id-public';

const createRoutePayload = {
  route: {
    name: 'public-gitea',
    match: { domains: ['code.example.com'], ports: [443] },
    action: {
      type: 'forward',
      targets: [{ host: '10.10.0.20', port: 3000 }],
      tls: { mode: 'terminate', certificate: 'auto' },
    },
  },
  metadata: {
    sourceBindings: [
      {
        sourceProfileRef: trustedProfileId,
        rateLimit: null,
        maxConnections: 5000,
        onExceeded: { type: '429' },
      },
      {
        sourceProfileRef: publicProfileId,
        onExceeded: { type: '429' },
        pathPolicies: [
          {
            pathClass: 'git-smart-http',
            rateLimit: { enabled: true, maxRequests: 1200, window: 60, keyBy: 'ip' },
          },
          {
            pathClass: 'static',
            rateLimit: { enabled: true, maxRequests: 600, window: 60, keyBy: 'ip' },
          },
          {
            pathClass: 'raw',
            rateLimit: { enabled: true, maxRequests: 120, window: 60, keyBy: 'ip' },
          },
          {
            pathClass: 'archive',
            rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
          },
          {
            pathClass: 'expensive-html',
            rateLimit: { enabled: true, maxRequests: 30, window: 60, keyBy: 'ip' },
          },
          {
            pathClass: 'normal-html',
            rateLimit: {
              enabled: true,
              maxRequests: 120,
              window: 60,
              keyBy: 'ip',
              onExceeded: {
                type: 'challenge',
                challenge: {
                  providerId: 'smartchallenge',
                  challengeType: 'wait',
                  applyTo: { methods: ['GET'], browserNavigationsOnly: true },
                  clearance: { ttlSeconds: 300, bindToHost: true, bindToRoute: true },
                },
                clearanceEffect: 'bypass-rate-limit',
              },
            },
          },
        ],
      },
    ],
  },
};

Source Profiles

Source profiles are reusable source-side security defaults. They can carry ipAllowList, ipBlockList, maxConnections, rateLimit, authentication fields, VPN fields, and challenge. Profiles can extend parent profiles; IP allow/block lists are unioned and scalar/object protection fields such as maxConnections, rateLimit, and challenge are overridden by the more specific profile.

When dbConfig.seedOnEmpty seeds an empty database, dcrouter's built-in profile set includes:

Profile Default purpose
TRUSTED NETWORKS Private networks, localhost, and high connection allowance
AI CRAWLERS Placeholder crawler profile with low per-IP request limits until verified crawler CIDRs are added
PUBLIC Public fallback profile with per-IP request limiting
STANDARD Standard private-network access profile

The source profile dashboard supports protection modes for rate limits and browser challenges: inherit/unset, none/clear inherited, or custom. Custom challenge settings expose the provider id, challenge type, and clearance TTL used by source-policy route compilation.

Challenge Support

dcrouter registers the smartchallenge provider with the wait challenge type when SmartProxy starts. Source policies can apply challenges in two ways:

  • challenge on a source profile, route binding, or path policy protects matching HTTP-visible traffic directly.
  • rateLimit.onExceeded.type: 'challenge' configures the rate-limit exceeded path to issue the configured challenge.

Challenge-aware source-policy variants and HTTP/3-augmented challenged routes force HTTP protocol matching where needed so SmartProxy can process browser challenges correctly.

ACME And Certificate Challenges

DB-backed ACME configuration drives SmartProxy certificate provisioning. When ACME is enabled and dcrouter has managed domains, DnsManager builds the DNS-01 provider used by SmartAcme. That provider creates and removes challenge TXT records through the same DNS record path used for dcrouter-hosted zones and provider-managed domains.

SmartAcme is configured with DNS-01 priority for managed domains. If SmartAcme is still starting or retrying account setup, the certificate provision callback falls back to HTTP-01 for that request. Issued certificates are stored through the proxy certificate store, and certificate status is tracked from both newly issued and store-loaded certificate events.

Operators can also create a managed Special Forward for an external Let's Encrypt HTTP-01 responder. The workflow accepts exact domains plus either an inline backend or a reusable Network Target. dcrouter compiles that intent into a public port 80 route matching only /.well-known/acme-challenge/*, assigns priority 1000, binds the PUBLIC source profile, and can optionally expose the route through RemoteIngress. The high-priority path route coexists with the normal HTTP-to-HTTPS redirect for the same domain.

Use the Routes view's Type multitoggle and select Special, or use the raw TypedRequest methods createLetsEncryptHttp01Forward, updateLetsEncryptHttp01Forward, and deleteLetsEncryptHttp01Forward. Generic route update/delete methods intentionally reject these managed routes.

Security Policy And IP Intelligence

When DB-backed persistence is enabled, SecurityPolicyManager maintains global deny policy from block rules and observed public-IP intelligence. Rule types are ip, cidr, asn, and organization; organization rules support exact or contains matching against enriched ASN and registrant organization data.

The compiled policy contains blockedIps and blockedCidrs. dcrouter merges it into SmartProxy's security policy and also compiles an IPv4 firewall snapshot for RemoteIngress edge synchronization. Security policy changes are audited when block rules are created, updated, or deleted.

The OpsServer exposes these security policy methods through TypedRequest: listSecurityBlockRules, createSecurityBlockRule, updateSecurityBlockRule, deleteSecurityBlockRule, listIpIntelligence, refreshIpIntelligence, getCompiledSecurityPolicy, and listSecurityPolicyAudit.

Production-Flavored Example

import { DcRouter } from '@serve.zone/dcrouter';

const router = new DcRouter({
  baseDir: '/var/lib/dcrouter',
  coreTrafficConfig: {
    routes: [
      {
        name: 'web-app',
        match: { domains: ['app.example.com'], ports: [443] },
        action: {
          type: 'forward',
          targets: [{ host: '10.10.0.21', port: 8080 }],
          tls: { mode: 'terminate', certificate: 'auto' },
        },
      },
      {
        name: 'internal-admin',
        match: { domains: ['admin.example.com'], ports: [443] },
        action: {
          type: 'forward',
          targets: [{ host: '10.10.0.30', port: 9000 }],
          tls: { mode: 'terminate', certificate: 'auto' },
        },
        vpnOnly: true,
      },
    ],
  },
  emailConfig: {
    hostname: 'mail.example.com',
    ports: [25, 587, 465],
    domains: [{ domain: 'example.com', dnsMode: 'internal-dns' }],
    routes: [
      {
        name: 'inbound-example',
        match: { recipients: '*@example.com' },
        action: {
          type: 'forward',
          forward: { host: 'mail-backend.example.com', port: 25 },
        },
      },
    ],
  },
  emailOutboundMode: 'remoteIngress',
  dnsNsDomains: ['ns1.example.com', 'ns2.example.com'],
  // Which zones the embedded DNS server answers for is not configured here.
  // A zone earns authority by its public delegation naming dnsNsDomains, and
  // that proof lives in the database — see the "DNS Authority" section above.
  publicIp: '203.0.113.10',
  remoteIngressConfig: {
    enabled: true,
    tunnelPort: 8443,
    hubDomain: 'ingress.example.com',
  },
  vpnConfig: {
    enabled: true,
    serverEndpoint: 'vpn.example.com',
    clients: [{ clientId: 'ops-laptop', description: 'Operations laptop' }],
  },
  opsServerPort: 3000,
});

await router.start();

VPN Target Profiles

Target profiles define what a VPN client can reach through domains, direct targets, and routeRefs. Set allowRoutesByClientSourceIp: true on a target profile when a VPN client should also be granted to routes whose source policy is meant to evaluate the client's real connecting IP.

dcrouter maps target profiles to SmartProxy VPN client grants. SmartVPN forwards both the real client source IP and authenticated VPN metadata through trusted PROXY v2 headers, so SmartProxy checks source policy and VPN client authorization separately for each connection. Route security.ipAllowList and security.ipBlockList stay the source of truth for real source-IP policy; vpnOnly adds the requirement for authenticated VPN metadata and a matching VPN client grant.

const targetProfile = {
  name: 'ops laptop source access',
  allowRoutesByClientSourceIp: true,
};

Automation API

The OpsServer exposes TypedRequest handlers at /typedrequest. You can use raw contracts or the object-oriented API client.

It also exposes an admin-JWT authenticated read-only MCP endpoint at /mcp. The MCP tools return safe summaries for runtime status, routes, source and target profiles, network targets, DNS, email domains, RemoteIngress edges, and VPN clients without API tokens, logs, certificates, private keys, or provider credentials.

pnpm add @serve.zone/dcrouter-apiclient
import { DcRouterApiClient } from '@serve.zone/dcrouter-apiclient';

const client = new DcRouterApiClient({
  baseUrl: 'https://dcrouter.example.com',
});

await client.login('admin@example.com', 'strong-password');

const route = await client.routes.build()
  .setName('api-gateway')
  .setMatch({ ports: 443, domains: ['api.example.com'] })
  .setAction({ type: 'forward', targets: [{ host: '127.0.0.1', port: 8081 }] })
  .save();

await route.toggle(true);

Use @serve.zone/dcrouter/interfaces or @serve.zone/dcrouter-interfaces when you want dcrouter-local raw TypedRequest contracts instead of resource managers. Use @serve.zone/interfaces for the canonical machine-facing gateway client route, DNS, domain, and mail contracts shared with Onebox and Cloudly.

Gateway-client mail contracts let Cloudly or Onebox claim exact app addresses, attach inbound smtpForward targets, enable managed outbound SMTP credentials, rotate those credentials, enqueue service mail, and query delivery status through TypedRequest. Managed SMTP users can only send as their exact claimed address; mismatched envelope or header From values are rejected before relay. Delivery status queries return the dcrouter spool item state, including deferred SmartMTA errors and next retry timestamps when outbound delivery is temporarily delayed.

RemoteIngress SMTP Egress

The email settings API and Ops UI expose outboundMode. Keep it at direct to let SmartMTA dial destination MX hosts from the dcrouter hub. Set it to remoteIngress to make SmartMTA request a one-shot RemoteIngress TCP egress proxy for each outbound SMTP connection while keeping the logical MX host and port for SMTP/TLS identity.

RemoteIngress edge create/update APIs accept an egress policy. Egress is default-disabled and is propagated to the hub only when egress.enabled === true.

Supported egress policy fields:

Field Purpose
enabled Enables edge-originating outbound TCP egress for that edge.
allowedPorts Destination ports allowed by dcrouter. Currently limited to SMTP port 25.
allowedHostPatterns Optional MX host patterns evaluated by the edge before dialing.
allowPrivateRanges Allows private destination ranges when explicitly enabled. Loopback, metadata, multicast, unspecified, documentation, shared, and other reserved ranges remain blocked by the RemoteIngress edge.
deniedCidrs CIDRs denied after edge-side DNS resolution.
maxConcurrentStreams Optional concurrent outbound SMTP stream cap for the edge.

RemoteIngress status responses include capabilities, egressEnabled, and source-bound egress identity. A mail edge is eligible only when it is enabled, carries the mail tag, permits destination port 25, is connected with a fresh heartbeat, uses native QUIC without TCP fallback, advertises both egressTcpV1 and egressIdentityV1, and reports a fresh non-stale localSocketBind identity. That identity must match the edge's declared publicIp or publicIpV6, the edge must have a mailHostname, its PTR must resolve exactly to that hostname, and the hostname's A/AAAA set must contain the source address. Domain edge filters narrow this pool and remain fail-closed when they match nothing.

RemoteIngress v5 edge runtimes must configure egressSourceIpv4 and/or egressSourceIpv6 with concrete addresses assigned to the edge host. The equivalent CLI flags are --egress-source-ipv4 and --egress-source-ipv6; the environment variables are REMOTEINGRESS_EGRESS_SOURCE_IPV4 and REMOTEINGRESS_EGRESS_SOURCE_IPV6. These addresses must match dcrouter's declared edge IPs. Every outbound socket is bound to the matching family, and a missing or unbindable family fails closed. There is no OS-selected address or direct-hub fallback in remoteIngress mode.

Managed email domains use the durable lifecycle pending -> active | failed -> deleting; the document is removed only after deletion cleanup completes. Creation must obtain a usable DKIM key before persistence, then automatically reconciles and validates MX, SPF, DKIM, DMARC, and direct per-edge A/AAAA records. Inbound acceptance is independent and remains available while outbound SMTP credentials and sending are gated until the domain has a validated activeRevision and a live eligible edge.

One eligible edge publishes one priority 10 MX target. Two or more eligible edges publish distinct stable priority 10 and priority 20 targets, each backed directly by A/AAAA rather than CNAME. SPF uses -all and authorizes every IPv4/IPv6 source identity required by the active or make-before-break transition revision. Activation requires the complete MX RRset, all record intents, and exact PTR plus forward-confirmed A/AAAA validation. Topology replacement validates the desired state before activation, then removes stale owned records.

The reconciler is globally serialized and coalesces concurrent triggers. It runs at startup, every five minutes, and after relevant domain, DNS, DKIM, topology, or egress-identity changes. Provider state is refreshed on each pass; only records marked managedBy: 'mail-dns-reconciler' are mutated or deleted. Unmanaged SPF, DMARC, DKIM, or MX conflicts fail visibly instead of being overwritten. Each domain persists desired and active revisions, per-record intent status, provider/validation errors, attempt and validation timestamps, capability state, and retryAt. Provider/API failures propagate through the email-domain API and never become false provisioning success.

DKIM generation failure is atomic. Automatic rotation defaults to 90 days with a 30-day retiring-selector overlap. dcrouter publishes and validates a staged selector, requires SmartMTA's selector-correct signing capability, promotes it for signing only after the DNS revision validates, retains the previous selector for the overlap, and later removes the owned record.

The additive public contracts include IEmailDomainActionResult, IEmailDomain.reconciliation, lifecycle/error/capability/retry fields, active/pending/retiring DKIM material, DNS intents and revisions, and edge identities. For mutation requests, success: true means the operation was accepted and durably recorded; lifecycleStatus: 'pending' or 'deleting' still means activation or cleanup has not completed.

OCI / Container Bootstrap

runCli() supports an environment-driven container mode when DCROUTER_MODE=OCI_CONTAINER.

import { runCli } from '@serve.zone/dcrouter';

await runCli();

Supported environment overrides include:

Variable Purpose
DCROUTER_CONFIG_PATH JSON file loaded as the base IDcRouterOptions object.
DCROUTER_BASE_DIR Runtime data root.
DCROUTER_TLS_EMAIL / DCROUTER_TLS_DOMAIN TLS/ACME seed settings.
DCROUTER_PUBLIC_IP / DCROUTER_PROXY_IPS Public/proxy IP exposure settings.
DCROUTER_DNS_NS_DOMAINS Nameserver hostnames. DCROUTER_DNS_SCOPES is no longer honored — DNS authority is delegation-verified database state, and a container still setting it is warned at startup.
DCROUTER_EMAIL_HOSTNAME / DCROUTER_EMAIL_PORTS Email server seed settings.
DCROUTER_CACHE_ENABLED Enables or disables DB-backed persistence.
DCROUTER_MAX_CONNECTIONS, DCROUTER_MAX_CONNECTIONS_PER_IP, DCROUTER_CONNECTION_RATE_LIMIT SmartProxy capacity and rate-limit overrides.

Docker Image

Release builds publish a multi-arch OCI image at code.foss.global/serve.zone/dcrouter:latest for linux/amd64 and linux/arm64. The image sets DCROUTER_MODE=OCI_CONTAINER and starts node ./cli.js.

docker run --rm --name dcrouter \
  --network host \
  -v dcrouter-data:/data \
  -e DCROUTER_BASE_DIR=/data \
  -e DCROUTER_TLS_EMAIL=ops@example.com \
  code.foss.global/serve.zone/dcrouter:latest

Host networking is the simplest container mode for a gateway that owns HTTP/S, SMTP, DNS, RADIUS, remote ingress, and dynamic proxy ports. For narrower deployments, publish only the ports you enable in IDcRouterOptions or via the DCROUTER_* environment overrides.

Published Modules

This repository intentionally publishes multiple module boundaries from one codebase.

Module Purpose Docs
@serve.zone/dcrouter Main runtime and orchestrator ./readme.md
@serve.zone/dcrouter/interfaces Shared contracts as a subpath export ./ts_interfaces/readme.md
@serve.zone/dcrouter/apiclient API client as a subpath export ./ts_apiclient/readme.md
@serve.zone/dcrouter-interfaces Standalone contracts package ./ts_interfaces/readme.md
@serve.zone/dcrouter-apiclient Standalone OO API client package ./ts_apiclient/readme.md
@serve.zone/dcrouter-migrations Standalone migration runner package ./ts_migrations/readme.md
@serve.zone/dcrouter-web Dashboard frontend module boundary ./ts_web/readme.md

Development

pnpm run build
pnpm test
pnpm run watch

Useful source entry points:

  • ts/index.ts exports DcRouter, runCli(), and public module surfaces.
  • ts/classes.dcrouter.ts owns service startup, dependency ordering, and IDcRouterOptions.
  • ts/opsserver/classes.opsserver.ts wires the dashboard server and TypedRequest handlers.
  • ts/remoteingress/ integrates @serve.zone/remoteingress with stored edge registrations.
  • ts_migrations/index.ts contains all DB schema migration steps.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
a traffic router intended to be gating your datacenter.
Readme 9.7 MiB
2026-07-15 17:41:26 +00:00
Languages
TypeScript 99.6%
Shell 0.3%