Licensing
Overview
GateControl is open-source-capable: without a license key, the application runs in a stripped-down community mode. Commercial functions are unlocked through a central feature-flag system. The system knows three states — unlicensed, community with key, and pro/lifetime — and falls back cleanly to the last safe state on every failure.
The entire feature gate runs through a single query API: license.hasFeature(key). This one function is consulted by middleware, the template engine, and services alike. Limits (peer count, route count) also come from the same feature object, but are queried via a second variant (getFeatureLimit / requireLimit).
Licensing is deliberately built defensively: the server must not block when the license server is unreachable. Data must never be destroyed when a downgrade is detected — existing peers/routes are only deactivated, not deleted. A new license key reactivates them.
Architecture
┌──────────────────┐
│ config/license │ GC_LICENSE_KEY, GC_LICENSE_SIGNING_KEY,
│ env oder DB │ tokenPath (JWT-Cache)
└────────┬─────────┘
│
▼
┌──────────────────┐ ┌────────────────────┐
│ license.js │◀──────▶│ callmetechie.de │
│ cachedFeatures │ HTTPS │ Lizenzserver │
│ cachedPlan │ └────────────────────┘
└─┬──────┬────────┬┘
│ │ │
hasFeature getFeatureLimit isWithinLimit
│ │ │
┌──────┴──┐ │ ┌────┴──────┐
│Middleware│ │ │Templates │
│requireX │ │ │license.features.*│
└─────────┘ │ └──────────┘
▼
┌──────────────────┐
│ Services │
│ (opportunistisch) │
└──────────────────┘
Components
| Component | Path | Role |
|---|---|---|
| License service | src/services/license.js |
Validation, caching, refresh, enforcement |
| Middleware | src/middleware/license.js |
requireFeature, requireLimit, injectLicense |
| JWT cache | /data/.license-token (default) |
Offline validation via HMAC signature |
| Hardware fingerprint | internal | SHA-256 over DMI-UUID + CPU + RAM |
License modes
| Mode | Condition | Features |
|---|---|---|
| unlicensed | No license key | COMMUNITY_FALLBACK (hardcoded) |
| community | License key with plan community |
Server response |
| pro / lifetime | License key with corresponding plan | Server response |
The unlicensed flag is separate from the plan. A user with a registered community key and plan='community' is not unlicensed — they get a current feature set from the license server instead of the frozen fallback. This distinction makes it possible to extend community features on the server side without a new client release.
Feature catalog
The COMMUNITY_FALLBACK (src/services/license.js line 15) is the source of truth for unlicensed mode. Numeric values are limits (-1 = unlimited), booleans are feature toggles.
Limits
| Key | Community | Meaning |
|---|---|---|
vpn_peers |
3 | Active WG peers |
http_routes |
1 | Active HTTP routes |
l4_routes |
0 | Active L4 routes |
gateway_peers |
1 | Home Gateway peers |
gateway_http_targets |
3 | HTTP routes per gateway |
Routing features
route_auth, ip_access_control, peer_acl, rate_limiting, compression, custom_headers, load_balancing, retry_on_error, circuit_breaker, request_mirroring, bot_blocking, request_debugging, acme_custom_ca, sticky_sessions.
Monitoring and operations
uptime_monitoring, traffic_history (true), prometheus_metrics, log_export, backup_restore (true), scheduled_backups, email_alerts, webhooks.
Integration
api_tokens, machine_binding, custom_branding, custom_dns, internal_dns.
Remote Desktop
remote_desktop (all RDP endpoints), rdp_via_gateway (access_mode=gateway), split_tunnel_preset.
Gateway-specific
gateway_tcp_routing, gateway_wol.
Validation flow
validateLicense() runs once at startup and then every 7 days (refreshLicenseInBackground):
- Load key — env variable takes precedence, otherwise from the DB (encrypted in
settings.license_signing_key_encrypted). - No key →
setCommunityMode(),unlicensed=true, enforcement, return. - No signing key →
setCommunityMode(), log warning. - Try cached token — verify JWT from
/data/.license-tokenwith HS256. The fingerprint must match. On success, apply, start background refresh. - Validate online — POST to
config.license.serverwith{license_key, hardware_fingerprint, device_name, product_slug}. Store the token. - Fallback to expired token — if the license server is unreachable and an
ignoreExpirationtoken exists. - Last fallback —
setCommunityMode()and keep running.
Steps 5 and 6 are important: a network outage must not cause the server to block. An expired token is better than no feature at all.
Hardware fingerprint
getHardwareFingerprint() builds a SHA-256 over:
/sys/class/dmi/id/product_uuid(BIOS/mainboard)os.cpus()[].model(CPU model)/proc/meminfo(MemTotal)- Fallback:
/etc/machine-id, thenos.hostname()
Important: device_name in the request payload uses the hostname from config.app.baseUrl (new URL(baseUrl).hostname), not os.hostname(). This prevents container instances from being counted as a new device on every restart. See also feedback_license_domain.md.
Enforcement on downgrade
When a license refresh detects a plan change (previousPlan !== cachedPlan), enforceLimitsInternal() runs:
- For every limit feature (
vpn_peers,http_routes,l4_routes), the actual count is computed. - If
count > limit, thecount - limitoldest entries (ORDER BYcreated_atASC) are set toenabled=0. - WireGuard config and Caddy config are synchronized — deactivated peers/routes disappear from the wire.
- Activity log entry
peer_license_disabled/route_license_disabled. - Optional email alert if
email_alerts_enabled=true.
Records are never deleted. A later upgrade does not automatically reactivate them, but the admin sees them in the UI as deactivated and can manually enable them.
Pattern for new features
For every new paid feature, four things must happen (see feedback_new_feature_license.md):
- Flag in COMMUNITY_FALLBACK — usually
false, as a limit usually0. - API guard —
router.use(requireFeature('X'))or per-route. - UI guard — template wrap with
{% if license.features.X %}and lock fallback. - Service fallback — on downgrade, no service must crash, only the feature code path is skipped.
Key rotation and removal
removeLicense() deletes the JWT, stops the refresh timer, sets community mode, removes the key from the DB, and starts enforcement. _overrideForTest(features) patches cachedFeatures directly — only for test setup, not for production.
Testing the licensing system
The service exports _overrideForTest and _getHardwareFingerprint for test harnesses. In test setup, the real refresh timer is not started (no startLicenseRefresh() call), so tests stay deterministic.
See also
- routing.md — HTTP/L4 routes are gated via
route_auth,rate_limiting, etc. - home-gateway.md —
gateway_peers,gateway_tcp_routing,gateway_wol. - rdp-routes.md —
remote_desktop,rdp_via_gateway. - internal-dns.md —
internal_dns. - ../API.md — HTTP response formats for 403 feature errors.
Source files
src/services/license.js— Core service,COMMUNITY_FALLBACK,validateLicense,enforceLimitsInternal.src/middleware/license.js—injectLicense,requireFeature,requireLimit,requireFeatureField.src/services/settings.js— DB-backed key storage for UI activation.config/default.js—license.server,license.tokenPath,license.key,license.signingKey.