Page MenuHomeVyOS Platform

# Feature Request: Expose a client-side source-address option for `service ntp`
Closed, ResolvedPublicFEATURE REQUEST

Description

Summary

Add a CLI option under service ntp (either globally or per server) to bind the local source address used for outgoing NTP client requests — analogous to bindacqaddress in the underlying chrony daemon. Currently VyOS only exposes interface (which restricts which interface(s) chronyd listens/responds on in its *server* role) and per-server options (interleave, noselect, nts, pool, prefer, ptp-transport) — none of which control the source address chrony uses when *sending* client requests to upstream NTP servers.

Motivation / real-world use case

We run edge routers (VyOS 1.5.1, FRR 10.5.2) with full BGP tables from multiple transit/peering sessions in a single default VRF. One peering interface (an IX peering LAN, RFC-style shared segment) carries an address space that is intentionally not globally routable beyond that exchange fabric — by design, not a misconfiguration.

BGP sessions are unaffected by this: every peer-group explicitly sets update-source bound to the router's loopback, so BGP traffic always uses a globally-reachable source address regardless of which physical interface the underlying route happens to egress through.

Ordinary OS-level services have no equivalent mechanism. NTP client requests (and DNS resolver queries, which have the same problem but no dedicated VyOS service to configure in the first place) use the kernel's default source-address selection for whatever route wins for a given destination. When BGP best-path selection for a given NTP/DNS server happens to prefer the interface facing the non-globally-routable IX peering LAN, chronyd's request goes out with that interface's own address as the source — and the reply can never find its way back, since that address isn't routable outside the exchange fabric. This causes intermittent, best-path-dependent NTP (and DNS) failures that are confusing to diagnose because they come and go as BGP paths change, with no configuration change on our side.

We verified via live tab-completion on production hardware (VyOS 1.5.1) that no source-address option exists anywhere in the service ntp config tree:

vyos@r1# set service ntp ?
Possible completions:
 > allow-client         Restrict to allowed IP client addresses
   interface            Interface
   leap-second          Leap second behavior (default: timezone)
+  listen-address       Local IP addresses to listen on
   local-stratum        Local reference stratum
 > offload              Configurable offload options
   ptp-transport        Enables the PTP transport for NTP packets
+> server               Network Time Protocol (NTP) server
   vrf                  VRF instance name

vyos@r1# set service ntp interface <TAB>
<text>  bond0  bond0.200  bond0.201  bond0.211  bond0.213  bond0.300  bond0.313
        eth0  eth1  eth2  eth3  eth4  eth5  eth6  eth7  eth7.800  lo

vyos@r1# set service ntp server 1.1.1.1 ?
Possible completions:
   interleave           Use the interleaved mode for the server
   noselect             Marks the server as unused
   nts                  Enable Network Time Security (NTS) for the server
   pool                 Associate with a number of remote servers
   prefer                Marks the server as preferred
   ptp-transport        Use the PTP transport for the server

interface restricts the *server*/listening role; it does not affect which local address chronyd uses as the source when it initiates a request to an upstream server.

What we actually want

