This commit is contained in:
Matthew Otto 2026-04-05 00:21:07 -05:00 committed by GitHub
commit 7d4fba2e72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 316 additions and 98 deletions

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= chrony
PLUGIN_VERSION= 1.5
PLUGIN_VERSION= 1.6
PLUGIN_REVISION= 3
PLUGIN_COMMENT= Chrony time synchronisation
PLUGIN_DEPENDS= chrony

View file

@ -4,6 +4,18 @@ better in virtual environments.
Plugin Changelog
----------------
1.6
* Update config UI to expose the following features:
- local/orphan mode
- pools
- prefer
- iburst
- min/max poll
- interleaving
* Add per-source NTS option
* Add NTP data diagnostics
1.5
* Allow adding a fallback NTP when using NTS

View file

@ -32,6 +32,36 @@ use OPNsense\Base\ApiMutableModelControllerBase;
class GeneralController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = '\OPNsense\Chrony\General';
protected static $internalModelName = 'general';
protected static $internalModelClass = '\OPNsense\Chrony\General';
public function searchItemAction()
{
return $this->searchBase("peers.peer", null, "address");
}
public function setItemAction($uuid)
{
return $this->setBase("peer", "peers.peer", $uuid);
}
public function addItemAction()
{
return $this->addBase("peer", "peers.peer");
}
public function getItemAction($uuid = null)
{
return $this->getBase("peer", "peers.peer", $uuid);
}
public function delItemAction($uuid)
{
return $this->delBase("peers.peer", $uuid);
}
public function toggleItemAction($uuid, $enabled = null)
{
return $this->toggleBase("peers.peer", $uuid, $enabled);
}
}

View file

@ -82,4 +82,15 @@ class ServiceController extends ApiMutableServiceControllerBase
$response = $backend->configdRun("chrony chronyauthdata");
return array("response" => $response);
}
/**
* show chrony ntpdata
* @return array
*/
public function chronyntpdataAction()
{
$backend = new Backend();
$response = $backend->configdRun("chrony chronyntpdata");
return array("response" => $response);
}
}

View file

@ -28,11 +28,13 @@
namespace OPNsense\Chrony;
class GeneralController extends \OPNsense\Base\IndexController
class IndexController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/Chrony/index');
$this->view->generalForm = $this->getForm('general');
$this->view->pick('OPNsense/Chrony/general');
$this->view->formDialogPeer = $this->getForm("dialogPeer");
$this->view->formGridPeer = $this->getFormGrid("dialogPeer");
}
}

View file

@ -0,0 +1,75 @@
<form>
<field>
<id>peer.pool</id>
<label>pool</label>
<type>checkbox</type>
<help>Address refers to a pool of NTP servers</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>peer.address</id>
<label>Address</label>
<type>text</type>
<help>The address/hostname of the NTP server or pool.</help>
</field>
<field>
<id>peer.prefer</id>
<label>prefer</label>
<type>checkbox</type>
<help>Prefer this source over sources without the prefer option.</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>peer.iburst</id>
<label>iburst</label>
<type>checkbox</type>
<help>Enable iburst for this source.</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>peer.xleave</id>
<label>xleave</label>
<type>checkbox</type>
<help>Enable interleaved mode for this source.</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
<field>
<id>peer.minpoll</id>
<label>minpoll</label>
<type>text</type>
<help>The minimum interval between requests sent to the server as a power of 2 in seconds.</help>
</field>
<field>
<id>peer.maxpoll</id>
<label>maxpoll</label>
<type>text</type>
<help>The maximum interval between requests sent to the server as a power of 2 in seconds.</help>
</field>
<field>
<id>peer.nts</id>
<label>NTS</label>
<type>checkbox</type>
<help>Enable NTS authentication.</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>boolean</formatter>
</grid_view>
</field>
</form>

View file

