2026-09-28 15:11:41 +00:00
2024-05-15 10:10:41 +02:00
2026-09-28 15:11:41 +00:00
2024-05-15 10:10:41 +02:00
2026-09-28 15:11:41 +00:00
2024-05-15 10:10:41 +02:00
2024-05-15 10:10:41 +02:00
2024-05-15 10:10:41 +02:00
2026-09-28 15:11:41 +00:00
2024-05-15 10:10:41 +02:00

CoreTraffic

CoreTraffic is the serve.zone ingress service. In its default cluster-ingress mode it is a cluster's ingress on Pallet: it registers with the cluster's relay, pulls the route table Cloudly computes, and applies it to @push.rocks/smartproxy for HTTP redirects, TLS termination, reverse proxying, plain TCP/UDP port forwards, default response headers and optional basic authentication. In standalone mode it exposes a CoreTraffic admin API for orchestrators such as Onebox.

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.

Runtime Modes

CORETRAFFIC_MODE selects the runtime mode:

Mode Purpose
cluster-ingress Default. Registers with the cluster's relay and serves the route table Cloudly computes.
standalone Starts the proxy engine from CORETRAFFIC_CONFIG and exposes the CoreTraffic admin API.

CoreTraffic 33 removed the coreflow mode: Coreflow 33 no longer serves the :3000 socket it dialled. A cluster ingress now reaches its cluster through the relay, as below.

Cluster-Ingress Mode

CoreTraffic is intentionally narrow. It is not the control plane and it does not discover services by itself. Cloudly computes the table (it is the only party that knows every service, every ready endpoint and every certificate), the cluster's relay holds it in memory, and CoreTraffic applies it.

Cloudly -- pushClusterRouteTable --> cluster relay (Coreflow)
                                        |  TypedSocket 8 over TLS, the listener the nodes dial
CoreTraffic -- registerClusterIngress -->|
            <-- pushClusterIngressRouteTableChanged { revision }
            -- getClusterIngressRouteTable { knownRevision } -->
            -> SmartProxy.updateRoutes(...)

Cloudly designates the ingress service with designateClusterIngress and writes three names into its workload's assignment environment (clusterIngressEnvironmentNames in @serve.zone/interfaces). They are the only environment cluster-ingress mode reads, and qenv.yml requires all three: a start or --check without them is refused naming every missing one, from readClusterIngressSettings (ts/coretraffic.clusteringress.environment.ts).

Variable Meaning
SERVEZONE_CLUSTER_RELAY_ORIGIN The exact https: origin of the cluster's relay, as Cloudly published it; judged by validateClusterRelay.
SERVEZONE_CLUSTER_NODE_ID The node this ingress runs on, as its assignment names it; repeated in the registration.
SERVEZONE_CLUSTER_INGRESS_BEARER The ingress bearer Cloudly minted with the designation (16 to 4096 characters). A secret: qenv reads it from the environment or a mounted secret, it travels in registerClusterIngress alone, and no log line, status body or refusal ever quotes it.

At startup CoreTraffic:

  • Judges the three names, before it binds any port.
  • Starts a fresh SmartProxy with no routes, and the status listener.
  • Opens one TypedSocket 8 client to the relay origin in the background. registerClusterIngress (bearer, node id, version, protocol offer) runs as the connection's restoration, so every connection and every reconnect registers before it counts as connected, and nothing else reaches the relay on a connection Cloudly did not accept.
  • Pulls getClusterIngressRouteTable with the revision it applied (null before the first), after every accepted registration, on every pushClusterIngressRouteTableChanged hint whose revision is newer than the applied one, and every 60 s while connected, so a lost hint costs at most one resync.

The protocol offer states the installed @serve.zone/interfaces release and accepts peers from 32.22.0 on, the release that named the registration refusals, the change hint and these environment names. The answer's offer, Cloudly's, is judged the same way.

Refusals and backoff

A refused registration never ends the ingress: it defers the next registration on the same client.

  • A named refusal (cluster-ingress-registration-refused with a reason and retryAfterMs, read with readClusterIngressRegistrationRefusal) is waited out by exactly its retryAfterMs.
  • A protocol refusal (protocol-incompatible) is waited out by the shared refusedOfferRetryIntervalMs (5 min): only an upgrade of one side settles it.
  • A rejection that names neither, and a registration that fails in transit, is waited out by a doubling backoff from 1 s to 300 s, the contract's bounds, reset by the next acceptance.

Each distinct refusal is logged once. A relay that is unreachable, a session TypedSocket ended (a denied restoration, exhausted retries or a failed handshake) and a client that stayed disconnected across two checks are all replaced by a new client after a doubling wait from 1 s to 60 s.

Applying a table

A table is judged by validateClusterRouteTable before SmartProxy sees it, and applied only when its revision is greater than the applied one. A table the contract or SmartProxy refuses leaves the running routes and the applied revision untouched, and is logged with its first reason, which never quotes certificate material. The table lives in memory only: SmartProxy holds each certificate for its route and never writes one to disk, and nothing logs a route.

