5 'require tools.widgets as widgets';
8 network
.registerPatternVirtual(/^yggdrasil-.+$/);
10 function validatePrivateKey(section_id
,value
) {
11 if (value
.length
== 0) {
14 if (!value
.match(/^([0-9a-fA-F]){128}$/)) {
15 if (value
!= "auto") {
16 return _('Invalid private key string %s').format(value
);
23 function validatePublicKey(section_id
,value
) {
24 if (value
.length
== 0) {
27 if (!value
.match(/^([0-9a-fA-F]){64}$/))
28 return _('Invalid public key string %s').format(value
);
32 function validateYggdrasilListenUri(section_id
,value
) {
33 if (value
.length
== 0) {
36 if (!value
.match(/^(tls|tcp|unix|quic):\/\//))
37 return _('Unsupported URI scheme in %s').format(value
);
41 function validateYggdrasilPeerUri(section_id
,value
) {
42 if (!value
.match(/^(tls|tcp|unix|quic|socks|sockstls):\/\//))
43 return _('URI scheme %s not supported').format(value
);
47 var cbiKeyPairGenerate
= form
.DummyValue
.extend({
48 cfgvalue: function(section_id
, value
) {
51 'click':ui
.createHandlerFn(this, function(section_id
,ev
) {
52 var prv
= this.section
.getUIElement(section_id
,'private_key'),
53 pub
= this.section
.getUIElement(section_id
,'public_key'),
56 return generateKey().then(function(keypair
){
57 prv
.setValue(keypair
.priv
);
58 pub
.setValue(keypair
.pub
);
62 },[_('Generate new key pair')]);
66 function updateActivePeers(ifname
) {
67 getPeers(ifname
).then(function(peers
){
68 var table
= document
.querySelector('#yggdrasil-active-peerings-' + ifname
);
70 while (table
.rows
.length
> 1) { table
.deleteRow(1); }
71 peers
.forEach(function(peer
) {
72 var row
= table
.insertRow(-1);
73 row
.style
.fontSize
= "xx-small";
75 row
.style
.opacity
= "66%";
77 var cell
= row
.insertCell(-1)
79 cell
.textContent
= peer
.remote
;
81 cell
= row
.insertCell(-1)
83 cell
.textContent
= peer
.up
? "Up" : "Down";
85 cell
= row
.insertCell(-1)
87 cell
.textContent
= peer
.inbound
? "In" : "Out";
89 cell
= row
.insertCell(-1)
91 cell
.innerHTML
= "<u style='cursor: default'>" + peer
.address
+ "</u>"
92 cell
.dataToggle
= "tooltip";
93 cell
.title
= "Key: " + peer
.key
;
95 cell
= row
.insertCell(-1)
97 cell
.textContent
= '%t'.format(peer
.uptime
);
99 cell
= row
.insertCell(-1)
100 cell
.className
= "td"
101 cell
.textContent
= '%.2mB'.format(peer
.bytes_recvd
);
103 cell
= row
.insertCell(-1)
104 cell
.className
= "td"
105 cell
.textContent
= '%.2mB'.format(peer
.bytes_sent
);
107 cell
= row
.insertCell(-1)
108 cell
.className
= "td"
109 cell
.textContent
= peer
.priority
;
111 cell
= row
.insertCell(-1)
112 cell
.className
= "td"
114 cell
.innerHTML
= "<u style='cursor: default'>%t ago</u>".format(peer
.last_error_time
)
115 cell
.dataToggle
= "tooltip"
116 cell
.title
= peer
.last_error
121 setTimeout(updateActivePeers
.bind(this, ifname
), 5000);
126 var cbiActivePeers
= form
.DummyValue
.extend({
127 cfgvalue: function(section_id
, value
) {
128 updateActivePeers(this.option
);
131 'id': 'yggdrasil-active-peerings-' + this.option
,
133 E('tr', {'class': 'tr'}, [
134 E('th', {'class': 'th'}, _('URI')),
135 E('th', {'class': 'th'}, _('State')),
136 E('th', {'class': 'th'}, _('Dir')),
137 E('th', {'class': 'th'}, _('IP Address')),
138 E('th', {'class': 'th'}, _('Uptime')),
139 E('th', {'class': 'th'}, _('RX')),
140 E('th', {'class': 'th'}, _('TX')),
141 E('th', {'class': 'th'}, _('Priority')),
142 E('th', {'class': 'th'}, _('Last Error')),
148 var generateKey
= rpc
.declare({
149 object
:'luci.yggdrasil',
150 method
:'generateKeyPair',
154 var getPeers
= rpc
.declare({
155 object
:'luci.yggdrasil',
157 params
:['interface'],
161 var callIsJumperInstalled
= rpc
.declare({
162 object
:'luci.yggdrasil-jumper',
163 method
:'isInstalled',
164 expect
:{isInstalled
: false}
167 var callValidateJumperConfig
= rpc
.declare({
168 object
:'luci.yggdrasil-jumper',
169 method
:'validateConfig',
171 expect
:{output
: "Unknown error."}
174 function validateJumperConfig(section
) {
175 var last_input
= "", last_output
= "";
177 return function(section_id
, input
) {
178 if (last_input
!= input
) {
181 callValidateJumperConfig(input
).then(function(output
) {
182 last_output
= output
;
184 var option
= section
.getUIElement(section_id
).jumper_config
;
185 option
.triggerValidation(section_id
);
189 if (last_output
.length
== 0) {
193 return _(last_output
);
197 return network
.registerProtocol('yggdrasil',
199 getI18n: function() {
200 return _('Yggdrasil Network');
202 getIfname: function() {
203 return this._ubus('l3_device') || this.sid
;
205 getType: function() {
208 getOpkgPackage: function() {
211 isFloating: function() {
214 isVirtual: function() {
217 getDevices: function() {
220 containsDevice: function(ifname
) {
221 return(network
.getIfnameOf(ifname
)==this.getIfname());
223 renderFormOptions: function(s
) {
225 o
=s
.taboption('general',form
.Value
,'private_key',_('Private key'),_('The private key for your Yggdrasil node'));
228 o
.validate
=validatePrivateKey
;
230 o
=s
.taboption('general',form
.Value
,'public_key',_('Public key'),_('The public key for your Yggdrasil node'));
232 o
.validate
=validatePublicKey
;
234 s
.taboption('general',cbiKeyPairGenerate
,'_gen_server_keypair',' ');
236 o
=s
.taboption('advanced',form
.Value
,'mtu',_('MTU'),_('A default MTU of 65535 is set by Yggdrasil. It is recomended to utilize the default.'));
239 o
.datatype
='range(1280, 65535)';
241 o
=s
.taboption('general',form
.TextValue
,'node_info',_('Node info'),_('Optional node info. This must be a { "key": "value", ... } map or set as null. This is entirely optional but, if set, is visible to the whole network on request.'));
245 o
=s
.taboption('general',form
.Flag
,'node_info_privacy',_('Node info privacy'),_('Enable node info privacy so that only items specified in "Node info" are sent back. Otherwise defaults including the platform, architecture and Yggdrasil version are included.'));
246 o
.default=o
.disabled
;
249 s
.tab('peers',_('Peers'));
251 o
=s
.taboption('peers', form
.SectionValue
, '_active', form
.NamedSection
, this.sid
, "interface", _("Active peers"))
253 ss
.option(cbiActivePeers
, this.sid
);
255 o
=s
.taboption('peers', form
.SectionValue
, '_listen', form
.NamedSection
, this.sid
, "interface", _("Listen for peers"))
258 o
=ss
.option(form
.DynamicList
,'listen_address',_('Listen addresses'), _('Add listeners in order to accept incoming peerings from non-local nodes. Multicast peer discovery works regardless of listeners set here. URI Format: <code>tls://0.0.0.0:0</code> or <code>tls://[::]:0</code> to listen on all interfaces. Choose an acceptable URI <code>tls://</code>, <code>tcp://</code>, <code>unix://</code> or <code>quic://</code>'));
259 o
.placeholder
="tls://0.0.0.0:0"
260 o
.validate
=validateYggdrasilListenUri
;
262 o
=s
.taboption('peers',form
.DynamicList
,'allowed_public_key',_('Accept from public keys'),_('If empty, all incoming connections will be allowed (default). This does not affect outgoing peerings, nor link-local peers discovered via multicast.'));
263 o
.validate
=validatePublicKey
;
265 o
=s
.taboption('peers', form
.SectionValue
, '_peers', form
.TableSection
, 'yggdrasil_%s_peer'.format(this.sid
), _("Peer addresses"))
269 ss
.addbtntitle
=_("Add peer address");
271 o
=ss
.option(form
.Value
,"address",_("Peer URI"));
272 o
.placeholder
="tls://0.0.0.0:0"
273 o
.validate
=validateYggdrasilPeerUri
;
274 ss
.option(widgets
.NetworkSelect
,"interface",_("Peer interface"));
276 o
=s
.taboption('peers', form
.SectionValue
, '_interfaces', form
.TableSection
, 'yggdrasil_%s_interface'.format(this.sid
), _("Multicast rules"))
278 ss
.addbtntitle
=_("Add multicast rule");
282 o
=ss
.option(widgets
.DeviceSelect
,"interface",_("Devices"));
285 ss
.option(form
.Flag
,"beacon",_("Send multicast beacon"));
287 ss
.option(form
.Flag
,"listen",_("Listen to multicast beacons"));
289 o
=ss
.option(form
.Value
,"port",_("Port"));
291 o
.datatype
='range(1, 65535)';
293 o
=ss
.option(form
.Value
,"password",_("Password"));
298 s
.tab('jumper',_('Jumper'));
306 _('%s is an independent project that aims to transparently reduce latency of a connection over Yggdrasil network, utilizing NAT traversal to bypass intermediary nodes.'.format('<a href="https://github.com/one-d-wide/yggdrasil-jumper">Yggdrasil Jumper</a>'))
307 + ' ' + _('It periodically probes for active sessions and automatically establishes direct peerings over internet with remote nodes running Yggdrasil Jumper without requiring firewall or port configuration.')
314 _('Enable Yggdrasil Jumper'),
315 _('The checkbox cannot be modified unless the <code>yggdrasil-jumper</code> package is installed.')
321 // Unlock enable option if jumper is installed
322 callIsJumperInstalled().then(function(isInstalled
) {
324 var o
= s
.children
.find(function(o
) { return o
.option
== "jumper_enable"; });
326 // Explicit rerendering request isn't needed because the protocol tab
327 // is constructed only after all async functions is done
338 o
.value('off', _('Off'));
339 o
.value('error', _('Error'));
340 o
.value('warn', _('Warn'));
341 o
.value('info', _('Info'));
342 o
.value('debug', _('Debug'));
343 o
.value('trace', _('Trace'));
350 'allocate_listen_addresses',
351 _('Allocate listen addresses'),
352 _('Allow Yggdrasil Jumper to automatically configure Yggdrasil with proper listen address and random port.')
360 'jumper_autofill_listen_addresses',
361 _('Autofill listen addresses'),
362 _('Retrieve the listener addresses from the Yggdrasil interface configuration.')
372 _('Additional configuration settings (in TOML format).')
375 o
.validate
=validateJumperConfig(s
);
379 deleteConfiguration: function() {
380 uci
.sections('network', 'yggdrasil_%s_interface'.format(this.sid
), function(s
) {
381 uci
.remove('network', s
['.name']);
383 uci
.sections('network', 'yggdrasil_%s_peer'.format(this.sid
), function(s
) {
384 uci
.remove('network', s
['.name']);