CallMeTechie
DE Login
Home Products Blog About Contact

Licensing

v1.x · Updated 1 month ago

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):

  1. Load key — env variable takes precedence, otherwise from the DB (encrypted in settings.license_signing_key_encrypted).
  2. No keysetCommunityMode(), unlicensed=true, enforcement, return.
  3. No signing keysetCommunityMode(), log warning.
  4. Try cached token — verify JWT from /data/.license-token with HS256. The fingerprint must match. On success, apply, start background refresh.
  5. Validate online — POST to config.license.server with {license_key, hardware_fingerprint, device_name, product_slug}. Store the token.
  6. Fallback to expired token — if the license server is unreachable and an ignoreExpiration token exists.
  7. Last fallbacksetCommunityMode() 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:

  1. /sys/class/dmi/id/product_uuid (BIOS/mainboard)
  2. os.cpus()[].model (CPU model)
  3. /proc/meminfo (MemTotal)
  4. Fallback: /etc/machine-id, then os.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:

  1. For every limit feature (vpn_peers, http_routes, l4_routes), the actual count is computed.
  2. If count > limit, the count - limit oldest entries (ORDER BY created_at ASC) are set to enabled=0.
  3. WireGuard config and Caddy config are synchronized — deactivated peers/routes disappear from the wire.
  4. Activity log entry peer_license_disabled / route_license_disabled.
  5. 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):

  1. Flag in COMMUNITY_FALLBACK — usually false, as a limit usually 0.
  2. API guardrouter.use(requireFeature('X')) or per-route.
  3. UI guard — template wrap with {% if license.features.X %} and lock fallback.
  4. 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

Source files

  • src/services/license.js — Core service, COMMUNITY_FALLBACK, validateLicense, enforceLimitsInternal.
  • src/middleware/license.jsinjectLicense, requireFeature, requireLimit, requireFeatureField.
  • src/services/settings.js — DB-backed key storage for UI activation.
  • config/default.jslicense.server, license.tokenPath, license.key, license.signingKey.

Cookie Settings

We use cookies to improve your experience. Essential cookies are always active.

Privacy Policy
ESC
↑↓ navigate open esc close