Skip to content

Commit 11a095c

Browse files
authored
feat(ntp): enhances time sync with DHCP NTP and custom servers (#625)
* Ensure the mDNS mode is set every time network state changes Eliminates (mostly) duplicate code * Add custom NTP and HTTP time sync servers Since the ordering may have been previously defaulted and saved as "ntp,http", but that was being ignored and fallback-defaults were being used, in Ordering, `ntp` means use the fallback NTP servers, and `http` means use the fallback HTTP URLs. Thus `ntp_user_provided` and `http_user_provided` are the user specified static lists. * Add support for using DHCP-provided NTP server
1 parent 584768b commit 11a095c

File tree

8 files changed

+166
-42
lines changed

8 files changed

+166
-42
lines changed

internal/confparser/confparser_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@ type testNetworkConfig struct {
4343
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
4444
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
4545
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
46-
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
46+
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
4747
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
4848
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
49+
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
50+
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
4951
}
5052

5153
func TestValidateConfig(t *testing.T) {

internal/network/config.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ type NetworkConfig struct {
4545
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
4646
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
4747
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
48-
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,ntp_fallback" default:"ntp,http"`
48+
TimeSyncOrdering []string `json:"time_sync_ordering,omitempty" one_of:"http,ntp,ntp_dhcp,ntp_user_provided,http_user_provided" default:"ntp,http"`
4949
TimeSyncDisableFallback null.Bool `json:"time_sync_disable_fallback,omitempty" default:"false"`
5050
TimeSyncParallel null.Int `json:"time_sync_parallel,omitempty" default:"4"`
51+
TimeSyncNTPServers []string `json:"time_sync_ntp_servers,omitempty" validate_type:"ipv4_or_ipv6" required_if:"TimeSyncOrdering=ntp_user_provided"`
52+
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
5153
}
5254

5355
func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {

internal/network/netif.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type NetworkInterfaceState struct {
2121
ipv6Addr *net.IP
2222
ipv6Addresses []IPv6Address
2323
ipv6LinkLocal *net.IP
24+
ntpAddresses []*net.IP
2425
macAddr *net.HardwareAddr
2526

2627
l *zerolog.Logger
@@ -76,6 +77,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
7677
onInitialCheck: opts.OnInitialCheck,
7778
cbConfigChange: opts.OnConfigChange,
7879
config: opts.NetworkConfig,
80+
ntpAddresses: make([]*net.IP, 0),
7981
}
8082

8183
// create the dhcp client
@@ -89,7 +91,7 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
8991
opts.Logger.Error().Err(err).Msg("failed to update network state")
9092
return
9193
}
92-
94+
_ = s.updateNtpServersFromLease(lease)
9395
_ = s.setHostnameIfNotSame()
9496

9597
opts.OnDhcpLeaseChange(lease)
@@ -135,6 +137,27 @@ func (s *NetworkInterfaceState) IPv6String() string {
135137
return s.ipv6Addr.String()
136138
}
137139

140+
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
141+
return s.ntpAddresses
142+
}
143+
144+
func (s *NetworkInterfaceState) NtpAddressesString() []string {
145+
ntpServers := []string{}
146+
147+
if s != nil {
148+
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
149+
150+
if len(s.ntpAddresses) > 0 {
151+
for _, server := range s.ntpAddresses {
152+
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
153+
ntpServers = append(ntpServers, server.String())
154+
}
155+
}
156+
}
157+
158+
return ntpServers
159+
}
160+
138161
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
139162
return s.macAddr
140163
}
@@ -318,6 +341,25 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
318341
return dhcpTargetState, nil
319342
}
320343