A way to pin the source address chronyd uses for outbound client requests to a stable, globally-reachable address (typically the router's loopback) — mirroring what update-source already does for BGP — independent of which physical interface the kernel's route selection happens to prefer for a given upstream NTP server at any given moment.

Current workaround (and why it's not ideal)

The generic Linux-level workaround is source-address-based policy routing (an ip rule plus a routing table with an explicit src on the relevant routes), which would work for any local service and isn't specific to NTP. We are still evaluating whether VyOS's policy route CLI can express this at all (checking set policy route <name> rule <n> set ? for a usable action); this is not yet confirmed to be exposed either, and even if it is, it's an indirect, harder-to-audit fix for what chrony can already do directly and cleanly with one config line.

Absent either option, the only "fix" available today is hoping BGP best-path selection doesn't route the relevant destinations over the affected interface — not an actual fix, just living with an intermittent, hard-to-explain failure mode.

Requested syntax (example)

set service ntp source-address '185.137.128.1'

or, if per-server granularity is preferred to match chrony's own per-directive flexibility:

set service ntp server 1.1.1.1 source-address '192.168.128.1'

This maps directly to chrony's existing bindacqaddress directive (source address used for client requests to NTP sources), which chrony has supported since 4.0. This is not a request for new chrony functionality — the underlying daemon already does exactly this — the ask is purely to expose it through VyOS's declarative config schema (interface-definitions XML), the same pattern as our earlier fib daddr type request.

Why this matters beyond our specific case

Any router or host that (a) participates in more than one routing domain with asymmetric reachability (multiple transit/peering paths, VRFs, IX fabrics with private addressing) and (b) needs its own local services — not just BGP — to always speak from a stable, globally-reachable identity, hits this same gap. BGP, BFD, and similar routing-protocol daemons in VyOS already solve this via update-source/equivalent; general host services (NTP, and by extension anything else chrony-adjacent) currently cannot, even though the daemon underneath already has the capability.

Environment

  • VyOS 1.5.1 (circinus), official generic ISO, FRR 10.5.2
  • Verified via live CLI tab-completion on production hardware (HP ProLiant DL360p Gen8)

Will send patch in some minutes

Details

Version
-
Is it a breaking change?
Unspecified (possibly destroys the router)
Issue type
Feature (new functionality)

Event Timeline

rherold created this object in space S1 VyOS Public.

How will it work if I got 2 different VRFs?

Like if I want to use VRF:MGMT to sync upstream and VRF:PROD to share downstream?

Currently there can only be a single vrf configured for service ntp.

Above means that Im currently forced to add a container using dockurr/chrony in VyOS where I do something like this:

set container name ntp allow-host-networks
set container name ntp environment ENABLE_NTS value 'false'
set container name ntp environment ENABLE_SYSCLK value 'false'
set container name ntp environment LOG_LEVEL value '0'
set container name ntp environment NOCLIENTLOG value 'false'
set container name ntp environment NTP_DIRECTIVES value 'ratelimit\nrtcsync\nbindacqdevice MGMT\nbinddevice PROD\nallow all 10.0.0.0/8'
set container name ntp environment NTP_SERVERS value '194.58.200.20'
set container name ntp environment TZ value 'Europe/Stockholm'
set container name ntp image 'docker.io/dockurr/chrony:4.8'
set container name ntp memory '32'
set container name ntp restart 'on-failure'

And at the same time use this for service ntp:

set service ntp allow-client address '192.0.2.0/24'
set service ntp server 194.58.200.20 prefer
set service ntp vrf 'MGMT'

This way service ntp will sync the RTC of the VyOS box with upstream using VRF:MGMT (and share if needed to other devices on VRF:MGMT - the allow-client line can be removed if this is not needed).

While the container (note it got ENABLE_SYSCLK set to false) will fetch time with upstream using VRF:MGMT and then share time downstream at VRF:PROD.

Unless I misread something it seems like https://github.com/vyos/vyos-1x/pull/5371 only allows for IPv4/IPv6-addresses which is incorrect.

Or rather using bindacqdevice and binddevice should be included in this patch aswell.

@Apachez: Thanks for the feedback — you're right that a source IP alone doesn't fully solve this for multi-VRF setups with overlapping address ranges.

I've added a complementary set service ntp source-interface <interface> option (chrony's bindacqdevice), which binds the outgoing client socket directly to a device instead of an address. It can be combined with source-address, since bindacqaddress and bindacqdevice bind different socket properties. If vrf is also configured, the interface
must belong to that VRF (same check already used for the existing interface option).

Regarding the single-VRF limitation: that's a pre-existing constraint of service ntp vrf (chronyd runs once per VRF context in VyOS), independent of this patch — this doesn't change or worsen it, and would need separate work to lift.

Pushed to PR #5371 (code) and #2185 (docs).

Its worsen the situation since selecting source-address wont work if thats in a different vrf so that smoketest will fail where it shouldnt.

I would expect when adding commands they shouldnt break things even further but fix things so there over time will be less things to "FIXME".

Im not sure how the naming should be but we would need something like (just a suggestion):

  1. Expand set service ntp vrf into two parts - one for incoming (binddevice) and one for outgoing (bindacqdevice) connections.

Perhaps something like this:

set service ntp vrf incoming PROD
set service ntp vrf outgoing MGMT

If not set then binddevice and bindacqdevice isnt set either mening vrf DEFAULT (or I think its named LOCAL when you look in the fib) is being used.

Since chrony is limited to only one interface (in Linux VRFs are listed as interfaces) there cant be a multiselection as in having binddevice work at both MGMT and PROD at the same time:

https://chrony-project.org/doc/latest/chrony.conf.html#binddevice
https://chrony-project.org/doc/latest/chrony.conf.html#bindacqdevice

This way we get a 1:1 map to bindacqdevice (outgoing) and binddevice (incoming).

Now you can set listen-address and source-address as pure IPv4/IPv6 addresses and the vrf-config will be within the set service ntp vrf context for both incoming and outgoing connections.

binddevice/bindacqdevice are already separately configurable — binddevice maps to the existing interface option (incoming/listen), bindacqdevice to this PR's new
ource-interface (outgoing/client). So incoming vs. outgoing device binding is already independent.

What's still single-valued is the VRF itself — chrony runs as one instance in one VRF (service ntp vrf <name>), so genuinely asymmetric incoming/outgoing VRF binding
isn't possible without running a second chrony instance, which is a bigger architectural change than this patch's scope (client source-address/interface). Given the
single-VRF limitation predates this PR, I'd suggest tracking that as its own task rather than blocking this one on it — happy to file it if there's agreement.

Again, VRF is selected through both bindacqdevice and binddevice so having "asymmetric incoming/outgoing VRF binding" works perfectly fine.

Look at this config example for dockurr/chrony:

set container name ntp allow-host-networks
set container name ntp environment ENABLE_NTS value 'false'
set container name ntp environment ENABLE_SYSCLK value 'false'
set container name ntp environment LOG_LEVEL value '0'
set container name ntp environment NOCLIENTLOG value 'false'
set container name ntp environment NTP_DIRECTIVES value 'ratelimit\nrtcsync\nbindacqdevice MGMT\nbinddevice PROD\nallow all 10.0.0.0/8'
set container name ntp environment NTP_SERVERS value '194.58.200.20'
set container name ntp environment TZ value 'Europe/Stockholm'
set container name ntp image 'docker.io/dockurr/chrony:4.8'
set container name ntp memory '32'
set container name ntp restart 'on-failure'

Note the line with NTP_DIRECTIVES.

Again I think the ntp services ntp vrf should be expanded into two subsets (incoming and outgoing) since its not obvious that VRF would be set through the "source-interface" option.

Currently Im forced to add chrony as a container since VyOS on its own doesnt properly support configuring service ntp for two different VRFs as chrony do support (one for outgoing requests and one for incoming requests).

You're right that device binding implicitly selects the VRF — bindacqdevice/binddevice on an interface that belongs to a VRF scopes that socket to it, no separate VRF
option needed for that to work at the chrony level.

What actually blocks your scenario today is a pre-existing check in verify() (not introduced by this patch — the same logic already applies to interface, this patch's
source-interface just follows the established pattern):

if 'vrf' in ntp:

tmp = get_interface_config(source_interface)
vrf_name = ntp['vrf']
if 'master' not in tmp or tmp['master'] != vrf_name:
    raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{source_interface}" '
                      f'does not belong to this VRF!')

This explicitly rejects interface/source-interface values outside the top-level vrf. Relaxing it would let your asymmetric-binding case work without restructuring vrf
into separate incoming/outgoing options — but that's a behavior change to existing, pre-patch logic affecting the whole service ntp vrf semantics, not something I think
should ride along with this patch's scope (client source-address/interface).

I would appreciate a maintainer call on this: should the VRF-match constraint be relaxed (as a follow-up), or is there a reason it's
intentionally strict that I'm missing? Happy to file a separate task and pick it up once there's agreement on direction.

Viacheslav changed the task status from Open to In progress.Aug 7 2026, 10:03 AM
Viacheslav assigned this task to rherold.
Viacheslav triaged this task as Normal priority.

@Viacheslav thx for cleanup the task,. Did you see that I ask for an decision from the team about the implementation?

I would appreciate a maintainer call on this: should the VRF-match constraint be relaxed (as a follow-up), or is there a reason it's
intentionally strict that I'm missing? Happy to file a separate task and pick it up once there's agreement on direction.
Viacheslav changed the task status from In progress to Needs testing.Aug 14 2026, 1:07 PM
Viacheslav moved this task from Need Triage to Completed on the VyOS Rolling board.