Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion go/fake-ntp-server-2/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@
"drift_model": "random_walk",
"drift_ppm": 50.0,
"drift_step_ppm": 50.0,
"drift_update_interval_sec": 10
"drift_update_interval_sec": 10,

"state_file": "fake-ntpd-state.json",
"persist_state": true
}
261 changes: 199 additions & 62 deletions go/fake-ntp-server-2/fake-ntpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"math/rand"
"net"
"os"
"os/signal"
"syscall"
"time"
)

Expand All @@ -20,46 +22,84 @@ const (
)

type Config struct {
Port int `json:"port"`
Debug bool `json:"debug"`
MinPoll int `json:"min_poll"`
MaxPoll int `json:"max_poll"`
MinPrecision int `json:"min_precision"`
MaxPrecision int `json:"max_precision"`
MaxRefTimeOffset int64 `json:"max_ref_time_offset"`
RefIDType string `json:"ref_id_type"`
MinStratum int `json:"min_stratum"`
MaxStratum int `json:"max_stratum"`
LeapIndicator int `json:"leap_indicator"`
VersionNumber int `json:"version_number"`
JitterMs int `json:"jitter_ms"`
DriftModel string `json:"drift_model"`
DriftPPM float64 `json:"drift_ppm"`
DriftStepPPM float64 `json:"drift_step_ppm"`
DriftUpdateSec int `json:"drift_update_interval_sec"`
Port int `json:"port"`
Debug bool `json:"debug"`
MinPoll int `json:"min_poll"`
MaxPoll int `json:"max_poll"`
MinPrecision int `json:"min_precision"`
MaxPrecision int `json:"max_precision"`
MaxRefTimeOffset int64 `json:"max_ref_time_offset"`
RefIDType string `json:"ref_id_type"`
MinStratum int `json:"min_stratum"`
MaxStratum int `json:"max_stratum"`
LeapIndicator int `json:"leap_indicator"`
VersionNumber int `json:"version_number"`
JitterMs int `json:"jitter_ms"`
ProcessingDelayMs int `json:"processing_delay_ms"`
DriftModel string `json:"drift_model"`
DriftPPM float64 `json:"drift_ppm"`
DriftStepPPM float64 `json:"drift_step_ppm"`
DriftUpdateSec int `json:"drift_update_interval_sec"`
StateFile string `json:"state_file"`
PersistState bool `json:"persist_state"`
}

type RuntimeState struct {
BaseTime time.Time `json:"base_time"`
StartWall time.Time `json:"start_wall"`
LastUpdate time.Time `json:"last_update"`
CurrentDrift float64 `json:"current_drift"`
RandomSeed int64 `json:"random_seed"`
RequestCounter uint64 `json:"request_counter"`
}

type DriftSimulator struct {
baseTime time.Time
startWall time.Time
model string
ppm float64
stepPPM float64
updateEvery time.Duration
lastUpdate time.Time
currentDrift float64
baseTime time.Time
startWall time.Time
model string
ppm float64
stepPPM float64
updateEvery time.Duration
lastUpdate time.Time
currentDrift float64
requestCounter uint64
}

func NewDriftSimulator(cfg Config) *DriftSimulator {
return &DriftSimulator{
baseTime: time.Now(),
startWall: time.Now(),
model: cfg.DriftModel,
ppm: cfg.DriftPPM,
stepPPM: cfg.DriftStepPPM,
updateEvery: time.Duration(cfg.DriftUpdateSec) * time.Second,
lastUpdate: time.Now(),
currentDrift: cfg.DriftPPM,
baseTime: time.Now(),
startWall: time.Now(),
model: cfg.DriftModel,
ppm: cfg.DriftPPM,
stepPPM: cfg.DriftStepPPM,
updateEvery: time.Duration(cfg.DriftUpdateSec) * time.Second,
lastUpdate: time.Now(),
currentDrift: cfg.DriftPPM,
requestCounter: 0,
}
}