@ -5,38 +5,24 @@
<type>checkbox</type>
<help>Enable Chrony time daemon.</help>
</field>
<field>
<id>general.localstratum</id>
<label>Local Stratum</label>
<type>text</type>
<help>(1-15) Local mode allows the system clock to be used when no other clocks are available. The number here specifies the stratum reported by the local clock and should normally be set to a number high enough to ensure that any other servers available to clients are preferred over this server.</help>
</field>
<field>
<id>general.orphanmode</id>
<label>Orphan Mode</label>
<type>checkbox</type>
<help></help>
</field>
<field>
<id>general.port</id>
<label>Listen Port</label>
<type>text</type>
<help>Set the port chrony listen to.</help>
</field>
<field>
<id>general.ntsclient</id>
<label>NTS Client Support</label>
<type>checkbox</type>
<help>Enable NTS in client mode. This will add another layer of security for peers when OPNsense is the client. Every server in Peers has to support NTS.</help>
</field>
<field>
<id>general.ntsnocert</id>
<label>NTS Disable Certcheck</label>
<type>checkbox</type>
<help>If you run NTS mode you can enable this option in order to ignore wrong time in certificates for the first check. This helps if your system starts with wrong time.</help>
</field>
<field>
<id>general.peers</id>
<label>NTP Peers</label>
<style>tokenize</style>
<type>select_multiple</type>
<allownew>true</allownew>
<help>Set as many NTP peers you need.</help>
</field>
<field>
<id>general.fallbackpeers</id>
<label>Fallback Peer</label>
<type>text</type>
<help>Set fallback peer if you use NTS and your system starts with wrong time. Best to only use this for internal trusted peers.</help>
</field>
<field>
<id>general.allowednetworks</id>
<label>Allowed Networks</label>
@ -45,4 +31,10 @@
<allownew>true</allownew>
<help>Set the networks allowed to synchronize time with this server. If this value is not set it will also not listen to the port and just synchronize the time for itself.</help>
</field>
<field>
<id>general.ntsnocert</id>
<label>NTS Disable Certcheck</label>
<type>checkbox</type>
<help>If you run NTS mode you can enable this option in order to ignore wrong time in certificates for the first check. This helps if your system starts with wrong time.</help>
</field>
</form>

View file

@ -7,31 +7,68 @@
<Default>0</Default>
<Required>Y</Required>
</enabled>
<localstratum type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>15</MaximumValue>
<Required>N</Required>
<ValidationMessage>Local stratum must be within 1-15.</ValidationMessage>
</localstratum>
<orphanmode type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</orphanmode>
<port type="PortField">
<Default>323</Default>
<Default>123</Default>
<Required>Y</Required>
</port>
<ntsclient type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</ntsclient>
<ntsnocert type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</ntsnocert>
<peers type="HostnameField">
<Default>0.opnsense.pool.ntp.org</Default>
<Required>Y</Required>
<FieldSeparator>,</FieldSeparator>
<AsList>Y</AsList>
</peers>
<fallbackpeers type="HostnameField">
<Required>N</Required>
</fallbackpeers>
<allowednetworks type="NetworkField">
<Required>N</Required>
<FieldSeparator>,</FieldSeparator>
<AsList>Y</AsList>
</allowednetworks>
<ntsnocert type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</ntsnocert>
<peers>
<peer type="ArrayField">
<pool type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</pool>
<address type="HostnameField">
<Default>opnsense.pool.ntp.org</Default>
<Required>Y</Required>
</address>
<prefer type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</prefer>
<iburst type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</iburst>
<xleave type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</xleave>
<minpoll type="IntegerField">
<MinimumValue>-6</MinimumValue>
<MaximumValue>24</MaximumValue>
<Required>N</Required>
<ValidationMessage>minpoll value must be between -6 and 24.</ValidationMessage>
</minpoll>
<maxpoll type="IntegerField">
<MinimumValue>-6</MinimumValue>
<MaximumValue>24</MaximumValue>
<Required>N</Required>
<ValidationMessage>maxpoll value must be between -6 and 24.</ValidationMessage>
</maxpoll>
<nts type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</nts>
</peer>
</peers>
</items>
</model>

View file

@ -1,7 +1,7 @@
<menu>
<Services>
<Chrony cssClass="fa fa-clock-o">
<General url="/ui/chrony/general/index"/>
<General url="/ui/chrony/index/index"/>
</Chrony>
</Services>
</menu>

View file

