Files
wlan-ap/feeds/tip/cloud_discovery/files/usr/bin/cloud_discovery
Marek Kwaczynski 460180f42e cloud_discovery: add blocklist for discovery methods
Introduce a blocklist mechanism to avoid retrying failed discovery
methods within the same discovery cycle. Each time a method fails
validation, it is added to the blacklist. The blacklist is cleared
once the device transitions to ONLINE or after all discovery methods
have been attempted.

This prevents repeated attempts of failing methods and ensures the
discovery process progresses more reliably.

Signed-off-by: Marek Kwaczynski <marek@shasta.cloud>
2025-09-24 12:48:50 +02:00

446 lines
10 KiB
Plaintext
Executable File

#!/usr/bin/ucode
'use strict';
import { ulog_open, ulog, ULOG_SYSLOG, ULOG_STDIO, LOG_DAEMON, LOG_INFO } from 'log';
import * as libubus from 'ubus';
import * as uloop from 'uloop';
import * as libuci from 'uci';
import * as math from 'math';
import * as fs from 'fs';
const DISCOVER = 0;
const VALIDATING = 1;
const ONLINE = 2;
const OFFLINE = 3;
const ORPHAN = 4;
const DISCOVER_DHCP = "DHCP";
const DISCOVER_FLASH = "FLASH";
const DISCOVER_LOOKUP = "OpenLAN";
let ubus = libubus.connect();
let uci = libuci.cursor();
let state = DISCOVER;
let discovery_method = "";
let discovery_block_list = [];
let validate_time;
let offline_time;
let orphan_time;
let interval;
let timeouts = {
'offline': 4 * 60 * 60,
'validate': 120,
'orphan': 2 * 60 * 60,
interval: 10000,
expiry_interval: 60 * 60 * 1000,
expiry_threshold: 1 * 365 * 24 * 60 * 60,
};
ulog_open(ULOG_SYSLOG | ULOG_STDIO, LOG_DAEMON, "cloud_discover");
ulog(LOG_INFO, 'Start\n');
uloop.init();
let cds_server = 'discovery.open-lan.org';
function detect_certificate_type() {
let pipe = fs.popen(`openssl x509 -in /etc/ucentral/cert.pem -noout -issuer`);
let issuer = pipe.read("all");
pipe.close();
if (match(issuer, /OpenLAN Demo Birth CA/)) {
ulog(LOG_INFO, 'Certificate type is "Demo" \n');
cds_server = 'discovery-qa.open-lan.org';
timeouts.expiry_threshold = 3 * 24 * 60 * 60;
} else if (match(issuer, /OpenLAN Birth Issuing CA/)) {
ulog(LOG_INFO, 'Certificate type is "Production"\n');
} else {
ulog(LOG_INFO, 'Certificate type is "TIP"\n');
}
}
function readjsonfile(path) {
let file = fs.readfile(path);
if (file)
file = json(file);
return file;
}
function timeouts_load(){
let data = uci.get_all('ucentral', 'timeouts');
for (let key in [ 'offline', 'validate', 'orphan' ])
if (data && data[key])
timeouts[key] = +data[key];
let time_skew = timeouts.offline / 50 * (math.rand() % 50);
timeouts.offline_skew = timeouts.offline + time_skew;
ulog(LOG_INFO, 'Randomizing offline time from %d->%d \n', timeouts.offline, timeouts.offline_skew);
time_skew = timeouts.orphan / 50 * (math.rand() % 50);
timeouts.orphan_skew = timeouts.orphan + time_skew;
ulog(LOG_INFO, 'Randomizing orphan time from %d->%d \n', timeouts.orphan, timeouts.orphan_skew);
}
function client_start() {
ulog(LOG_INFO, '(re)starting client\n');
system('/etc/init.d/ucentral restart');
}
function dhcp_restart() {
ulog(LOG_INFO, 'restarting dhcp\n');
system('killall -USR1 udhcpc');
}
function ntp_restart() {
ulog(LOG_INFO, 'restarting ntp\n');
system('/etc/init.d/sysntpd restart');
}
function gateway_load() {
return readjsonfile('/etc/ucentral/gateway.json');
}
function discovery_state_write() {
if (length(discovery_method) == 0)
return;
let discovery_state = {
"type": discovery_method,
"updated": time()
};
fs.writefile('/etc/ucentral/discovery.state.json', discovery_state);
}
function gateway_write(data) {
let gateway = gateway_load();
gateway ??= {};
let new = {};
let changed = false;
for (let key in [ 'server', 'port', 'valid', 'hostname_validate' ]) {
if (exists(data, key))
new[key] = data[key];
else if (exists(gateway, key))
new[key] = gateway[key];
if (new[key] != gateway[key])
changed = true;
}
if (changed) {
fs.writefile('/etc/ucentral/gateway.json', new);
system('sync');
}
return changed;
}
function gateway_available() {
let gateway = gateway_load();
if (!gateway || !gateway.server || !gateway.port)
return false;
return true;
}
function set_state(set) {
if (state == set)
return;
let prev = state;
state = set;
switch(state) {
case DISCOVER:
ulog(LOG_INFO, 'Setting cloud to undiscovered\n');
fs.unlink('/tmp/cloud.json');
fs.unlink('/etc/ucentral/gateway.json');
gateway_write({ valid: false });
dhcp_restart();
break;
case VALIDATING:
ulog(LOG_INFO, 'Wait for validation\n');
validate_time = time();
state = VALIDATING;
push(discovery_block_list, discovery_method);
break;
case ONLINE:
ulog(LOG_INFO, 'Connected to cloud\n');
if (prev == VALIDATING) {
ulog(LOG_INFO, 'Setting cloud controller to validated\n');
gateway_write({ valid: true });
discovery_state_write();
discovery_block_list = [];
}
break;
case OFFLINE:
ulog(LOG_INFO, 'Lost connection to cloud\n');
offline_time = time();
break;
case ORPHAN:
ulog(LOG_INFO, 'Device is an orphan\n');
orphan_time = time();
break;
}
}
function discover_dhcp() {
let dhcp = readjsonfile('/tmp/cloud.json');
if (dhcp?.dhcp_server && dhcp?.dhcp_port) {
if (gateway_write({ server: dhcp.dhcp_server, port:dhcp.dhcp_port, valid: false, hostname_validate: dhcp.no_validation ? 0 : 1 })) {
ulog(LOG_INFO, `Discovered cloud via DHCP ${dhcp.dhcp_server}:${dhcp.dhcp_port}\n`);
client_start();
set_state(VALIDATING);
}
return true;
}
return !dhcp?.lease;
}
function redirector_lookup() {
const path = '/tmp/ucentral.redirector';
ulog(LOG_INFO, 'Contact redirector service\n');
let serial = uci.get('system', '@system[-1]', 'mac');
fs.unlink(path);
system(`curl -k --cert /etc/ucentral/operational.pem --key /etc/ucentral/key.pem --cacert /etc/ucentral/operational.ca https://${cds_server}/v1/devices/${serial} --output /tmp/ucentral.redirector`);
if (!fs.stat(path))
return;
let redir = readjsonfile(path);
if (redir?.controller_endpoint) {
let controller_endpoint = split(redir.controller_endpoint, ':');
if (gateway_write({ server: controller_endpoint[0], port: controller_endpoint[1] || 15002, valid: false, hostname_validate: 1 })) {
ulog(LOG_INFO, `Discovered cloud via lookup service ${controller_endpoint[0]}:${controller_endpoint[1] || 15002}\n`);
client_start();
set_state(VALIDATING);
}
} else {
ulog(LOG_INFO, 'Failed to discover cloud endpoint\n');
}
}
function discover_flash() {
if (!fs.stat('/etc/ucentral/gateway.flash'))
return 1;
ulog(LOG_INFO, 'Using pre-populated cloud information\n');
fs.writefile('/etc/ucentral/gateway.json', fs.readfile('/etc/ucentral/gateway.flash'));
client_start();
set_state(VALIDATING);
return 0;
}
function time_is_valid() {
let valid = !!fs.stat('/tmp/ntp.set');
if (!valid)
ntp_restart();
ulog(LOG_INFO, `Time is ${valid ? '': 'not '}valid\n`);
return valid;
}
function is_discover_method_blacked() {
if (discovery_method in discovery_block_list)
return true;
return false;
}
function interval_handler() {
printf(`State ${state}\n`);
switch(state) {
case DISCOVER:
if (timeouts.interval < 60000)
timeouts.interval += 10000;
break;
default:
timeouts.interval = 10000;
break;
}
printf('setting interval to %d\n', timeouts.interval);
interval.set(timeouts.interval);
switch(state) {
case ORPHAN:
if (time() - orphan_time <= timeouts.orphan_skew)
break;
orphan_time = time();
/* fall through */
case DISCOVER:
ulog(LOG_INFO, 'Starting discover\n');
if (!time_is_valid())
return;
if (system('/usr/bin/est_client enroll'))
return;
discovery_method = DISCOVER_DHCP;
if (!is_discover_method_blacked() && discover_dhcp())
return;
discovery_method = DISCOVER_FLASH;
if (!is_discover_method_blacked() && !discover_flash())
return;
discovery_method = DISCOVER_LOOKUP;
redirector_lookup();
discovery_block_list = [];
break;
case VALIDATING:
if (time() - validate_time <= timeouts.validate)
break;
ulog(LOG_INFO, 'validation failed, restarting discovery process\n');
set_state(DISCOVER);
break;
case OFFLINE:
if (time() - offline_time <= timeouts.offline_skew)
break;
ulog(LOG_INFO, 'offline for too long, setting device as orphaned\n');
set_state(ORPHAN);
break;
}
}
function trigger_reenroll() {
ulog(LOG_INFO, 'triggering reenroll\n');
if (system('/usr/bin/est_client reenroll')) {
ulog(LOG_INFO, 'reenroll failed\n');
return;
}
ulog(LOG_INFO, 'reenroll succeeded\n');
ulog(LOG_INFO, 'stopping client\n');
system('/etc/init.d/ucentral stop');
set_state(DISCOVER);
}
function expiry_handler() {
let stat = fs.stat('/etc/ucentral/operational.ca');
if (!stat)
return;
let ret = system(`openssl x509 -checkend ${timeouts.expiry_threshold} -noout -in /certificates/operational.pem`);
if (!ret) {
ulog(LOG_INFO, 'checked certificate expiry - all ok\n');
return;
}
ulog(LOG_INFO, 'certificate will expire soon\n');
trigger_reenroll();
}
let ubus_methods = {
discover: {
call: function(req) {
set_state(DISCOVER);
return 0;
},
args: {
}
},
renew: {
call: function(req) {
if (state != ONLINE)
return;
ulog(LOG_INFO, 'Validate cloud due to DHCP renew event\n');
let gateway = gateway_load();
let cloud = readjsonfile('/tmp/cloud.json');
if (!cloud?.dhcp_server || !cloud?.dhcp_port)
return 0;
if (cloud.dhcp_server != gateway?.server || cloud.dhcp_port != gateway?.port)
set_state(DISCOVER);
else
ulog(LOG_INFO, 'Cloud has not changed\n');
},
args: {
}
},
online: {
call: function(req) {
set_state(ONLINE);
return 0;
},
args: {
}
},
offline: {
call: function(req) {
if (state == ONLINE)
set_state(OFFLINE);
return 0;
},
args: {
}
},
reload: {
call: function(req) {
timeouts_load();
return 0;
},
args: {
}
},
status: {
call: function(req) {
const names = [ 'discover', 'validate', 'online', 'offline', 'orphan' ];
let ret = { state: names[state] };
switch(state){
case OFFLINE:
ret.since = time() - offline_time;
break;
case ORPHAN:
ret.since = time() - orphan_time;
break;
case VALIDATING:
ret.since = time() - validate_time;;
break;
}
return ret;
},
args: {},
},
reenroll: {
call: function(req) {
trigger_reenroll();
return 0;
},
args: {},
},
};
detect_certificate_type();
if (gateway_available()) {
let status = ubus.call('ucentral', 'status');
ulog(LOG_INFO, 'cloud is known\n');
if (status?.connected) {
state = ONLINE;
} else {
client_start();
set_state(VALIDATING);
}
} else {
dhcp_restart();
}
timeouts_load();
interval = uloop.interval(timeouts.interval, interval_handler);
uloop.interval(timeouts.expiry_interval, expiry_handler);
ubus.publish('cloud', ubus_methods);
uloop.run();
uloop.done();