func NewDriftSimulatorFromState(cfg Config, state *RuntimeState) *DriftSimulator {
return &DriftSimulator{
baseTime: state.BaseTime,
startWall: state.StartWall,
model: cfg.DriftModel,
ppm: cfg.DriftPPM,
stepPPM: cfg.DriftStepPPM,
updateEvery: time.Duration(cfg.DriftUpdateSec) * time.Second,
lastUpdate: state.LastUpdate,
currentDrift: state.CurrentDrift,
requestCounter: state.RequestCounter,
}
}

func (d *DriftSimulator) GetState() *RuntimeState {
return &RuntimeState{
BaseTime: d.baseTime,
StartWall: d.startWall,
LastUpdate: d.lastUpdate,
CurrentDrift: d.currentDrift,
RequestCounter: d.requestCounter,
}
}

Expand All @@ -71,6 +111,7 @@ func (d *DriftSimulator) Now() time.Time {
}

if d.model == "random_walk" && time.Since(d.lastUpdate) >= d.updateEvery {
// Use the global random source seeded by the main function
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

This comment is incorrect since the code now uses a local rand.Rand instance passed as a parameter, not a global random source. The comment should be updated to reflect the current implementation.

Suggested change
// Use the global random source seeded by the main function
// Use the local rand.Rand instance passed as a parameter

Copilot uses AI. Check for mistakes.
delta := (rand.Float64()*2 - 1) * d.stepPPM
d.currentDrift += delta
d.lastUpdate = time.Now()
Expand Down Expand Up @@ -101,37 +142,65 @@ type NTPPacket struct {
func loadConfig(path string) Config {
file, err := os.Open(path)
if err != nil {
log.Fatalf("Kan configbestand niet openen: %v", err)
log.Fatalf("Cannot open config file: %v", err)
}
defer file.Close()

decoder := json.NewDecoder(file)
var config Config
err = decoder.Decode(&config)
if err != nil {
log.Fatalf("Fout bij inlezen configbestand: %v", err)
log.Fatalf("Error reading config file: %v", err)
}

// Validaties
// Validations
if config.LeapIndicator < 0 || config.LeapIndicator > 3 {
log.Fatalf("Ongeldige leap-indicator: %d (moet 0–3 zijn)", config.LeapIndicator)
log.Fatalf("Invalid leap indicator: %d (must be 0–3)", config.LeapIndicator)
}
if config.VersionNumber < 1 || config.VersionNumber > 7 {
log.Fatalf("Ongeldig version number: %d (moet 1–7 zijn)", config.VersionNumber)
log.Fatalf("Invalid version number: %d (must be 1–7)", config.VersionNumber)
}
if config.MinStratum < 0 || config.MaxStratum > 16 || config.MinStratum > config.MaxStratum {
log.Fatalf("Ongeldige stratum-range: %d-%d (moet 0–16 en min<=max)", config.MinStratum, config.MaxStratum)
log.Fatalf("Invalid stratum range: %d-%d (must be 0–16 and min<=max)", config.MinStratum, config.MaxStratum)
}
if config.MinPrecision > config.MaxPrecision {
log.Fatalf("Ongeldige precision-range: %d-%d", config.MinPrecision, config.MaxPrecision)
log.Fatalf("Invalid precision range: %d-%d", config.MinPrecision, config.MaxPrecision)
}
if config.MinPoll > config.MaxPoll {
log.Fatalf("Ongeldige poll-range: %d-%d", config.MinPoll, config.MaxPoll)
log.Fatalf("Invalid poll range: %d-%d", config.MinPoll, config.MaxPoll)
}

// Set defaults for new config options
if config.StateFile == "" {
config.StateFile = "fake-ntpd-state.json"
}

return config
}

func saveState(filename string, state *RuntimeState) error {
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(filename, data, 0o644)
}

func loadState(filename string) (*RuntimeState, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

var state RuntimeState
err = json.Unmarshal(data, &state)
if err != nil {
return nil, err
}

return &state, nil
}

func refIDFromType(refid string, strat uint8) uint32 {
switch strat {
case 0, 1, 16:
Expand All @@ -141,6 +210,15 @@ func refIDFromType(refid string, strat uint8) uint32 {
}
}

func refIDFromTypeWithRng(refid string, strat uint8, rng *rand.Rand) uint32 {
switch strat {
case 0, 1, 16:
return binary.BigEndian.Uint32([]byte(refid))
default:
return rng.Uint32()
}
}

func ntpTimestampParts(t time.Time) (sec uint32, frac uint32) {
unixSecs := t.Unix()
nanos := t.Nanosecond()
Expand All @@ -159,14 +237,23 @@ func parseClientInfo(req []byte) (version uint8, mode uint8, txSec uint32, txFra
return
}

func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) []byte {
func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator, rng *rand.Rand) ([]byte, time.Duration, time.Duration, time.Duration, time.Duration, time.Duration) {
drift.requestCounter++
realNow := time.Now()
now := drift.Now()
driftOffset := now.Sub(realNow)

// Apply jitter to RxTime and TxTime using deterministic random
jitter := time.Duration(rng.Intn(cfg.JitterMs*2+1)-cfg.JitterMs) * time.Millisecond
processingDelay := time.Duration(cfg.ProcessingDelayMs) * time.Millisecond
refTimeOffset := time.Duration(cfg.MaxRefTimeOffset) * time.Second

// Pas jitter toe op RxTime en TxTime
// TODO
Jitter := time.Duration(rand.Intn(cfg.JitterMs*2+1)-cfg.JitterMs) * time.Millisecond
rxTime := now.Add(Jitter).Add(-10 * time.Millisecond) // 10 ms minder dan txTime
txTime := now.Add(Jitter)
rxTime := now.Add(jitter).Add(-processingDelay) // processing delay before txTime
txTime := now.Add(jitter)

// Calculate total offset from real time in the TxTime we're sending
// (RefTime doesn't affect client sync, only TxTime matters)
totalOffset := driftOffset + jitter

refTime := now.Add(-time.Duration(cfg.MaxRefTimeOffset) * time.Second)
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

The refTime calculation uses a hardcoded negative offset while refTimeOffset variable is calculated but not used. Consider using the refTimeOffset variable for consistency: refTime := now.Add(-refTimeOffset)

Suggested change
refTime := now.Add(-time.Duration(cfg.MaxRefTimeOffset) * time.Second)
refTime := now.Add(-refTimeOffset)

Copilot uses AI. Check for mistakes.

Expand All @@ -179,11 +266,11 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) []byte
mode := uint8(4)
settings := (li << 6) | (vn << 3) | mode

precision := int8(rand.Intn(cfg.MaxPrecision-cfg.MinPrecision+1) + cfg.MinPrecision)
poll := int8(rand.Intn(cfg.MaxPoll-cfg.MinPoll+1) + cfg.MinPoll)
precision := int8(rng.Intn(cfg.MaxPrecision-cfg.MinPrecision+1) + cfg.MinPrecision)
poll := int8(rng.Intn(cfg.MaxPoll-cfg.MinPoll+1) + cfg.MinPoll)

stratum := uint8(rand.Intn(cfg.MaxStratum-cfg.MinStratum+1) + cfg.MinStratum)
refid := refIDFromType(cfg.RefIDType, stratum)
stratum := uint8(rng.Intn(cfg.MaxStratum-cfg.MinStratum+1) + cfg.MinStratum)
refid := refIDFromTypeWithRng(cfg.RefIDType, stratum, rng)

packet := NTPPacket{
Settings: settings,
Expand Down Expand Up @@ -220,17 +307,62 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) []byte
binary.BigEndian.PutUint32(buf[40:], packet.TxTimeSec)
binary.BigEndian.PutUint32(buf[44:], packet.TxTimeFrac)

return buf
return buf, totalOffset, driftOffset, jitter, processingDelay, refTimeOffset
}

func main() {
configPath := flag.String("config", "config.json", "Pad naar configbestand")
configPath := flag.String("config", "config.json", "Path to config file")
resetState := flag.Bool("reset-state", false, "Reset saved state and start fresh")
flag.Parse()
rand.Seed(time.Now().UnixNano())

cfg := loadConfig(*configPath)
driftSim := NewDriftSimulator(cfg)
//timeFormat := "2006-01-02 15:04:05 MST"

var driftSim *DriftSimulator
var globalRng *rand.Rand
var currentState *RuntimeState

// Load or create state
if cfg.PersistState && !*resetState {
if state, err := loadState(cfg.StateFile); err == nil {
log.Printf("Loaded state from %s", cfg.StateFile)
driftSim = NewDriftSimulatorFromState(cfg, state)
globalRng = rand.New(rand.NewSource(state.RandomSeed))
currentState = state
} else {
log.Printf("Could not load state (%v), starting fresh", err)
}
}

// Create fresh state if not loaded
if driftSim == nil {
seed := time.Now().UnixNano()
rand.Seed(seed)
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

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

Using the global rand.Seed() is deprecated and not thread-safe. Since you're already creating a local rand.Rand instance on line 340, you should remove this line and only use the local instance.

Suggested change
rand.Seed(seed)

Copilot uses AI. Check for mistakes.
globalRng = rand.New(rand.NewSource(seed))
driftSim = NewDriftSimulator(cfg)
currentState = driftSim.GetState()
currentState.RandomSeed = seed
log.Printf("Starting with fresh state (seed: %d)", seed)
}

// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Received shutdown signal, saving state...")
if cfg.PersistState {
state := driftSim.GetState()
state.RandomSeed = currentState.RandomSeed
if err := saveState(cfg.StateFile, state); err != nil {
log.Printf("Error saving state: %v", err)
} else {
log.Printf("State saved to %s", cfg.StateFile)
}
}
os.Exit(0)
}()

// timeFormat := "2006-01-02 15:04:05 MST"
timeFormat := "Jan _2 2006 15:04:05.00000000 (MST)"

addr := net.UDPAddr{
Expand All @@ -239,11 +371,11 @@ func main() {
}
conn, err := net.ListenUDP("udp", &addr)
if err != nil {
log.Fatalf("Kan niet luisteren op UDP %d: %v", addr.Port, err)
log.Fatalf("Cannot listen on UDP %d: %v", addr.Port, err)
}
defer conn.Close()

log.Println("Fake NTP-server gestart op poort", addr.Port)
log.Println("Fake NTP server started on port", addr.Port)

for {
buf := make([]byte, NtpPacketSize)
Expand All @@ -255,7 +387,7 @@ func main() {
version, mode, txSec, txFrac := parseClientInfo(buf)
if mode != 3 {
if cfg.Debug {
fmt.Printf("Genegeerd verzoek van %s met mode %d\n", clientAddr.IP.String(), mode)
fmt.Printf("Ignored request from %s with mode %d\n", clientAddr.IP.String(), mode)
}
continue
}
Expand All @@ -264,14 +396,19 @@ func main() {
txFloat := float64(txSec-NtpEpochOffset) + float64(txFrac)/math.Pow(2, 32)
txUnixSec := int64(txFloat)
txTime := time.Unix(txUnixSec, int64((txFloat-float64(txUnixSec))*1e9)).UTC().Format(timeFormat)
fmt.Printf("Verzoek van %s\n - NTP versie: %d\n - Client transmit timestamp: %s\n",
fmt.Printf("Request from %s (NTP v%d)\n - Client transmit timestamp: %s\n",
clientAddr.IP.String(), version, txTime)
}

resp := createFakeNTPResponse(buf, cfg, driftSim)
resp, totalOffset, driftOffset, jitter, processingDelay, refTimeOffset := createFakeNTPResponse(buf, cfg, driftSim, globalRng)
_, err = conn.WriteToUDP(resp, clientAddr)
if err != nil && cfg.Debug {
log.Printf("Fout bij versturen: %v", err)
log.Printf("Error sending: %v", err)
}

if cfg.Debug {
fmt.Printf("Response sent - Total offset: %v | Drift: %.6f ppm (%v), Jitter: %v, Processing delay: %v, RefTime offset: %v\n",
totalOffset, driftSim.currentDrift, driftOffset, jitter, processingDelay, refTimeOffset)
}
}
}