Skip to content

check_ntp_offset: read the windows time status from w32time.dll - #489

Open
inqrphl wants to merge 6 commits into
mainfrom
ntp-offset-w32timedll
Open

inqrphl wants to merge 6 commits into
mainfrom
ntp-offset-w32timedll

Conversation

@inqrphl

@inqrphl inqrphl commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

AI GENERATED

Summary

Adds a w32time source to check_ntp_offset that reads the Windows Time status
from w32time.dll (W32TimeQueryStatus) instead of spawning w32tm.exe. It is
preferred on 64-bit Windows; w32tm.exe stays as fallback and can still be
selected with source=w32tm.

Why

  • No process spawn and no parsing of localized w32tm output; the DLL returns
    typed values and needs no admin rights.
  • Ported from windows_exporter (PR #2813); no new dependency or package.

What changed

  • New w32time source: Windows implementation in
    pkg/snclient/check_ntp_offset_windows.go, stub in
    check_ntp_offset_other.go, shared types and entry formatting in
    check_ntp_offset.go.
  • Automatic detection is now an ordered source list; the other sources are
    unchanged.
  • New attributes, only set by w32time:
Attribute Description
leap_indicator 0 no warning, 1 minute has 61s, 2 minute has 59s, 3 not synchronized
state Clock discipline: 0 unset, 1 hold, 2 sync, 3 spike
state_name State name (Unset, Hold, Sync, Spike)
last_sync_result 0 success, 1 no data, 2 stale data, 3 change too big, 4 shutdown
last_sync_result_name Result name (Success, NoData, …)
root_delay Round-trip delay to the primary source, ms
root_dispersion Maximum error relative to the primary source, ms
last_sync Last successful synchronization, RFC 3339 (UTC)

I can trim those if needed @sni . I know that other providers are not as detailed, but just like check_drivesize I get a bit more information when using DLLs directly.

Behaviour

  • Metrics are unchanged (offset, stratum, plus jitter where available);
    w32time has no jitter, like w32tm.
  • A failed query returns UNKNOWN (service stopped, access denied, missing DLL,
    unsupported layout), so a collection error is not reported as a clock problem.
  • A successful query with a state other than Sync returns CRITICAL, as before.
  • In auto mode on 64-bit Windows the source attribute changes from w32tm to
    w32time.

32-bit Windows

w32time is unavailable there (386 layout mismatch; no 32-bit w32time.dll on
64-bit Windows). auto uses w32tm, and source=w32time returns UNKNOWN.

Need a VM to test/fix this out, but did not do it yet. Basic 32bit compiled snclient is not enough, I need the 32bit version of the DLL that I can only get in a 32 bit windows install. If it is important, I can test it separately @sni

Verification

Compared field by field with w32tm /query /status /verbose on Windows 11 24H2
amd64 while the clock was 2.08s fast: phase offset (2.0826758s, same sign), poll
interval, root delay/dispersion, precision, stratum, leap indicator, source,
state, last sync result and last successful sync all matched. The phase offset
matched again after a resync (0.0008642s).

  • go test ./pkg/snclient -run 'TestCheckNTPOffset|TestParseW32TimeStatus'
    covers the captured W32TIME_STATUS_INFO buffer, entry formatting and state
    handling; the live Windows test skips unless the service is running and in
    Sync.
  • go vet passes for windows/linux/darwin/freebsd; 386 and amd64 builds compile;
    make docs was run.
  • Not run locally: make golangci, make citest.

Review notes

  • The DLL export check runs in a sync.Once; a missing DLL or export falls back
    cleanly to the next source.
  • parseW32TimeStatus rejects the 386 layout instead of reading shifted fields.

inqrphl and others added 2 commits November 3, 2026 14:17
Add a w32time source which reads the status of the Windows Time service
directly from w32time.dll (W32TimeQueryStatus) instead of spawning
w32tm.exe. It needs no administrative rights and provides the same data
as w32tm /query /status /verbose: phase offset, stratum, source, clock
state, last synchronization result and the time of the last successful
synchronization.

On 64 bit windows the new source is preferred in the automatic source
detection. w32tm.exe is kept as a fallback and remains selectable with
source=w32tm. 32 bit builds cannot use it: the 386 structure layout does
not match the dll and on 64 bit windows there is no w32time.dll for a
32 bit process, so they continue to use w32tm.

The new source adds the filter attributes leap_indicator, state,
state_name, last_sync_result, last_sync_result_name, root_delay,
root_dispersion and last_sync, which are only set for w32time. The
existing metrics (offset, stratum) are unchanged.

A failed query returns UNKNOWN instead of CRITICAL, so a stopped time
service does not show up as a clock problem. A successful query which
reports a state other than Sync still returns CRITICAL, as before.

The implementation is a port of the windows_exporter W32Time support
(prometheus-community/windows_exporter#2813). check_ntp_offset_windows.go
carries the Apache-2.0 header and the origin, check_ntp_offset_other.go
is the stub for the other platforms. The structure layout was verified
against w32tm /query /status on windows.

The source detection was changed from a sequence of if blocks to an
ordered list to make adding the new source simpler.

Unit tests cover the captured W32TIME_STATUS_INFO buffer and the entry
formatting, the live windows test skips unless the service is running
and synchronized. Changes and the generated check documentation are
updated.
@inqrphl

inqrphl commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

make clean after make docs is failing due to changes in the omd.consol.de repository

| jitter | jitter of the clock in milliseconds |
| offset | Time offset to the NTP server in milliseconds. SNClient adds this value as a metric. |
| offset_seconds | Time offset to the NTP server in seconds. SNClient does not add this value as a metric. SNClient converts thresholds from 'offset_seconds' to 'offset' without a message. |
| Attribute | Description |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some of these attributes are only present in w32time.dll backend.

it is way more verbose than some other tools like chrony. i included all of them here, what is not needed can be trimmed.

return nil
}

// ntpSource is a provider of time data, tried in the order of ntpSources

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored this to a struct to avoid golangci warnings

Comment thread pkg/snclient/check_ntp_offset.go Outdated
}

// w32timeStatus is the status of the local Windows Time service. It is filled
// by queryW32TimeStatus, a port of the windows_exporter (Apache-2.0, Copyright

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed to add license details here, its apache 2


// On 386 there is no w32time.dll in a 32 bit process and the structure layout
// differs, so automatic detection has to use w32tm.
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like it says in the PR description, this method likely does not work on a 32 bit system.

Need to find its own dll and test it, the structs could be packed differently.

Do not want to commit it without testing, so return an error here.

| last_sync_result_name | name of the last synchronization result. Only set with source w32time. |
| root_delay | total round-trip delay to the primary time source in milliseconds. Only set with source w32time. |
| root_dispersion | root dispersion, the maximum error relative to the primary time source in milliseconds. Only set with source w32time. |
| last_sync | time of the last successful synchronization in RFC 3339 format. Only set with source w32time. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be translated to another format if needed. cannot remember if snclient has a preferred time format

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default format is unix timestamps (unit: UDate) then there are formatters to convert this in any user supplied format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, its now an unix timestamp with milisecond precision

// recorded by the windows_exporter project (Apache-2.0, Copyright The
// Prometheus Authors) and is reused to test the ported parser.
const statusSample = "" +
"78000000 00000000 01000000 06000000" +

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this binary data comes from prometheus tests.

testing to this detail might be too excessive to put in snclient, so I can remove this if desired.

@inqrphl
inqrphl requested a review from sni November 11, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants