From e9a1eb08272e6848e84277c5f2ad4c5617242d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 26 Jul 2025 12:39:37 -0700 Subject: [PATCH 1/5] fake-ntp-server: translate logging to English --- go/fake-ntp-server-2/fake-ntpd.go | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/go/fake-ntp-server-2/fake-ntpd.go b/go/fake-ntp-server-2/fake-ntpd.go index b5d2f05..b470a8a 100644 --- a/go/fake-ntp-server-2/fake-ntpd.go +++ b/go/fake-ntp-server-2/fake-ntpd.go @@ -101,7 +101,7 @@ 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() @@ -109,24 +109,24 @@ func loadConfig(path string) Config { 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) } return config @@ -162,10 +162,10 @@ func parseClientInfo(req []byte) (version uint8, mode uint8, txSec uint32, txFra func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) []byte { now := drift.Now() - // Pas jitter toe op RxTime en TxTime + // Apply jitter to RxTime and 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 + rxTime := now.Add(Jitter).Add(-10 * time.Millisecond) // 10 ms less than txTime txTime := now.Add(Jitter) refTime := now.Add(-time.Duration(cfg.MaxRefTimeOffset) * time.Second) @@ -224,7 +224,7 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) []byte } func main() { - configPath := flag.String("config", "config.json", "Pad naar configbestand") + configPath := flag.String("config", "config.json", "Path to config file") flag.Parse() rand.Seed(time.Now().UnixNano()) @@ -239,11 +239,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) @@ -255,7 +255,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 } @@ -264,14 +264,14 @@ 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\n - NTP version: %d\n - Client transmit timestamp: %s\n", clientAddr.IP.String(), version, txTime) } resp := createFakeNTPResponse(buf, cfg, driftSim) _, err = conn.WriteToUDP(resp, clientAddr) if err != nil && cfg.Debug { - log.Printf("Fout bij versturen: %v", err) + log.Printf("Error sending: %v", err) } } } From d6f3767a168aa5cf974de4d66f14bc21674eadf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 26 Jul 2025 12:58:16 -0700 Subject: [PATCH 2/5] feat(logging): add drift and jitter logging to responses - Log current drift rate (ppm) and cumulative offset (ms) - Log actual jitter value applied to each response - Log reference time offset for complete timing picture - Enhanced createFakeNTPResponse to return timing values --- go/fake-ntp-server-2/fake-ntpd.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/go/fake-ntp-server-2/fake-ntpd.go b/go/fake-ntp-server-2/fake-ntpd.go index b470a8a..a183299 100644 --- a/go/fake-ntp-server-2/fake-ntpd.go +++ b/go/fake-ntp-server-2/fake-ntpd.go @@ -40,14 +40,14 @@ type Config struct { } 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 } func NewDriftSimulator(cfg Config) *DriftSimulator { @@ -159,8 +159,10 @@ 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) ([]byte, time.Duration, time.Duration) { + realNow := time.Now() now := drift.Now() + totalDriftOffset := now.Sub(realNow) // Apply jitter to RxTime and TxTime // TODO @@ -220,7 +222,7 @@ 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, totalDriftOffset, Jitter } func main() { @@ -230,7 +232,7 @@ func main() { cfg := loadConfig(*configPath) driftSim := NewDriftSimulator(cfg) - //timeFormat := "2006-01-02 15:04:05 MST" + // timeFormat := "2006-01-02 15:04:05 MST" timeFormat := "Jan _2 2006 15:04:05.00000000 (MST)" addr := net.UDPAddr{ @@ -268,10 +270,15 @@ func main() { clientAddr.IP.String(), version, txTime) } - resp := createFakeNTPResponse(buf, cfg, driftSim) + resp, driftOffset, actualJitter := createFakeNTPResponse(buf, cfg, driftSim) _, err = conn.WriteToUDP(resp, clientAddr) if err != nil && cfg.Debug { log.Printf("Error sending: %v", err) } + + if cfg.Debug { + fmt.Printf("Response sent - Drift: %.6f ppm (%.3f ms offset), Jitter: %v, RefTime offset: %d s\n", + driftSim.currentDrift, float64(driftOffset.Nanoseconds())/1e6, actualJitter, cfg.MaxRefTimeOffset) + } } } From 7a290bd5ddf1456f243f79d933f17bfe3349d92f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 26 Jul 2025 13:00:37 -0700 Subject: [PATCH 3/5] feat(config): make processing delay configurable - Add ProcessingDelayMs field to Config struct - Replace hardcoded 10ms delay with configurable value - Update debug logging to show processing delay setting - Allows simulation of different server processing times --- go/fake-ntp-server-2/fake-ntpd.go | 42 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/go/fake-ntp-server-2/fake-ntpd.go b/go/fake-ntp-server-2/fake-ntpd.go index a183299..ee3c0a0 100644 --- a/go/fake-ntp-server-2/fake-ntpd.go +++ b/go/fake-ntp-server-2/fake-ntpd.go @@ -20,23 +20,24 @@ 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"` } type DriftSimulator struct { @@ -167,7 +168,8 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byt // Apply jitter to RxTime and 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 less than txTime + processingDelay := time.Duration(cfg.ProcessingDelayMs) * time.Millisecond + rxTime := now.Add(Jitter).Add(-processingDelay) // processing delay before txTime txTime := now.Add(Jitter) refTime := now.Add(-time.Duration(cfg.MaxRefTimeOffset) * time.Second) @@ -277,8 +279,8 @@ func main() { } if cfg.Debug { - fmt.Printf("Response sent - Drift: %.6f ppm (%.3f ms offset), Jitter: %v, RefTime offset: %d s\n", - driftSim.currentDrift, float64(driftOffset.Nanoseconds())/1e6, actualJitter, cfg.MaxRefTimeOffset) + fmt.Printf("Response sent - Drift: %.6f ppm (%.3f ms offset), Jitter: %v, Processing delay: %d ms, RefTime offset: %d s\n", + driftSim.currentDrift, float64(driftOffset.Nanoseconds())/1e6, actualJitter, cfg.ProcessingDelayMs, cfg.MaxRefTimeOffset) } } } From e4859d2ef301f60ee44d262093993a08b67a2d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 26 Jul 2025 14:05:55 -0700 Subject: [PATCH 4/5] feat(logging): enhance debug output with total offset - Move NTP version inline with client IP for compact display - Calculate total time offset from real time (drift + jitter) - Log all offset components with total offset shown first - Add comment clarifying RefTime doesn't affect client sync - Provide complete visibility into timing manipulations --- go/fake-ntp-server-2/fake-ntpd.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/go/fake-ntp-server-2/fake-ntpd.go b/go/fake-ntp-server-2/fake-ntpd.go index ee3c0a0..02ab5b1 100644 --- a/go/fake-ntp-server-2/fake-ntpd.go +++ b/go/fake-ntp-server-2/fake-ntpd.go @@ -160,17 +160,23 @@ func parseClientInfo(req []byte) (version uint8, mode uint8, txSec uint32, txFra return } -func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byte, time.Duration, time.Duration) { +func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byte, time.Duration, time.Duration, time.Duration, time.Duration, time.Duration) { realNow := time.Now() now := drift.Now() - totalDriftOffset := now.Sub(realNow) + driftOffset := now.Sub(realNow) // Apply jitter to RxTime and TxTime // TODO - Jitter := time.Duration(rand.Intn(cfg.JitterMs*2+1)-cfg.JitterMs) * time.Millisecond + jitter := time.Duration(rand.Intn(cfg.JitterMs*2+1)-cfg.JitterMs) * time.Millisecond processingDelay := time.Duration(cfg.ProcessingDelayMs) * time.Millisecond - rxTime := now.Add(Jitter).Add(-processingDelay) // processing delay before txTime - txTime := now.Add(Jitter) + refTimeOffset := time.Duration(cfg.MaxRefTimeOffset) * time.Second + + 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) @@ -224,7 +230,7 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byt binary.BigEndian.PutUint32(buf[40:], packet.TxTimeSec) binary.BigEndian.PutUint32(buf[44:], packet.TxTimeFrac) - return buf, totalDriftOffset, Jitter + return buf, totalOffset, driftOffset, jitter, processingDelay, refTimeOffset } func main() { @@ -268,19 +274,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("Request from %s\n - NTP version: %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, driftOffset, actualJitter := createFakeNTPResponse(buf, cfg, driftSim) + resp, totalOffset, driftOffset, jitter, processingDelay, refTimeOffset := createFakeNTPResponse(buf, cfg, driftSim) _, err = conn.WriteToUDP(resp, clientAddr) if err != nil && cfg.Debug { log.Printf("Error sending: %v", err) } if cfg.Debug { - fmt.Printf("Response sent - Drift: %.6f ppm (%.3f ms offset), Jitter: %v, Processing delay: %d ms, RefTime offset: %d s\n", - driftSim.currentDrift, float64(driftOffset.Nanoseconds())/1e6, actualJitter, cfg.ProcessingDelayMs, cfg.MaxRefTimeOffset) + 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) } } } From a75d5eca52f757727f060f585081fee08c543bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ask=20Bj=C3=B8rn=20Hansen?= Date: Sat, 26 Jul 2025 15:23:42 -0700 Subject: [PATCH 5/5] feat(persistence): add runtime state persistence - Add RuntimeState struct for drift and random state - Save/load state to JSON file on startup/shutdown - Maintain consistent responses across restarts - Add --reset-state flag to start fresh - Use deterministic random values with saved seed --- go/fake-ntp-server-2/config.json | 5 +- go/fake-ntp-server-2/fake-ntpd.go | 176 +++++++++++++++++++++++++----- 2 files changed, 153 insertions(+), 28 deletions(-) diff --git a/go/fake-ntp-server-2/config.json b/go/fake-ntp-server-2/config.json index f496ddc..3e788a1 100644 --- a/go/fake-ntp-server-2/config.json +++ b/go/fake-ntp-server-2/config.json @@ -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 } diff --git a/go/fake-ntp-server-2/fake-ntpd.go b/go/fake-ntp-server-2/fake-ntpd.go index 02ab5b1..9703709 100644 --- a/go/fake-ntp-server-2/fake-ntpd.go +++ b/go/fake-ntp-server-2/fake-ntpd.go @@ -10,6 +10,8 @@ import ( "math/rand" "net" "os" + "os/signal" + "syscall" "time" ) @@ -38,29 +40,66 @@ type Config struct { 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, } } @@ -72,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 delta := (rand.Float64()*2 - 1) * d.stepPPM d.currentDrift += delta d.lastUpdate = time.Now() @@ -130,9 +170,37 @@ func loadConfig(path string) Config { 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: @@ -142,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() @@ -160,14 +237,14 @@ func parseClientInfo(req []byte) (version uint8, mode uint8, txSec uint32, txFra return } -func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byte, time.Duration, time.Duration, time.Duration, time.Duration, time.Duration) { +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 - // TODO - jitter := time.Duration(rand.Intn(cfg.JitterMs*2+1)-cfg.JitterMs) * time.Millisecond + // 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 @@ -189,11 +266,11 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byt 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, @@ -235,11 +312,56 @@ func createFakeNTPResponse(req []byte, cfg Config, drift *DriftSimulator) ([]byt func main() { 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) + + 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) + 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)" @@ -278,7 +400,7 @@ func main() { clientAddr.IP.String(), version, txTime) } - resp, totalOffset, driftOffset, jitter, processingDelay, refTimeOffset := 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("Error sending: %v", err)