-
Notifications
You must be signed in to change notification settings - Fork 4
Various tweaks to fake ntpd 2 #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e9a1eb0
d6f3767
7a290bd
e4859d2
a75d5ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,8 @@ import ( | |||||
| "math/rand" | ||||||
| "net" | ||||||
| "os" | ||||||
| "os/signal" | ||||||
| "syscall" | ||||||
| "time" | ||||||
| ) | ||||||
|
|
||||||
|
|
@@ -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, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -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 | ||||||
| delta := (rand.Float64()*2 - 1) * d.stepPPM | ||||||
| d.currentDrift += delta | ||||||
| d.lastUpdate = time.Now() | ||||||
|
|
@@ -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: | ||||||
|
|
@@ -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() | ||||||
|
|
@@ -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) | ||||||
|
||||||
| refTime := now.Add(-time.Duration(cfg.MaxRefTimeOffset) * time.Second) | |
| refTime := now.Add(-refTimeOffset) |
Copilot
AI
Jul 28, 2025
There was a problem hiding this comment.
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.
| rand.Seed(seed) |
There was a problem hiding this comment.
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.