💋 Sync 2026-10-11 00:16:14
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot] committed 2026-10-11 00:16:14 +08:00
1 parent c7bafc635d
commit c71da84035
264 files changed
+8169 -3944

No files matched your search

+4 -6
View File
@@ -16,7 +16,7 @@ IPKG_INSTROOT="${IPKG_INSTROOT:-}"
export IPKG_INSTROOT
readonly PKG_NAME='safeshield'
readonly PKG_VERSION='0.3.24-r1'
readonly PKG_VERSION='0.3.25-r1'
if type extra_command >/dev/null 2>&1; then
extra_command 'apply_local_rules' 'Apply local allow/block rules using the cached Hub artifact'
@@ -232,7 +232,7 @@ service_triggers() {
status_service() {
local status stage valid_line_count
local enabled refresh_interval_s refresh_on_boot require_wan boot_delay
local license_key artifact_tier artifact_version license_plan device_profile
local artifact_tier artifact_version entitlement_plan device_profile
local artifact_source_count artifact_block_source_count artifact_allow_source_count
local dnsmasq_version dnsmasq_min_version
local statistics_enabled statistics_snapshot_interval_s statistics_retention_hours
@@ -246,7 +246,6 @@ status_service() {
refresh_on_boot="$(ss_config_get config refresh_on_boot 1)"
require_wan="$(ss_config_get config require_wan 1)"
boot_delay="$(ss_config_get config boot_start_delay_s 30)"
license_key="$(ss_config_get config license_key '')"
statistics_enabled="$(ss_config_get config statistics_enabled 1)"
statistics_snapshot_interval_s="$(ss_config_get config statistics_snapshot_interval_s 60)"
statistics_retention_hours="$(ss_config_get config statistics_retention_hours 168)"
@@ -264,7 +263,7 @@ status_service() {
artifact_source_count="$(json get artifact_source_count 2>/dev/null)"
artifact_block_source_count="$(json get artifact_block_source_count 2>/dev/null)"
artifact_allow_source_count="$(json get artifact_allow_source_count 2>/dev/null)"
license_plan="$(json get license_plan 2>/dev/null)"
entitlement_plan="$(json get entitlement_plan 2>/dev/null)"
device_profile="$(json get device_profile 2>/dev/null)"
dnsmasq_version="$(json get dnsmasq_version 2>/dev/null)"
dnsmasq_min_version="$(json get dnsmasq_min_version 2>/dev/null)"
@@ -278,7 +277,6 @@ status_service() {
printf 'refresh_on_boot: %s\n' "$refresh_on_boot"
printf 'require_wan: %s\n' "$require_wan"
printf 'boot_start_delay_s: %s\n' "$boot_delay"
printf 'license_key: %s\n' "$(ss_mask_secret "$license_key")"
if [ -f "${SS_BLOCKLIST_FILE}" ]; then
printf '%s\n' 'blocklist: installed'
@@ -296,7 +294,7 @@ status_service() {
printf 'status: %s\n' "$status"
[ -n "$stage" ] && printf 'stage: %s\n' "$stage"
[ -n "$license_plan" ] && printf 'license_plan: %s\n' "$license_plan"
[ -n "$entitlement_plan" ] && printf 'entitlement_plan: %s\n' "$entitlement_plan"
[ -n "$device_profile" ] && printf 'device_profile: %s\n' "$device_profile"
[ -n "$artifact_tier" ] && printf 'artifact_tier: %s\n' "$artifact_tier"
[ -n "$artifact_version" ] && printf 'artifact_version: %s\n' "$artifact_version"
@@ -0,0 +1,11 @@
#!/bin/sh
set -e
# SafeShield authentication is device-scoped. Remove the retired raw license
# key from preserved configurations so obsolete secrets do not remain on flash.
if uci -q get safeshield.config.license_key >/dev/null 2>&1; then
uci -q delete safeshield.config.license_key
uci -q commit safeshield
fi
exit 0
@@ -177,6 +177,17 @@ ss_read_installed_version() {
printf '%s' "$version"
}
ss_device_credential_path() {
printf '%s' "${SS_DEVICE_CREDENTIAL_FILE:-/etc/smartsafehub/device-credential.json}"
}
ss_device_credential_token() {
local path
path="$(ss_device_credential_path)"
[ -r "$path" ] || return 1
ss_json_get_file "$path" '@.token'
}
ss_write_resolve_payload() {
local out="$1"
local model vendor arch memory physical_fingerprint safeshield_version
@@ -202,7 +213,6 @@ ss_write_resolve_payload() {
cat >"$out" <<__SAFESHIELD_JSON__
{
"license_key": $(ss_json_value "$ss_license_key"),
"device": {
"physical_fingerprint": $(ss_json_value "$physical_fingerprint"),
"fingerprint_version": ${SS_FINGERPRINT_VERSION:-1},
@@ -494,22 +504,25 @@ ss_resolve_artifact_sources() {
}
ss_resolve_artifact() {
local url response payload retries ok statistics_entitlement_rc
local url response payload retries ok statistics_entitlement_rc device_credential
SS_RESOLVE_ERROR_CODE='api_resolve_failed'
url='https://www.smartsafehub.com/api/v1/licenses/resolve'
url='https://www.smartsafehub.com/api/v1/devices/sync'
payload="$SS_API_PAYLOAD"
response="$SS_API_RESPONSE"
ss_status_set artifact_download_url_present "0"
if [ -z "$ss_license_key" ]; then
log_info "license_key is empty; resolving SafeShield artifact as unlicensed/free device"
ss_status_set license_plan "free"
ss_status_set license_status "unlicensed"
fi
ss_write_resolve_payload "$payload" || return 1
device_credential="$(ss_device_credential_token 2>/dev/null || true)"
if [ -z "$device_credential" ]; then
log_warn "SmartSafeHub device credential is not available yet; retrying on the next refresh"
SS_RESOLVE_ERROR_CODE='device_registration_pending'
ss_status_set device_registration_status 'pending'
ss_status_set health_api_resolve "0"
return 1
fi
ss_status_set device_registration_status 'registered'
retries=1
ok=0
@@ -517,7 +530,7 @@ ss_resolve_artifact() {
ss_should_stop && return 130
log_info "Resolving SafeShield artifact via Hub API (try ${retries}/${ss_download_retry})"
if ss_http_post_json "$url" "$payload" "$response" && [ -s "$response" ]; then
if ss_http_post_json "$url" "$payload" "$response" "Device ${device_credential}" && [ -s "$response" ]; then
ok=1
break
fi
@@ -546,8 +559,8 @@ ss_resolve_artifact() {
ss_resolved_artifact_version="$(ss_json_get_file "$response" '@.artifact.version')"
ss_resolved_artifact_unique_domains="$(ss_json_get_file "$response" '@.artifact.unique_domains')"
ss_resolved_artifact_rules="$(ss_json_get_file "$response" '@.artifact.rules')"
ss_resolved_license_plan="$(ss_json_get_file "$response" '@.license.plan')"
ss_resolved_license_status="$(ss_json_get_file "$response" '@.license.status')"
ss_resolved_entitlement_plan="$(ss_json_get_file "$response" '@.entitlement.plan')"
ss_resolved_entitlement_status="$(ss_json_get_file "$response" '@.entitlement.status')"
ss_resolved_device_profile="$(ss_json_get_file "$response" '@.device.profile')"
if command -v ss_statistics_sync_upload_entitlement >/dev/null 2>&1; then
@@ -555,7 +568,7 @@ ss_resolve_artifact() {
ss_statistics_sync_upload_entitlement "$response" || statistics_entitlement_rc=$?
case "$statistics_entitlement_rc" in
0) ;;
2) log_info "Cloud statistics upload is not enabled for the current license" ;;
2) log_info "Cloud statistics upload is not enabled for the current plan" ;;
*) log_warn "Hub API response did not include usable statistics upload credentials" ;;
esac
fi
@@ -572,12 +585,12 @@ ss_resolve_artifact() {
ss_status_set artifact_version "$ss_resolved_artifact_version"
ss_status_set artifact_unique_domains "${ss_resolved_artifact_unique_domains:-0}"
ss_status_set artifact_rules "${ss_resolved_artifact_rules:-0}"
ss_status_set license_plan "${ss_resolved_license_plan:-free}"
ss_status_set license_status "${ss_resolved_license_status:-unlicensed}"
ss_status_set entitlement_plan "${ss_resolved_entitlement_plan:-free}"
ss_status_set entitlement_status "${ss_resolved_entitlement_status:-unlicensed}"
ss_status_set device_profile "$ss_resolved_device_profile"
SS_RESOLVE_ERROR_CODE=''
log_ok "Resolved ${ss_resolved_artifact_tier:-unknown}/${ss_resolved_artifact_version:-unknown} artifact sources for plan ${ss_resolved_license_plan:-free}"
log_ok "Resolved ${ss_resolved_artifact_tier:-unknown}/${ss_resolved_artifact_version:-unknown} artifact sources for plan ${ss_resolved_entitlement_plan:-free}"
}
ss_verify_artifact_sha256() {
@@ -5,7 +5,6 @@
ss_enabled="0"
ss_verbosity="2"
ss_license_key=""
ss_device_vendor=""
ss_device_model=""
ss_device_arch=""
@@ -105,7 +104,6 @@ ss_load_config() {
ss_enabled="$(ss_config_get config enabled 0)"
ss_verbosity="$(ss_config_get config verbosity 2)"
ss_license_key="$(ss_config_get config license_key '')"
ss_device_vendor="$(ss_config_get config device_vendor '')"
ss_device_model="$(ss_config_get config device_model '')"
ss_device_arch="$(ss_config_get config device_arch '')"
@@ -358,20 +358,22 @@ ss_statistics_load_upload_credentials() {
}
ss_statistics_refresh_upload_credentials() {
local request response resolve_url retries rc=1
local request response sync_url retries rc=1 device_credential
resolve_url='https://www.smartsafehub.com/api/v1/licenses/resolve'
request="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-resolve-request.json"
response="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-resolve-response.json"
sync_url='https://www.smartsafehub.com/api/v1/devices/sync'
request="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-sync-request.json"
response="${SS_STATISTICS_DIR:-/tmp/safeshield/statistics}/upload-sync-response.json"
ss_load_config || return 1
device_credential="$(ss_device_credential_token 2>/dev/null || true)"
[ -n "$device_credential" ] || return 1
mkdir -p "${request%/*}" || return 1
ss_refresh_lock_open_wait "${ss_download_timeout:-10}" || return $?
if ! ss_write_resolve_payload "$request"; then
printf '{}\n' >"$request" || {
ss_refresh_lock_close
return 1
fi
}
retries=1
while [ "$retries" -le "${ss_download_retry:-3}" ]; do
@@ -379,7 +381,7 @@ ss_statistics_refresh_upload_credentials() {
rc=130
break
}
if ss_http_post_json "$resolve_url" "$request" "$response" && [ -s "$response" ]; then
if ss_http_post_json "$sync_url" "$request" "$response" "Device ${device_credential}" && [ -s "$response" ]; then
rc=0
ss_statistics_sync_upload_entitlement "$response" || rc=$?
break
@@ -70,8 +70,8 @@ ss_status_reset_blocklist_fields() {
}
ss_status_reset_artifact_fields() {
ss_status_set license_plan ""
ss_status_set license_status ""
ss_status_set entitlement_plan ""
ss_status_set entitlement_status ""
ss_status_set physical_fingerprint ""
ss_status_set fingerprint_version ""
ss_status_set identity_provider ""
@@ -8,23 +8,6 @@ command_exists() {
command -v "$1" >/dev/null 2>&1
}
ss_mask_secret() {
local value="$1"
local len
[ -n "$value" ] || {
printf '%s' ''
return 0
}
len="${#value}"
if [ "$len" -le 8 ] 2>/dev/null; then
printf '%s' '********'
else
printf '%s****%s' "${value%${value#????}}" "${value#${value%????}}"
fi
}
is_valid_integer() {
case "$1" in
'' | *[!0-9]*) return 1 ;;
@@ -375,7 +375,7 @@ upload_send_pending() {
continue
fi
if [ "$refresh_rc" -eq 2 ]; then
log_info 'Cloud statistics upload is no longer enabled for the current license'
log_info 'Cloud statistics upload is no longer enabled for the current account entitlement'
upload_pending_clear
return 2
fi
@@ -433,14 +433,11 @@ main() {
fi
wait_s="$SS_STATS_UPLOAD_INTERVAL_S"
if [ -z "${ss_license_key:-}" ]; then
ss_statistics_upload_denied || ss_statistics_disable_cloud_upload >/dev/null 2>&1 || true
fi
if ss_statistics_upload_denied; then
upload_pending_clear
if [ -n "${ss_license_key:-}" ] && ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
if ss_statistics_upload_denied_recheck_due "$SS_STATS_UPLOAD_DENIED_RECHECK_S"; then
log_info 'Rechecking Hub statistics upload entitlement'
# Stamp this attempt before network I/O so a failed resolve cannot
# Stamp this attempt before network I/O so a failed device sync cannot
# turn into a routine upload retry loop while entitlement is denied.
ss_statistics_set_upload_entitlement denied >/dev/null 2>&1 || true
entitlement_rc=0
@@ -18,9 +18,7 @@
"set_enabled",
"refresh",
"rule_add",
"rule_delete",
"license_get",
"license_update"
"rule_delete"
]
}
}
@@ -8,7 +8,6 @@ let status = require('status');
let config = require('config');
let refresh = require('refresh');
let rules = require('rules');
let license = require('license');
let statistics = require('statistics');
return {
@@ -66,17 +65,7 @@ return {
refresh: true
},
call: rules.delete
},
license_get: {
call: license.get
},
license_update: {
args: {
license_key: ''
},
call: license.update
}
}
};
@@ -12,7 +12,6 @@ let reload_uci = core.reload_uci;
let cfg = core.cfg;
let to_bool = core.to_bool;
let to_int = core.to_int;
let mask_secret = core.mask_secret;
let api_error = core.api_error;
let uci_commit_option = core.uci_commit_option;
let bool_uci = core.bool_uci;
@@ -46,10 +45,6 @@ function build_config() {
version: CONFIG_SCHEMA_VERSION
},
values: values,
license: {
configured: !!cfg('license_key', ''),
key_masked: mask_secret(cfg('license_key', ''))
},
device: {
vendor: cfg('device_vendor', ''),
model: cfg('device_model', ''),
@@ -67,9 +62,6 @@ function validate_config_value(name, value) {
return api_error('dedicated_method_required', 'Use set_enabled to change enabled state', name);
}
if (name == 'license_key') {
return api_error('dedicated_method_required', 'Use license_update to change the license key', name);
}
return api_error('unknown_option', 'Unsupported SafeShield configuration option', name);
}
@@ -149,20 +149,6 @@ function identity_cfg(name, def) {
return (v == null) ? def : v;
}
function mask_secret(v) {
let s = sprintf('%s', v || '');
let n = length(s);
if (n == 0) {
return '';
}
if (n <= 8) {
return '********';
}
return sprintf('%s...%s', substr(s, 0, 4), substr(s, n - 4));
}
function api_error(code, message, field) {
let error = {
@@ -242,7 +228,6 @@ return {
reload_uci: reload_uci,
cfg: cfg,
identity_cfg: identity_cfg,
mask_secret: mask_secret,
api_error: api_error,
uci_commit_option: uci_commit_option,
uci_delete_option: uci_delete_option,
@@ -1,90 +0,0 @@
'use strict';
let core = require('core');
let runtime = require('runtime');
let reload_uci = core.reload_uci;
let cfg = core.cfg;
let trim = core.trim;
let mask_secret = core.mask_secret;
let api_error = core.api_error;
let uci_commit_option = core.uci_commit_option;
let uci_delete_option = core.uci_delete_option;
let start_refresh_async = runtime.start_refresh_async;
let reset_statistics_upload_state = runtime.reset_statistics_upload_state;
function license_get_call() {
reload_uci();
let key = cfg('license_key', '');
return {
ok: true,
license: {
configured: !!key,
key: key
}
};
}
function license_update_call(request) {
reload_uci();
if (request.args.license_key == null) {
return api_error('missing_argument', 'license_key is required; use an empty string to clear it', 'license_key');
}
let key = trim(request.args.license_key);
if (length(key) > 512 || match(key, /[\r\n]/)) {
return api_error('invalid_license_key', 'license_key must be at most 512 characters and contain no line breaks', 'license_key');
}
let current = cfg('license_key', '');
if (current == key) {
return {
ok: true,
changed: false,
license: {
configured: !!key,
key_masked: mask_secret(key)
},
refresh: {
requested: false,
accepted: false,
reason: 'unchanged'
}
};
}
let saved = key == ''
? uci_delete_option('license_key')
: uci_commit_option('license_key', key);
if (!saved) {
return api_error('uci_commit_failed', 'Failed to update the SafeShield license key');
}
reset_statistics_upload_state();
let refresh = start_refresh_async();
return {
ok: true,
changed: true,
license: {
configured: !!key,
key_masked: mask_secret(key)
},
refresh: {
requested: true,
accepted: refresh.accepted,
reason: refresh.reason || ''
}
};
}
return {
get: license_get_call,
update: license_update_call
};
@@ -2,7 +2,6 @@
let core = require('core');
let fs = require('fs');
let ubus = require('ubus').connect();
let PKG_NAME = core.PKG_NAME;
let STATUS_FILE = core.STATUS_FILE;
@@ -12,43 +11,72 @@ let to_bool = core.to_bool;
let reload_uci = core.reload_uci;
let cfg = core.cfg;
function service_running(name) {
let r = ubus.call('service', 'list', { name: name });
if (!r || !r[name] || !r[name].instances) {
return false;
// Do not call ubus synchronously from an rpcd handler. In particular,
// service.list may require the same event loop currently serving the caller.
// Use an independent ubus client with its built-in -t timeout (seconds).
// BusyBox/OpenWrt installations do not necessarily provide coreutils timeout.
// Only successful procd lookups are briefly reused. A failed lookup must not
// poison subsequent requests or be mistaken for a stopped service.
let service_cache = {};
function service_status(name, use_cache) {
if (name != PKG_NAME && name != 'dnsmasq') {
return { state: 'unknown', running: false, lookup_ok: false };
}
for (let inst_name, inst in r[name].instances) {
if (inst.running) {
return true;
let now = time();
let cached = service_cache[name];
if (use_cache && cached && cached.at == now) {
return cached.value;
}
let pipe = fs.popen(sprintf("ubus -t 2 call service list '{\"name\":\"%s\"}' 2>/dev/null", name), 'r');
if (!pipe) {
return { state: 'unknown', running: false, lookup_ok: false };
}
let output = pipe.read('all');
pipe.close();
if (!output) {
return { state: 'unknown', running: false, lookup_ok: false };
}
let result = json(output);
// procd returns an empty object when the named service is absent.
// That is a valid stopped response, not a transport failure.
if (type(result) != 'object') {
return { state: 'unknown', running: false, lookup_ok: false };
}
let instances = result[name] && result[name].instances;
let running = false;
if (type(instances) == 'object') {
for (let instance_name, instance in instances) {
if (instance && instance.running) {
running = true;
break;
}
}
}
return false;
let value = { state: running ? 'running' : 'stopped', running: running, lookup_ok: true, instances: instances || {} };
if (use_cache) {
service_cache[name] = { at: now, value: value };
}
return value;
}
function service_running(name) {
return service_status(name, false).running;
}
function service_instance_running(name, instance_name) {
let r = ubus.call('service', 'list', { name: name });
if (!r || !r[name] || !r[name].instances || !r[name].instances[instance_name]) {
return false;
}
return !!r[name].instances[instance_name].running;
let state = service_status(name, false);
return !!(state.instances && state.instances[instance_name] && state.instances[instance_name].running);
}
function dnsmasq_running() {
let r = ubus.call('service', 'list', { name: 'dnsmasq' });
if (!r || !r.dnsmasq || !r.dnsmasq.instances) {
return false;
}
for (let inst_name, inst in r.dnsmasq.instances) {
if (inst.running) {
return true;
}
}
return false;
return service_running('dnsmasq');
}
function run_service_action(action, timeout_ms) {
@@ -85,10 +113,11 @@ function start_refresh_async() {
};
}
if (!service_running(PKG_NAME)) {
let service = service_status(PKG_NAME, false);
if (!service.running) {
return {
accepted: false,
reason: 'service_stopped'
reason: service.lookup_ok ? 'service_stopped' : 'service_unavailable'
};
}
@@ -129,10 +158,11 @@ function start_local_apply_async() {
};
}
if (!service_running(PKG_NAME)) {
let service = service_status(PKG_NAME, false);
if (!service.running) {
return {
accepted: false,
reason: 'service_stopped'
reason: service.lookup_ok ? 'service_stopped' : 'service_unavailable'
};
}
@@ -155,6 +185,7 @@ function start_local_apply_async() {
return {
service_running: service_running,
service_status: service_status,
service_instance_running: service_instance_running,
dnsmasq_running: dnsmasq_running,
run_service_action: run_service_action,
@@ -91,7 +91,8 @@ function build_statistics() {
let snapshot_interval_s = to_int(cfg('statistics_snapshot_interval_s', '60'), 60);
let data = read_json_file(STATISTICS_FILE, {});
let totals = (type(data.totals) == 'object') ? data.totals : {};
let collector_running = service_instance_running(PKG_NAME, 'statistics');
let collector_service = runtime.service_status(PKG_NAME, true);
let collector_running = !!(collector_service.instances && collector_service.instances.statistics && collector_service.instances.statistics.running);
let effective_enabled = enabled && collector_running;
let source = sanitize_source(data.source);
if (!effective_enabled) {
@@ -106,6 +107,8 @@ function build_statistics() {
effective_enabled: effective_enabled,
available: !!data.schema,
collector_running: collector_running,
collector_state: collector_service.lookup_ok ? (collector_running ? 'running' : 'stopped') : 'unknown',
collector_lookup_ok: collector_service.lookup_ok,
volatile: to_bool(data.volatile, true),
storage: sprintf('%s', data.storage || 'tmpfs'),
persistent: to_bool(data.persistent, false),
@@ -20,7 +20,6 @@ let file_size_kb = core.file_size_kb;
let to_bool = core.to_bool;
let to_optional_bool = core.to_optional_bool;
let to_int = core.to_int;
let mask_secret = core.mask_secret;
let service_running = runtime.service_running;
let dnsmasq_running = runtime.dnsmasq_running;
@@ -134,8 +133,6 @@ function build_status() {
let refresh_interval_s = to_int(cfg('refresh_interval_s', '28800'), 28800);
let boot_start_delay_s = to_int(cfg('boot_start_delay_s', '30'), 30);
let license_key = cfg('license_key', '');
let license_configured = !!license_key;
let apply_local_overrides = to_bool(cfg('apply_local_overrides', '1'), true);
let cfg_physical_fingerprint = identity_cfg('physical_fingerprint', '');
@@ -186,8 +183,8 @@ function build_status() {
let artifact_block_source_count = to_int(data.artifact_block_source_count || (artifact_download_url_present ? 1 : 0), 0);
let artifact_allow_source_count = to_int(data.artifact_allow_source_count || 0, 0);
let license_plan = data.license_plan || '';
let license_status = data.license_status || '';
let entitlement_plan = data.entitlement_plan || '';
let entitlement_status = data.entitlement_status || '';
let physical_fingerprint = data.physical_fingerprint || cfg_physical_fingerprint || '';
let fingerprint_version = to_int(data.fingerprint_version || cfg_fingerprint_version || 1, 1);
let identity_provider = data.identity_provider || cfg_identity_provider || '';
@@ -217,8 +214,10 @@ function build_status() {
let hub_source = build_api_source(data, enabled);
let installed = blocklist_installed || file_exists(BLOCKLIST_FILE);
let refreshd_running = service_running(PKG_NAME);
let dns_running = dnsmasq_running();
let refresh_service = runtime.service_status(PKG_NAME, true);
let dns_service = runtime.service_status('dnsmasq', true);
let refreshd_running = refresh_service.running;
let dns_running = dns_service.running;
let generated_at = time();
let next_refresh_in_s = 0;
@@ -245,7 +244,11 @@ function build_status() {
runtime: {
refreshd_running: refreshd_running,
refreshd_state: refresh_service.state,
refreshd_lookup_ok: refresh_service.lookup_ok,
dnsmasq_running: dns_running,
dnsmasq_state: dns_service.state,
dnsmasq_lookup_ok: dns_service.lookup_ok,
dnsmasq_version: dnsmasq_version,
dnsmasq_min_version: dnsmasq_min_version,
dns_runtime_ok: health_dns_runtime || dns_running,
@@ -255,11 +258,9 @@ function build_status() {
last_result: last_result,
last_error_code: last_error_code
},
license: {
configured: license_configured,
key_masked: mask_secret(license_key),
plan: license_plan,
status: license_status
entitlement: {
plan: entitlement_plan,
status: entitlement_status
},
device: {