344+
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
345+
if lease != nil && len(lease.NTPServers) > 0 {
346+
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
347+
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
348+
349+
for _, ntpServer := range lease.NTPServers {
350+
if ntpServer != nil {
351+
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
352+
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
353+
}
354+
}
355+
} else {
356+
s.l.Info().Msg("no NTP servers found in lease")
357+
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
358+
}
359+
360+
return nil
361+
}
362+
321363
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
322364
dhcpTargetState, err := s.update()
323365
if err != nil {

internal/timesync/http.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ var defaultHTTPUrls = []string{
1919
// "http://www.msftconnecttest.com/connecttest.txt",
2020
}
2121

22-
func (t *TimeSync) queryAllHttpTime() (now *time.Time) {
23-
chunkSize := 4
24-
httpUrls := t.httpUrls
22+
func (t *TimeSync) queryAllHttpTime(httpUrls []string) (now *time.Time) {
23+
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
24+
t.l.Info().Strs("httpUrls", httpUrls).Int("chunkSize", chunkSize).Msg("querying HTTP URLs")
2525

2626
// shuffle the http urls to avoid always querying the same servers
2727
rand.Shuffle(len(httpUrls), func(i, j int) { httpUrls[i], httpUrls[j] = httpUrls[j], httpUrls[i] })

internal/timesync/metrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var (
7373
},
7474
[]string{"url"},
7575
)
76+
7677
metricNtpServerInfo = promauto.NewGaugeVec(
7778
prometheus.GaugeOpts{
7879
Name: "jetkvm_timesync_ntp_server_info",

internal/timesync/ntp.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package timesync
22

33
import (
4+
"context"
45
"math/rand/v2"
56
"strconv"
67
"time"
@@ -21,9 +22,9 @@ var defaultNTPServers = []string{
2122
"3.pool.ntp.org",
2223
}
2324

24-
func (t *TimeSync) queryNetworkTime() (now *time.Time, offset *time.Duration) {
25-
chunkSize := 4
26-
ntpServers := t.ntpServers
25+
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
26+
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
27+
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")
2728

2829
// shuffle the ntp servers to avoid always querying the same servers
2930
rand.Shuffle(len(ntpServers), func(i, j int) { ntpServers[i], ntpServers[j] = ntpServers[j], ntpServers[i] })
@@ -46,6 +47,10 @@ type ntpResult struct {
4647

4748
func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (now *time.Time, offset *time.Duration) {
4849
results := make(chan *ntpResult, len(servers))
50+
51+
_, cancel := context.WithTimeout(context.Background(), timeout)
52+
defer cancel()
53+
4954
for _, server := range servers {
5055
go func(server string) {
5156
scopedLogger := t.l.With().
@@ -66,15 +71,25 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
6671
return
6772
}
6873

74+
if response.IsKissOfDeath() {
75+
scopedLogger.Warn().
76+
Str("kiss_code", response.KissCode).
77+
Msg("ignoring NTP server kiss of death")
78+
results <- nil
79+
return
80+
}
81+
82+
rtt := float64(response.RTT.Milliseconds())
83+
6984
// set the last RTT
7085
metricNtpServerLastRTT.WithLabelValues(
7186
server,
72-
).Set(float64(response.RTT.Milliseconds()))
87+
).Set(rtt)
7388

7489
// set the RTT histogram
7590
metricNtpServerRttHistogram.WithLabelValues(
7691
server,
77-
).Observe(float64(response.RTT.Milliseconds()))
92+
).Observe(rtt)
7893

7994
// set the server info
8095
metricNtpServerInfo.WithLabelValues(
@@ -91,10 +106,13 @@ func (t *TimeSync) queryMultipleNTP(servers []string, timeout time.Duration) (no
91106
scopedLogger.Info().
92107
Str("time", now.Format(time.RFC3339)).
93108
Str("reference", response.ReferenceString()).
94-
Str("rtt", response.RTT.String()).
109+
Float64("rtt", rtt).
95110
Str("clockOffset", response.ClockOffset.String()).
96111
Uint8("stratum", response.Stratum).
97112
Msg("NTP server returned time")
113+
114+
cancel()
115+
98116
results <- &ntpResult{
99117
now: now,
100118
offset: &response.ClockOffset,

internal/timesync/timesync.go

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ type TimeSync struct {
2828
syncLock *sync.Mutex
2929
l *zerolog.Logger
3030

31-
ntpServers []string
32-
httpUrls []string
33-
networkConfig *network.NetworkConfig
31+
networkConfig *network.NetworkConfig
32+
dhcpNtpAddresses []string
3433

3534
rtcDevicePath string
3635
rtcDevice *os.File //nolint:unused
@@ -64,14 +63,13 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
6463
}
6564

6665
t := &TimeSync{
67-
syncLock: &sync.Mutex{},
68-
l: opts.Logger,
69-
rtcDevicePath: rtcDevice,
70-
rtcLock: &sync.Mutex{},
71-
preCheckFunc: opts.PreCheckFunc,
72-
ntpServers: defaultNTPServers,
73-
httpUrls: defaultHTTPUrls,
74-
networkConfig: opts.NetworkConfig,
66+
syncLock: &sync.Mutex{},
67+
l: opts.Logger,
68+
dhcpNtpAddresses: []string{},
69+
rtcDevicePath: rtcDevice,
70+
rtcLock: &sync.Mutex{},
71+
preCheckFunc: opts.PreCheckFunc,
72+
networkConfig: opts.NetworkConfig,
7573
}
7674

7775
if t.rtcDevicePath != "" {
@@ -82,34 +80,42 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
8280
return t
8381
}
8482

83+
func (t *TimeSync) SetDhcpNtpAddresses(addresses []string) {
84+
t.dhcpNtpAddresses = addresses
85+
}
86+
8587
func (t *TimeSync) getSyncMode() SyncMode {
8688
syncMode := SyncMode{
89+
Ntp: true,
90+
Http: true,
91+
Ordering: []string{"ntp_dhcp", "ntp", "http"},
8792
NtpUseFallback: true,
8893
HttpUseFallback: true,
8994
}
90-
var syncModeString string
9195

9296
if t.networkConfig != nil {
93-
syncModeString = t.networkConfig.TimeSyncMode.String
97+
switch t.networkConfig.TimeSyncMode.String {
98+
case "ntp_only":
99+
syncMode.Http = false
100+
case "http_only":
101+
syncMode.Ntp = false
102+
}
103+
94104
if t.networkConfig.TimeSyncDisableFallback.Bool {
95105
syncMode.NtpUseFallback = false
96106
syncMode.HttpUseFallback = false
97107
}
98-
}
99108

100-
switch syncModeString {
101-
case "ntp_only":
102-
syncMode.Ntp = true
103-
case "http_only":
104-
syncMode.Http = true
105-
default:
106-
syncMode.Ntp = true
107-
syncMode.Http = true
109+
var syncOrdering = t.networkConfig.TimeSyncOrdering
110+
if len(syncOrdering) > 0 {
111+
syncMode.Ordering = syncOrdering
112+
}
108113
}
109114

115+
t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
116+
110117
return syncMode
111118
}
112-
113119
func (t *TimeSync) doTimeSync() {
114120
metricTimeSyncStatus.Set(0)
115121
for {
@@ -154,16 +160,61 @@ func (t *TimeSync) Sync() error {
154160
offset *time.Duration
155161
)
156162

157-
syncMode := t.getSyncMode()
158-
159163
metricTimeSyncCount.Inc()
160164

161-
if syncMode.Ntp {
162-
now, offset = t.queryNetworkTime()
163-
}
165+
syncMode := t.getSyncMode()
164166

165-
if syncMode.Http && now == nil {
166-
now = t.queryAllHttpTime()
167+
Orders:
168+
for _, mode := range syncMode.Ordering {
169+
switch mode {
170+
case "ntp_user_provided":
171+
if syncMode.Ntp {
172+
t.l.Info().Msg("using NTP custom servers")
173+
now, offset = t.queryNetworkTime(t.networkConfig.TimeSyncNTPServers)
174+
if now != nil {
175+
t.l.Info().Str("source", "NTP").Time("now", *now).Msg("time obtained")
176+
break Orders
177+
}
178+
}
179+
case "ntp_dhcp":
180+
if syncMode.Ntp {
181+
t.l.Info().Msg("using NTP servers from DHCP")
182+
now, offset = t.queryNetworkTime(t.dhcpNtpAddresses)
183+
if now != nil {
184+
t.l.Info().Str("source", "NTP DHCP").Time("now", *now).Msg("time obtained")
185+
break Orders
186+
}
187+
}
188+
case "ntp":
189+
if syncMode.Ntp && syncMode.NtpUseFallback {
190+
t.l.Info().Msg("using NTP fallback")
191+
now, offset = t.queryNetworkTime(defaultNTPServers)
192+
if now != nil {
193+
t.l.Info().Str("source", "NTP fallback").Time("now", *now).Msg("time obtained")
194+
break Orders
195+
}
196+
}
197+
case "http_user_provided":
198+
if syncMode.Http {
199+
t.l.Info().Msg("using HTTP custom URLs")
200+
now = t.queryAllHttpTime(t.networkConfig.TimeSyncHTTPUrls)
201+
if now != nil {
202+
t.l.Info().Str("source", "HTTP").Time("now", *now).Msg("time obtained")
203+
break Orders
204+
}
205+
}
206+
case "http":
207+
if syncMode.Http && syncMode.HttpUseFallback {
208+
t.l.Info().Msg("using HTTP fallback")
209+
now = t.queryAllHttpTime(defaultHTTPUrls)
210+
if now != nil {
211+
t.l.Info().Str("source", "HTTP fallback").Time("now", *now).Msg("time obtained")
212+
break Orders
213+
}
214+
}
215+
default:
216+
t.l.Warn().Str("mode", mode).Msg("unknown time sync mode, skipping")
217+
}
167218
}
168219

169220
if now == nil {

network.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ func networkStateChanged() {
1919
// do not block the main thread
2020
go waitCtrlAndRequestDisplayUpdate(true)
2121

22+
if timeSync != nil {
23+
if networkState != nil {
24+
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
25+
}
26+
27+
timeSync.Sync()
28+
}
29+
2230
// always restart mDNS when the network state changes
2331
if mDNS != nil {
2432
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())

0 commit comments

Comments
 (0)