<# .SYNOPSIS Set Windows Time Zone based on IP address and sync time via NTP .DESCRIPTION timesync.osdcloud.ch .NOTES Version: 0.1 Creation Date: 25.11.2025 Author: Akos Bakos Company: SmartCon GmbH Contact: akos.bakos@smartcon.ch Copyright (c) 2025 SmartCon GmbH HISTORY: Date By Comments ---------- --- ---------------------------------------------------------- 25.11.2025 Akos Bakos Script created #> $Global:Transcript = "$((Get-Date).ToString('yyyy-MM-dd-HHmmss'))-TimeSync.log" Start-Transcript -Path (Join-Path "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\OSD\" $Global:Transcript) -ErrorAction Ignore | Out-Null #region Helper Functions function Write-DarkGrayDate { [CmdletBinding()] param ( [Parameter(Position=0)] [System.String] $Message ) if ($Message) { Write-Host -ForegroundColor DarkGray "$((Get-Date).ToString('yyyy-MM-dd-HHmmss')) $Message" } else { Write-Host -ForegroundColor DarkGray "$((Get-Date).ToString('yyyy-MM-dd-HHmmss')) " -NoNewline } } function Write-DarkGrayHost { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [System.String] $Message ) Write-Host -ForegroundColor DarkGray $Message } function Write-DarkGrayLine { [CmdletBinding()] param () Write-Host -ForegroundColor DarkGray "=========================================================================" } function Write-SectionHeader { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [System.String] $Message ) Write-DarkGrayLine Write-DarkGrayDate Write-Host -ForegroundColor Cyan $Message } function Write-SectionSuccess { [CmdletBinding()] param ( [Parameter(Position=0)] [System.String] $Message = 'Success!' ) Write-DarkGrayDate Write-Host -ForegroundColor Green $Message } #endregion Write-SectionHeader "Set Windows Time Zone based on IP address and sync time via NTP" # DNS readiness function Wait-Network { param([int]$TimeoutSec = 45) $deadline = (Get-Date).AddSeconds($TimeoutSec) do { try { [void][System.Net.Dns]::GetHostEntry('time.windows.com'); return $true } catch { Start-Sleep 2 } } while ((Get-Date) -lt $deadline) return $false } # Robust NTP (returns UTC) function Get-NtpUtc { param([string]$Server = 'time.windows.com', [int]$TimeoutMs = 4000) $addrs = [System.Net.Dns]::GetHostAddresses($Server) $addr = $addrs | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if (-not $addr) { $addr = $addrs | Where-Object { $_.AddressFamily -eq 'InterNetworkV6' } | Select-Object -First 1 } if (-not $addr) { throw "DNS failed for $Server" } $ep = [System.Net.IPEndPoint]::new($addr,123) $udp = [System.Net.Sockets.UdpClient]::new() $udp.Client.ReceiveTimeout = $TimeoutMs $udp.Connect($ep) $req = New-Object byte[] 48; $req[0] = 0x1B [void]$udp.Send($req,$req.Length) $remote = $ep $resp = $udp.Receive([ref]$remote) $udp.Close() if ($resp.Length -lt 48) { throw "Bad NTP response" } $int = [System.BitConverter]::ToUInt32(($resp[43],$resp[42],$resp[41],$resp[40]),0) $frac = [System.BitConverter]::ToUInt32(($resp[47],$resp[46],$resp[45],$resp[44]),0) $seconds = $int + ($frac / [math]::Pow(2,32)) $epoch = [DateTime]::SpecifyKind([datetime]'1900-01-01T00:00:00Z','Utc') return $epoch.AddSeconds($seconds) # UTC } # Set UTC directly (fallback when TZ tools missing) function Set-UTCSystemTime { param([Parameter(Mandatory)][DateTime]$Utc) Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @" using System; using System.Runtime.InteropServices; public class NativeMethods { [StructLayout(LayoutKind.Sequential)] public struct SYSTEMTIME { public ushort Year; public ushort Month; public ushort DayOfWeek; public ushort Day; public ushort Hour; public ushort Minute; public ushort Second; public ushort Milliseconds; } [DllImport("kernel32.dll", SetLastError=true)] public static extern bool SetSystemTime(ref SYSTEMTIME st); } "@ -ErrorAction SilentlyContinue $u = $Utc.ToUniversalTime() $st = New-Object Win32.NativeMethods+SYSTEMTIME $st.Year=[uint16]$u.Year; $st.Month=[uint16]$u.Month; $st.Day=[uint16]$u.Day $st.Hour=[uint16]$u.Hour; $st.Minute=[uint16]$u.Minute; $st.Second=[uint16]$u.Second $st.Milliseconds=[uint16]$u.Millisecond [void][Win32.NativeMethods]::SetSystemTime([ref]$st) } # TZ helpers function Resolve-TzUtilPath { $candidates = @( (Join-Path $env:SystemRoot 'System32\tzutil.exe'), (Join-Path $env:WINDIR 'Sysnative\tzutil.exe') ) | Select-Object -Unique foreach ($p in $candidates) { if (Test-Path $p) { return $p } } $null } function Set-SystemTimeZone { param([Parameter(Mandatory)][string]$Id) try { if (Get-Command Set-TimeZone -ErrorAction SilentlyContinue) { Set-TimeZone -Id $Id -ErrorAction Stop return $true } } catch {} $tz = Resolve-TzUtilPath if ($tz) { $p = Start-Process -FilePath $tz -ArgumentList @('/s', $Id) -WindowStyle Hidden -Wait -PassThru return ($p.ExitCode -eq 0) } Write-Warning "No Set-TimeZone or tzutil.exe (likely specialize/WinPE)." $false } # IANA TZ detection and mapping function Get-IanaTimeZone { try { return (Invoke-RestMethod -Uri "https://ipinfo.io/json" -TimeoutSec 6 -UseBasicParsing).timezone } catch { return $null } } function Map-IanaToWindowsTz { param([Parameter(Mandatory)][string]$Iana) $xmlUrl = "https://raw.githubusercontent.com/FlorianSLZ/scloud/main/scripts/Set-TimeZoneByIPAddress/windowsZones.xml" [xml]$windowsZones = Invoke-RestMethod -Uri $xmlUrl -TimeoutSec 10 -UseBasicParsing $mapping = $windowsZones.supplementalData.windowsZones.mapTimezones.mapZone | Where-Object { ($_.type -split '\s+') -contains $Iana } if (-not $mapping) { throw "No mapping for IANA: $Iana" } $mapping.other | Select-Object -First 1 } # Orchestration if (-not (Wait-Network -TimeoutSec 60)) { Write-Warning "Network/DNS not ready; skipping time/TZ operations." return } # 1) Detect and set Windows TZ (best effort) $tzSet = $false try { Write-DarkGrayHost "Fetching IANA time zone..." $iana = Get-IanaTimeZone if ($iana) { Write-DarkGrayHost "Detected IANA TZ: $iana" $winTz = Map-IanaToWindowsTz -Iana $iana Write-DarkGrayHost "Mapped Windows TZ: $winTz" $tzSet = Set-SystemTimeZone -Id $winTz if ($tzSet) { Write-Host -ForegroundColor Yellow "Windows Time Zone set: $winTz" } else { Write-Warning "Could not set Windows Time Zone in this phase." } } else { Write-Warning "Could not detect IANA time zone." } } catch { Write-Warning "TZ step skipped: $($_.Exception.Message)" } # 2) NTP sync (UTC) then set clock try { Write-DarkGrayHost "Synchronizing system time with NTP..." $servers = @('time.windows.com','time.nist.gov','pool.ntp.org') $utc = $null foreach ($s in $servers) { try { $utc = Get-NtpUtc -Server $s -TimeoutMs 4000; break } catch { Write-DarkGrayHost "NTP failed ($s): $($_.Exception.Message)" } } if (-not $utc) { throw "All NTP servers failed." } if ($tzSet) { $local = $utc.ToLocalTime() Set-Date -Date $local -ErrorAction Stop | Out-Null Write-DarkGrayHost "Time synced. UTC=$($utc.ToString('u')) Local=$([DateTime]::Now.ToString('u'))" } else { Set-UTCSystemTime -Utc $utc Write-DarkGrayHost "Time synced (UTC set directly). UTC=$([DateTime]::UtcNow.ToString('u'))" } $skewSec = [math]::Abs(([DateTime]::UtcNow - $utc).TotalSeconds) Write-DarkGrayHost "Residual UTC skew: $([math]::Round($skewSec,2)) s" Write-SectionSuccess "Time/TZ completed" } catch { Write-Warning "Failed to sync time: $($_.Exception.Message)" } Stop-Transcript | Out-Null