Container port Route Purpose
7999 http-to-https-redirect Redirects HTTP traffic to https://{domain}{path} with status 301.
8000 https-<hostname> One route per table route: terminates TLS with the route's certificate and forwards to every destination address:port (the workload lease address and target port). Enables QUIC/HTTP/3 with transport: 'all' and advertises Alt-Svc on port 443. Basic authentication when the route states one.
listenPort public-<name> One plain forward per table port route, over its tcp or udp transport.
3000 status listener Read-only GET /health and GET /ready, bound on all interfaces for the runtime's readiness probe.

The ingress service's runtime spec publishes 7999 as the uplink's public 80 and 8000 as 443 (TCP and UDP). Every managed route receives a response header named servezone_coretraffic_version with the running package version.

Readiness

  • GET /ready answers 200 once the first table is applied and SmartProxy runs, and 503 with { ok: false, error } before that, for example "error": "no cluster route table has been applied yet". It stays 200 through a relay outage: the ingress keeps serving the last applied table.
  • GET /health answers 200 while the listener serves.

Both carry lifecycleState, smartProxyRunning, appliedRevision, routes, portRoutes, appliedAt and relay: { connection, registered, relayRevision, refusal }: revisions, counts and the standing refusal, never a route, a certificate or the bearer.

Traffic statistics

getClusterTrafficStatistics, which the relay carries from Cloudly to the registered ingress, is answered with the named refusal cluster-traffic-statistics-unavailable: this release collects no day-bucketed statistics, and an empty page would read as a day without traffic.

Usage

CoreTraffic is normally started by the platform as a service. For direct cluster-ingress use, with the three SERVEZONE_CLUSTER_* names in the environment:

import { CoreTraffic } from 'coretraffic';

const coreTraffic = new CoreTraffic({ mode: 'cluster-ingress' });
await coreTraffic.start();

process.on('SIGTERM', async () => {
  await coreTraffic.stop();
});

Repository scripts:

pnpm install
pnpm build
pnpm start
pnpm test
pnpm run build:docker

Container Image

The service image is built from Dockerfile_##version##, so tsdocker publishes it under the release version tag only — code.foss.global/serve.zone/coretraffic:<version> for each release (for example coretraffic:33.0.0). From 32.0.0 on CoreTraffic publishes no latest tag; the existing coretraffic:latest stays frozen at the 2.1.0 build for consumers that still pull it by that name, so pin CoreTraffic by version.

The image carries qenv.yml beside cli.js, which cluster-ingress mode reads its required names from.

The image carries org.opencontainers.image.version and version labels. Coreflow's base-service auto-update only recreates an image that has a comparable version label, and the multi-platform build passes no label of its own, so both labels are raised in the Dockerfile with every release.

Standalone Mode

Standalone mode starts CoreTraffic with a local admin API used by orchestrators such as Onebox:

CORETRAFFIC_MODE=standalone node cli.js

Environment variables - this table is the complete set of CORETRAFFIC_* variables CoreTraffic reads, and test/test.environment.node.ts fails when the sources and this table disagree (cluster-ingress mode reads only the three SERVEZONE_CLUSTER_* names above):

Variable Default Reader Meaning
CORETRAFFIC_MODE cluster-ingress getCoretrafficMode (ts/coretraffic.classes.coretraffic.ts) cluster-ingress or standalone; any other value refuses the start by name.
CORETRAFFIC_CONFIG /etc/coretraffic/config.json getConfigPath (ts/coretraffic.classes.standaloneservice.ts) Standalone config path; a path with no file there starts with empty routes.
CORETRAFFIC_ADMIN_HOST 127.0.0.1 getAdminHost (ts/coretraffic.classes.standaloneservice.ts) Standalone admin bind host.
CORETRAFFIC_ADMIN_PORT 3000 getEnvNumber (ts/coretraffic.classes.standaloneservice.ts) Standalone admin bind port; a value that is not a TCP port number refuses the start by name.
CORETRAFFIC_ADMIN_TOKEN none getAdminToken, enforced by validateAdminExposure (ts/coretraffic.classes.standaloneservice.ts) Bearer token for the admin API. Mandatory once CORETRAFFIC_ADMIN_HOST is not loopback, and optional on a loopback bind, where it still protects every admin endpoint except the unauthenticated health probes (GET /health and GET /ready).

Every CORETRAFFIC_* variable has a default, so standalone mode needs no environment at all to start and never reads qenv.yml. A missing value never refuses a standalone start; a value a reader cannot use does, by name - an unknown CORETRAFFIC_MODE, an admin port that is not a TCP port number, or a non-loopback CORETRAFFIC_ADMIN_HOST without CORETRAFFIC_ADMIN_TOKEN.

Admin API:

  • GET /health: liveness; answers 200 while the admin API serves.
  • GET /ready: readiness; answers 200 while the service is started and SmartProxy runs, and otherwise 503 with { ok: false, error, lifecycleState, smartProxyRunning }, for example "error": "SmartProxy is not running" after a reload whose restart of the previous config failed too.
  • GET /routes: current raw routes and active routes.
  • PUT /routes or POST /routes: replace routes with either an array or { "routes": [...] }. When SmartProxy rejects the set, the call fails as described below and the previous routes keep running and stay in GET /routes.
  • POST /reload: reload config from CORETRAFFIC_CONFIG and restart the proxy engine. CoreTraffic builds the next engine first, and SmartProxy validates its routes while doing so: a config refused there fails the call as described below while the running engine and its connections carry on untouched. A config SmartProxy refuses only when the next engine starts, such as a refusal of its Rust engine, comes after the running engine has stopped to free its ports; CoreTraffic then starts the previous config again and the call fails as described below.
  • POST /security-policy: update global SmartProxy security policy. It waits for a route update or reload in progress and then applies to the engine that runs afterwards.
  • GET /statistics: SmartProxy runtime statistics.
  • GET /listening-ports: currently listening proxy ports.

Admin API errors answer JSON with ok: false and the reason in error:

  • 401: a protected endpoint was called without the right admin token.
  • 404: no such endpoint.
  • 422: SmartProxy refused the route set, in its TypeScript validation or in its Rust engine (SmartProxy's RouteValidationError). error carries SmartProxy's bounded summary and errors lists every refused route as { index, route, messages }: index is the route's position in the refused set's active routes, the list CoreTraffic hands SmartProxy: with httpToHttpsRedirect enabled the generated http-to-https-redirect route comes first, so a configured route's index is one above its position in routes. route is its name (else its id, else route[<index>]) and messages lists every reason.
  • 500: any other failure, for example a malformed request body, a service that is not started, or a reload whose previous config could not be restarted either.
{
  "ok": false,
  "error": "Route validation failed: 1 route(s) have errors: route 'http-broken-example-com': response.body requires an explicit content-type header",
  "errors": [
    {
      "index": 1,
      "route": "http-broken-example-com",
      "messages": ["response.body requires an explicit content-type header"]
    }
  ]
}

The config is regular ISmartProxyOptions JSON with one standalone extension: httpToHttpsRedirect.

{
  "httpToHttpsRedirect": {
    "enabled": true,
    "httpPort": 80,
    "httpsPort": 443,
    "statusCode": 301
  },
  "routes": [
    {
      "name": "app-example-com",
      "match": {
        "ports": 443,
        "domains": "app.example.com",
        "protocol": "http"
      },
      "action": {
        "type": "forward",
        "targets": [{ "host": "app", "port": 3000 }],
        "tls": {
          "mode": "terminate",
          "certificate": {
            "key": "-----BEGIN PRIVATE KEY-----\\n...",
            "cert": "-----BEGIN CERTIFICATE-----\\n..."
          }
        }
      }
    }
  ]
}

Routes pass through to SmartProxy unchanged, from the config file and from PUT /routes alike, so every SmartProxy 28 route action works here. That includes "type": "respond", which answers with a fixed action.response (status, headers, body) and no upstream, for example a 503 with Retry-After for a stopped service. SmartProxy validates the response and refuses the whole route update when it is invalid.

Check commands:

CORETRAFFIC_MODE=standalone node cli.js --check
node cli.js --check   # cluster-ingress: judges the SERVEZONE_CLUSTER_* names, prints origin and node, never the bearer
node cli.js --check-admin-security

Important Files

Path Purpose
ts/index.ts CLI startup wrapper exporting CoreTraffic, runCli, and stop.
ts/coretraffic.classes.coretraffic.ts Main lifecycle, mode selection and SmartProxy instance.
ts/coretraffic.clusteringress.environment.ts Cluster-ingress environment (qenv), listeners and their validation.
ts/coretraffic.classes.clusterrelaysession.ts TypedSocket session with the relay: registration, refusal backoff, hint, table pulls, statistics refusal.
ts/coretraffic.classes.clusteringressservice.ts Cluster-ingress lifecycle, table application and the status listener.
ts/coretraffic.clusteringress.routes.ts Route table to SmartProxy routes.
ts/coretraffic.classes.standaloneservice.ts Standalone CoreTraffic config loader and admin API.
qenv.yml The names cluster-ingress mode requires.
Dockerfile_##version## Service image build; the file name is the published image tag.

Operational Notes

  • Standalone mode starts with empty routes when CORETRAFFIC_CONFIG does not exist.
  • CoreTraffic does not issue certificates; it uses the key/certificate material in the table Cloudly computed.
  • CoreTraffic replaces the full managed route set on every applied table.
  • Cloudly 33.0.0 and Coreflow 33.0.0 answer a refused registration with an unnamed rejection rather than the named refusal the contract states, so a cluster ingress registering against them waits out the doubling 1 s to 300 s backoff instead of a stated retryAfterMs.

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
serve.zone ingress service for TLS termination, reverse proxying, redirects, authentication, and route application.
Readme
1.8 MiB
Languages
TypeScript 98.7%
Shell 1%
JavaScript 0.3%