{"id":50846939,"url":"https://github.com/aykutsp/ntp-server","last_synced_at":"2026-06-14T10:34:16.265Z","repository":{"id":352440639,"uuid":"1215154868","full_name":"aykutsp/ntp-server","owner":"aykutsp","description":"Production-grade NTP server written in Go — upstream sync, rate limiting, CIDR policy, HTTP management API","archived":false,"fork":false,"pushed_at":"2026-04-19T15:25:25.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-14T10:34:11.223Z","etag":null,"topics":["access-control","clock-synchronization","distributed-systems","high-performance","infrastructure","networking","ntp","ntp-client","ntp-protocol","ntp-server","observability","rate-limiting","rfc-compliant","time-protocol","time-sync","time-synchronization"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aykutsp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T14:55:03.000Z","updated_at":"2026-04-24T07:58:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aykutsp/ntp-server","commit_stats":null,"previous_names":["aykutsp/ntp-server"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aykutsp/ntp-server","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aykutsp%2Fntp-server","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aykutsp%2Fntp-server/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aykutsp%2Fntp-server/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aykutsp%2Fntp-server/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aykutsp","download_url":"https://codeload.github.com/aykutsp/ntp-server/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aykutsp%2Fntp-server/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34318523,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-control","clock-synchronization","distributed-systems","high-performance","infrastructure","networking","ntp","ntp-client","ntp-protocol","ntp-server","observability","rate-limiting","rfc-compliant","time-protocol","time-sync","time-synchronization"],"created_at":"2026-06-14T10:34:15.055Z","updated_at":"2026-06-14T10:34:16.256Z","avatar_url":"https://github.com/aykutsp.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# NTP Server\n\n[![CI](https://github.com/aykutsp/ntp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/aykutsp/ntp-server/actions/workflows/ci.yml)\n[![Live Demo](https://github.com/aykutsp/ntp-server/actions/workflows/live-demo.yml/badge.svg)](https://github.com/aykutsp/ntp-server/actions/workflows/live-demo.yml)\n[![Go](https://img.shields.io/badge/Go-1.22%2B-00ADD8?logo=go)](https://go.dev/)\n[![License](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)\n\nA production-grade NTP server written in Go. Runs on Linux, macOS, and Windows. Designed for high-throughput environments with full RFC 4330 compliance, upstream synchronization, access control, rate limiting, and an HTTP management API.\n\n---\n\n## Live Demo\n\n**[https://aykutsp.github.io/ntp-server](https://aykutsp.github.io/ntp-server)**\n\nThe demo page shows:\n- Live UTC clock ticking in your browser\n- Stratum, Reference ID, offset and RTT from the last CI run\n- Upstream sync status\n- Copy-paste connection details and client examples for chrony, ntpd, macOS, Windows, and Python\n\nThe page is backed by a `docs/demo-result.json` file that gets written on every CI run. The workflow:\n1. Builds the Docker image and starts the container\n2. Hits `/healthz` and `/v1/status`\n3. Runs `cmd/ntp-query` against the live container\n4. Writes the NTP + status JSON into `docs/demo-result.json` and commits it\n5. GitHub Pages picks up the change and redeploys automatically\n\n[View CI workflow](https://github.com/aykutsp/ntp-server/actions/workflows/live-demo.yml) · [Trigger manually](https://github.com/aykutsp/ntp-server/actions/workflows/live-demo.yml)\n\n---\n\n## Quick Start\n\n```bash\ngit clone https://github.com/aykutsp/ntp-server.git\ncd ntp-server\ngo run ./cmd/ntp-server -config ./configs/server.example.json\n```\n\nIn a second terminal:\n\n```bash\n# Health check\ncurl http://127.0.0.1:8080/healthz\n\n# Server status\ncurl http://127.0.0.1:8080/v1/status\n\n# NTP query\ngo run ./cmd/ntp-query -server 127.0.0.1:12300\n```\n\n---\n\n## Testing with Your NTP Client\n\nYou can point any standard NTP client at this server. Below are the most common methods.\n\n### Linux — chrony\n\nEdit `/etc/chrony.conf`:\n\n```\nserver 127.0.0.1 port 12300 iburst\n```\n\nThen restart:\n\n```bash\nsudo systemctl restart chronyd\nchronyc tracking\nchronyc sources -v\n```\n\n### Linux — ntpd (ntp package)\n\nEdit `/etc/ntp.conf`:\n\n```\nserver 127.0.0.1 port 12300 iburst\n```\n\nRestart and verify:\n\n```bash\nsudo systemctl restart ntp\nntpq -p\n```\n\n### macOS — sntp (built-in)\n\n```bash\nsntp -S 127.0.0.1\n```\n\nOr with a custom port using `ntpdate`:\n\n```bash\nsudo ntpdate -u -p 4 127.0.0.1\n```\n\n### Windows — w32tm\n\n```powershell\nw32tm /config /manualpeerlist:\"127.0.0.1\" /syncfromflags:manual /reliable:YES /update\nw32tm /resync /force\nw32tm /query /status\n```\n\n### Python — ntplib\n\n```python\nimport ntplib, datetime\n\nc = ntplib.NTPClient()\n# Change host/port to your server address\nresponse = c.request('127.0.0.1', port=12300, version=4)\nprint(\"Offset :\", response.offset)\nprint(\"Stratum:\", response.stratum)\nprint(\"Time   :\", datetime.datetime.utcfromtimestamp(response.tx_time))\n```\n\nInstall with: `pip install ntplib`\n\n### Built-in query tool\n\n```bash\ngo run ./cmd/ntp-query -server 127.0.0.1:12300\n```\n\n---\n\n## How NTP Works\n\nNTP (Network Time Protocol) synchronizes clocks across a network using a hierarchical model called the **stratum system**.\n\n```\nStratum 0  ──  Atomic clocks, GPS receivers (reference clocks)\n    │\nStratum 1  ──  Servers directly connected to Stratum 0 (e.g. time.google.com)\n    │\nStratum 2  ──  Servers synced from Stratum 1  ◄── this server operates here\n    │\nStratum 3+ ──  Your devices, VMs, containers\n```\n\n### The 4-Timestamp Exchange (RFC 4330)\n\nEvery NTP transaction uses exactly four timestamps to calculate clock offset and round-trip delay:\n\n```\nClient                          Server\n  │                               │\n  │── Request ──────────────────► │  T1 = client transmit time\n  │                               │  T2 = server receive time\n  │◄─────────────────── Response ─│  T3 = server transmit time\n  │                               │\nT4 = client receive time\n\nOffset = ((T2 - T1) + (T3 - T4)) / 2\nDelay  = (T4 - T1) - (T3 - T2)\n```\n\nThe offset tells the client how far its clock is from the server. The delay accounts for asymmetric network paths.\n\n### What This Server Does\n\n1. **Listens on UDP 123 (or any configured port)** — NTP uses UDP because it is stateless and low-latency. TCP handshakes would add unacceptable jitter to time measurements.\n\n2. **Upstream synchronization** — On startup and every N seconds, the server queries multiple upstream NTP servers (Cloudflare, Google, pool.ntp.org by default). It collects samples, picks the lowest-latency candidates, and takes the median offset to reduce noise. This keeps the server's own clock accurate.\n\n3. **Packet processing** — Each incoming 48-byte UDP packet is validated (mode=3, version 3 or 4). The server fills in the four NTP timestamps and returns a 48-byte response.\n\n4. **Access control** — CIDR allow/deny lists and optional reverse-DNS policy filter which clients can receive responses.\n\n5. **Rate limiting** — A per-client token bucket and a global token bucket prevent abuse. Denied clients receive a Kiss-of-Death packet (stratum 0, reference ID = `RATE`) so well-behaved NTP clients back off automatically.\n\n6. **Kiss-of-Death (KoD)** — A standard NTP mechanism. When a client is denied, the server sends a special response with stratum=0 and a 4-byte ASCII code in the reference ID field. RFC-compliant clients stop polling when they receive this.\n\n---\n\n## Architecture for 1 Million Concurrent Clients\n\nServing 1M+ devices simultaneously requires a deliberate deployment architecture. A single process can handle a very high packet rate, but geographic distribution and redundancy are essential at this scale.\n\n### Capacity Model\n\nA single instance on a modern Linux server (8–16 cores, tuned UDP stack) can sustain roughly **50,000–200,000 requests/second** depending on NIC throughput, kernel buffer configuration, and packet size. NTP packets are tiny (48 bytes), so the bottleneck is almost always the kernel UDP receive path, not CPU.\n\nTo reach 1M concurrent devices (assuming each polls every 64–1024 seconds), the actual packet rate is modest — roughly **1,000–15,000 packets/second** — well within a single instance. The challenge is **reliability, latency, and geographic distribution**, not raw throughput.\n\n### Recommended Deployment Pattern\n\n```\n                        ┌─────────────────────────────────┐\n                        │         Anycast / GeoDNS         │\n                        │   pool.yourdomain.com  UDP 123   │\n                        └────────────┬────────────────────┘\n                                     │\n              ┌──────────────────────┼──────────────────────┐\n              │                      │                       │\n     ┌────────▼──────┐     ┌─────────▼─────┐     ┌──────────▼────┐\n     │  Region: US   │     │  Region: EU   │     │  Region: APAC │\n     │               │     │               │     │               │\n     │  node-1 :123  │     │  node-1 :123  │     │  node-1 :123  │\n     │  node-2 :123  │     │  node-2 :123  │     │  node-2 :123  │\n     │  node-3 :123  │     │  node-3 :123  │     │  node-3 :123  │\n     └───────────────┘     └───────────────┘     └───────────────┘\n              │                      │                       │\n     ┌────────▼──────────────────────▼───────────────────────▼────┐\n     │              Upstream NTP (Stratum 1)                       │\n     │   time.cloudflare.com  time.google.com  pool.ntp.org        │\n     └─────────────────────────────────────────────────────────────┘\n```\n\n### Layer-by-Layer Breakdown\n\n**1. Anycast or GeoDNS routing**\n\nAnycast assigns the same IP to multiple nodes in different regions. The network automatically routes each client to the nearest node. GeoDNS achieves similar results at the DNS layer. Both approaches reduce latency and provide transparent failover — if a node goes down, traffic shifts to the next closest node without any client-side change.\n\n**2. Regional node clusters (minimum 3 per region)**\n\nRunning at least 3 nodes per region provides:\n- Redundancy if one node loses upstream sync or crashes\n- Horizontal capacity for traffic spikes\n- Rolling restarts without downtime\n\nAll nodes in a region share identical configuration (managed via GitOps). Each node independently syncs from upstream NTP servers.\n\n**3. L4 load balancer (UDP)**\n\nWithin a region, an L4 load balancer (e.g. HAProxy, AWS NLB, or Linux IPVS) distributes UDP packets across nodes. Because NTP is stateless (each request is independent), no session affinity is needed. Consistent hashing on source IP is optional but reduces per-client jitter.\n\n**4. Worker pool per node**\n\nEach node runs a configurable number of goroutine workers sharing a single UDP socket. Workers call `ReadFromUDP` concurrently, process the packet, and call `WriteToUDP`. The kernel handles the actual socket multiplexing. Recommended worker count: `2x–6x` CPU cores, tuned by benchmarking.\n\n**5. Kernel-level UDP tuning**\n\nAt 1M+ device scale, the kernel UDP receive buffer is the most common bottleneck:\n\n```bash\n# Increase socket buffer limits\nsysctl -w net.core.rmem_max=134217728\nsysctl -w net.core.wmem_max=134217728\nsysctl -w net.core.netdev_max_backlog=65536\n\n# Increase file descriptor limits (systemd)\n# LimitNOFILE=1048576 in the service unit\n```\n\nThe server's `readBufferBytes` and `writeBufferBytes` config fields map directly to `SO_RCVBUF` / `SO_SNDBUF` on the socket.\n\n**6. Rate limiting and abuse protection**\n\nAt internet scale, a public NTP server will receive amplification attack traffic. The server's two-tier rate limiter handles this:\n- **Global bucket** — caps total packet rate across all clients\n- **Per-client bucket** — caps individual client polling rate\n- **Kiss-of-Death responses** — signals RFC-compliant clients to back off\n\n**7. Observability**\n\nEach node exposes `/v1/stats` with counters for requests, responses, rate-denied, ACL-denied, sync success/failure, and bytes in/out. Feed these into Prometheus + Grafana or any metrics pipeline to detect packet drops, upstream sync failures, or traffic anomalies in real time.\n\n### Configuration for High Scale\n\n```json\n{\n  \"ntp\": {\n    \"workers\": 32,\n    \"readBufferBytes\": 134217728,\n    \"writeBufferBytes\": 134217728,\n    \"globalRateLimitPerSecond\": 500000,\n    \"globalRateLimitBurst\": 120000,\n    \"clientRateLimitPerSecond\": 64,\n    \"clientRateLimitBurst\": 128\n  }\n}\n```\n\n---\n\n## Docker\n\n```bash\ndocker compose up --build -d\n```\n\nPorts:\n- UDP `12300` — NTP\n- TCP `8080` — HTTP management API\n\n---\n\n## Configuration\n\nReference file: [`configs/server.example.json`](./configs/server.example.json)\n\n| Field | Description |\n|---|---|\n| `ntp.listenAddress` | UDP bind address and port |\n| `ntp.workers` | Number of concurrent worker goroutines |\n| `ntp.readBufferBytes` / `writeBufferBytes` | Kernel socket buffer sizes |\n| `ntp.clientRateLimitPerSecond` | Per-client request rate cap |\n| `ntp.globalRateLimitPerSecond` | Global request rate cap |\n| `policy.allowCIDRs` / `denyCIDRs` | IP access control |\n| `policy.dns.*` | Reverse DNS policy |\n| `upstream.servers` | Upstream NTP servers to sync from |\n| `api.listenAddress` | HTTP management API bind address |\n| `api.authToken` | Optional Bearer token for API auth |\n\n---\n\n## Running as a Service\n\n### Linux (systemd)\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable ntp-server\nsudo systemctl start ntp-server\n```\n\n### macOS (launchd)\n\n```bash\nsudo cp deploy/launchd/com.ntp.server.plist /Library/LaunchDaemons/\nsudo launchctl load /Library/LaunchDaemons/com.ntp.server.plist\n```\n\n### Windows (Service)\n\n```powershell\n.\\deploy\\windows\\install-service.ps1 `\n  -BinaryPath \"C:\\Program Files\\NtpServer\\ntp-server.exe\" `\n  -ConfigPath \"C:\\ProgramData\\NtpServer\\config.json\"\n```\n\n---\n\n## API Client Library\n\nPackage: [`pkg/apiclient`](./pkg/apiclient/client.go)\n\n```go\nclient := apiclient.New(\"http://127.0.0.1:8080\", \"\", 2*time.Second)\nstatus, err := client.Status(ctx)\nfmt.Println(status.Synced, status.OffsetMillis)\n```\n\n---\n\n## Development\n\n```bash\ngo test ./... -race -count=1\ngo run ./cmd/ntp-server -print-default-config\n```\n\nMulti-platform build:\n\n```bash\n./scripts/build.sh v1.0.0      # Linux / macOS\n.\\scripts\\build.ps1 -Version v1.0.0  # Windows\n```\n\n---\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faykutsp%2Fntp-server","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faykutsp%2Fntp-server","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faykutsp%2Fntp-server/lists"}