@ -31,12 +31,20 @@
<li><a data-toggle="tab" href="#chronysourcestats">{{ lang._('Source Stats') }}</a></li>
<li><a data-toggle="tab" href="#chronytracking">{{ lang._('Tracking') }}</a></li>
<li><a data-toggle="tab" href="#chronyauthdata">{{ lang._('Auth Data') }}</a></li>
<li><a data-toggle="tab" href="#chronyntpdata">{{ lang._('NTP Data') }}</a></li>
</ul>
<div class="tab-content content-box tab-content">
<div id="general" class="tab-pane fade in active">
<div class="content-box" style="padding-bottom: 1.5em;">
{{ partial("layout_partials/base_form",['fields':generalForm,'id':'frm_general_settings'])}}
<h1>{{ lang._('Sources') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridPeer) }}
{{ partial("layout_partials/base_dialog",['fields':formDialogPeer,'id':formGridPeer['edit_dialog_id'],'label':lang._('Edit source')])}}
<div class="col-md-12">
<hr />
<button class="btn btn-primary" id="saveAct" type="button"><b>{{ lang._('Save') }}</b> <i id="saveAct_progress"></i></button>
@ -55,10 +63,38 @@
<div id="chronyauthdata" class="tab-pane fade in">
<pre id="listchronyauthdata"></pre>
</div>
<div id="chronyntpdata" class="tab-pane fade in">
<pre id="listchronyntpdata"></pre>
</div>
</div>
<script>
$(document).ready(function() {
$("#{{formGridPeer['table_id']}}").UIBootgrid(
{
search:'/api/chrony/general/search_item/',
get:'/api/chrony/general/get_item/',
set:'/api/chrony/general/set_item/',
add:'/api/chrony/general/add_item/',
del:'/api/chrony/general/del_item/',
toggle:'/api/chrony/general/toggle_item/'
}
);
$("#reconfigureAct").SimpleActionButton();
});
var chronyActiveInterval = null;
var chronyTabUpdateMap = {
"#chronysources": update_chronysources,
"#chronysourcestats": update_chronysourcestats,
"#chronytracking": update_chronytracking,
"#chronyauthdata": update_chronyauthdata,
"#chronyntpdata": update_chronyntpdata
};
// Put API call into a function, needed for auto-refresh
function update_chronysources() {
ajaxCall(url="/api/chrony/service/chronysources", sendData={}, callback=function(data,status) {
@ -68,11 +104,10 @@ function update_chronysources() {
function update_chronysourcestats() {
ajaxCall(url="/api/chrony/service/chronysourcestats", sendData={}, callback=function(data,status) {
$("#listchronysourcestats").text(data['response']);
$("#listchronysourcestats").text($('<div>').html(data['response']).text());
});
}
// Put API call into a function, needed for auto-refresh
function update_chronytracking() {
ajaxCall(url="/api/chrony/service/chronytracking", sendData={}, callback=function(data,status) {
$("#listchronytracking").text(data['response']);
@ -85,31 +120,50 @@ function update_chronyauthdata() {
});
}
$(function() {
var data_get_map = {'frm_general_settings':"/api/chrony/general/get"};
mapDataToFormUI(data_get_map).done(function(data){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
function update_chronyntpdata() {
ajaxCall(url="/api/chrony/service/chronyntpdata", sendData={}, callback=function(data,status) {
$("#listchronyntpdata").text(data['response']);
});
}
function autoRefresh(tabId) {
if (chronyActiveInterval !== null) {
clearInterval(chronyActiveInterval);
chronyActiveInterval = null;
}
if (chronyTabUpdateMap[tabId]) {
// run once immediately
chronyTabUpdateMap[tabId]();
// autorefresh active tab every second
chronyActiveInterval = setInterval(chronyTabUpdateMap[tabId], 1000);
}
}
$(function() {
var data_get_map = {'frm_general_settings':"/api/chrony/general/get"};
mapDataToFormUI(data_get_map).done(function(data){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
updateServiceControlUI('chrony');
// Call function update_neighbor with a auto-refresh of 5 seconds
setInterval(update_chronysources, 5000);
setInterval(update_chronysourcestats, 5000);
setInterval(update_chronytracking, 5000);
setInterval(update_chronyauthdata, 5000);
// auto-refresh only the current tab
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var targetTab = $(e.target).attr("href");
autoRefresh(targetTab);
});
// link save button to API set action
$("#saveAct").click(function(){
saveFormToEndpoint(url="/api/chrony/general/set", formid='frm_general_settings',callback_ok=function(){
$("#saveAct_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url="/api/chrony/service/reconfigure", sendData={}, callback=function(data,status) {
updateServiceControlUI('chrony');
$("#saveAct_progress").removeClass("fa fa-spinner fa-pulse");
});
// link save button to API set action
$("#saveAct").click(function(){
saveFormToEndpoint(url="/api/chrony/general/set", formid='frm_general_settings',callback_ok=function(){
$("#saveAct_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url="/api/chrony/service/reconfigure", sendData={}, callback=function(data,status) {
updateServiceControlUI('chrony');
$("#saveAct_progress").removeClass("fa fa-spinner fa-pulse");
});
});
});
});
</script>

View file

@ -23,13 +23,13 @@ type:script_output
message:request chrony status
[chronysources]
command:/usr/local/bin/chronyc -m 'timeout 100' 'retries 0' sources
command:/usr/local/bin/chronyc -m 'timeout 100' 'retries 0' 'sources -v'
parameters:
type:script_output
message:show chrony sources
[chronysourcestats]
command:/usr/local/bin/chronyc -m 'timeout 100' 'retries 0' sourcestats
command:/usr/local/bin/chronyc -m 'timeout 100' 'retries 0' 'sourcestats -v'
parameters:
type:script_output
message:show chrony sourcestats
@ -41,7 +41,13 @@ type:script_output
message:show chrony tracking
[chronyauthdata]
command:/usr/local/bin/chronyc -N -m 'timeout 100' 'retries 0' authdata
command:/usr/local/bin/chronyc -N -m 'timeout 100' 'retries 0' 'authdata -v'
parameters:
type:script_output
message:show chrony authdata
[chronyntpdata]
command:/usr/local/bin/chronyc -N -m 'timeout 100' 'retries 0' ntpdata
parameters:
type:script_output
message:show chrony ntpdata

View file

@ -1,36 +1,35 @@
{% if helpers.exists('OPNsense.chrony.general.enabled') and OPNsense.chrony.general.enabled == '1' %}
port {{ OPNsense.chrony.general.port }}
{% if not helpers.empty('OPNsense.chrony.general.allowednetworks') %}
{% for network in OPNsense.chrony.general.allowednetworks.split(',') %}
allow {{ network }}
{% endfor %}
{% endif %}
{% if not helpers.empty('OPNsense.chrony.general.peers') %}
{% set peers = OPNsense.chrony.general.peers.peer %}
{% if peers is mapping %}
{% set peers = [peers] %}
{% endif %}
{% for peer in peers %}
{% if peer.pool == '1' %}pool {% else %}server {% endif %}{{peer.address}}{% if peer.prefer == '1' %} prefer{% endif %}{% if peer.iburst == '1' %} iburst{% endif %}{% if peer.xleave == '1' %} xleave{% endif %}{% if peer.minpoll is defined and peer.minpoll != '' %} minpoll {{ peer.minpoll }}{% endif %}{% if peer.maxpoll is defined and peer.maxpoll != '' %} maxpoll {{ peer.maxpoll }}{% endif %}{% if peer.nts == '1' %} nts{% endif %}
{% endfor %}
{% endif %}
{% if not helpers.empty('OPNsense.chrony.general.localstratum') %}
local stratum {{ OPNsense.chrony.general.localstratum }} {% if helpers.exists('OPNsense.chrony.general.orphanmode') and OPNsense.chrony.general.orphanmode == '1' %}orphan{% endif %}
{% endif %}
driftfile /var/db/chrony/drift
pidfile /var/run/chrony/chronyd.pid
makestep 1 3
{% if helpers.exists('OPNsense.chrony.general.ntsclient') and OPNsense.chrony.general.ntsclient == '1' %}
ntsdumpdir /var/lib/chrony
ntstrustedcerts /usr/local/etc/ssl/cert.pem
nosystemcert
{% endif %}
{% if helpers.exists('OPNsense.chrony.general.ntsnocert') and OPNsense.chrony.general.ntsnocert == '1' %}
{% if helpers.exists('OPNsense.chrony.general.ntsnocert') and OPNsense.chrony.general.ntsnocert == '1' %}
nocerttimecheck 1
{% endif %}
{% if not helpers.empty('OPNsense.chrony.general.peers') %}
{% for peer in OPNsense.chrony.general.peers.split(',') %}
server {{ peer }} iburst {% if helpers.exists('OPNsense.chrony.general.ntsclient') and OPNsense.chrony.general.ntsclient == '1' %}nts{% endif %}
{% endfor %}
{% endif %}
{% if helpers.exists('OPNsense.chrony.general.fallbackpeers') and OPNsense.chrony.general.fallbackpeers != '' %}
authselectmode mix
server {{ OPNsense.chrony.general.fallbackpeers }}
{% endif %}
{% if not helpers.empty('OPNsense.chrony.general.allowednetworks') %}
{% for network in OPNsense.chrony.general.allowednetworks.split(',') %}
allow {{ network }}
{% endfor %}
{% endif %}
{% endif %}
{% endif %}