Skip to content

Commit dd584e5

Browse files
authored
geolocation: add geoprobe identity to signed TWAMP replies, make verify interval configurable (#3168)
Resolves: #3166 ## Summary of Changes - Add `GeoprobePubkey` field to signed TWAMP reply packets so targets can distinguish which physical probe responded, even when multiple probes share the same signing authority - Rename `ReflectorPubkey` → `AuthorityPubkey` for clarity (reply now carries both the authority and geoprobe identities) - Replace the package-level `VerifyInterval` global with a per-reflector constructor argument, exposed as `--verify-interval` CLI flag on geoprobe-agent (default 29s) - Lower default probe interval for `geoprobe-target-sender` from 60s to 30s to fit within the verify window ## Diff Breakdown | Category | Files | Lines (+/-) | Net | |--------------|-------|-------------|------| | Core logic | 4 | +44 / -33 | +11 | | Scaffolding | 2 | +30 / -20 | +10 | | Tests | 4 | +85 / -45 | +40 | | Docs | 1 | +7 / -4 | +3 | Most change is in tests and packet structure; core logic is a small net addition. <details> <summary>Key files (click to expand)</summary> - `tools/twamp/pkg/signed/packet.go` — add GeoprobePubkey field, rename ReflectorPubkey → AuthorityPubkey, update reply size (204→236 bytes) - `tools/twamp/pkg/signed/reflector_linux.go` — store verifyInterval and geoprobePubkey as struct fields, use per-instance interval - `tools/twamp/pkg/signed/reflector.go` — remove global VerifyInterval var, add verifyInterval param to constructor - `controlplane/telemetry/cmd/geoprobe-agent/main.go` — add `--verify-interval` flag (default 29s), pass geoprobePubkey + interval to reflector - `controlplane/telemetry/cmd/geoprobe-target-sender/main.go` — lower default interval to 30s, adapt to renamed reply fields - `rfcs/rfc16-geolocation-verification.md` — update reply packet layout to reflect new GeoprobePubkey field </details> ## Testing Verification - All `tools/twamp/pkg/signed` tests pass (packet round-trip, byte layout, rate-limit, concurrent clients, signature verification) - `geoprobe-target-sender` unit tests pass - Lint clean across all changed packages
1 parent 43cd877 commit dd584e5

12 files changed

Lines changed: 164 additions & 103 deletions

File tree

controlplane/telemetry/cmd/geoprobe-agent/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
defaultTWAMPReflectorTimeout = 1 * time.Second
3333
defaultMaxOffsetAge = 1 * time.Hour
3434
defaultEvictionInterval = 30 * time.Minute
35+
defaultVerifyInterval = 29 * time.Second
3536
discoveryInterval = 60 * time.Second
3637
)
3738

@@ -43,12 +44,13 @@ var (
4344
additionalParent = flag.String("additional-parent", "", "Trusted parent DZD in the format devicekey,metricskey (base58 pubkeys).")
4445
additionalTargets = flag.String("additional-targets", "", "Comma-separated list of target addresses (host:port) to measure and send composite offsets.")
4546
twampListenPort = flag.Uint("twamp-listen-port", defaultTWAMPListenPort, "Port for TWAMP reflector.")
46-
signedTWAMPListenPort = flag.Uint("signed-twamp-port", defaultSignedTWAMPListenPort, "Port for Signed TWAMP reflector.")
47-
allowedPubkeysFlag = flag.String("allowed-pubkeys", "", "Comma-separated base58 Ed25519 pubkeys always authorized for signed TWAMP probes.")
47+
signedTWAMPListenPort = flag.Uint("signed-twamp-port", defaultSignedTWAMPListenPort, "Port for Signed TWAMP reflector for inbound probing.")
48+
allowedPubkeysFlag = flag.String("allowed-pubkeys", "", "Comma-separated base58 Ed25519 pubkeys always authorized for signed TWAMP probes in inbound probing.")
4849
udpListenPort = flag.Uint("udp-listen-port", defaultUDPListenPort, "Port for receiving DZD offset datagrams.")
4950
probeInterval = flag.Duration("probe-interval", defaultProbeInterval, "Interval between measurement cycles.")
5051
twampSenderTimeout = flag.Duration("twamp-sender-timeout", defaultTWAMPSenderTimeout, "Timeout for TWAMP probes to targets.")
5152
maxOffsetAge = flag.Duration("max-offset-age", defaultMaxOffsetAge, "TTL for cached DZD offsets.")
53+
verifyInterval = flag.Duration("verify-interval", defaultVerifyInterval, "Minimum time between signature verifications per sender for the signed TWAMP reflector in inbound probing.")
5254
verbose = flag.Bool("verbose", false, "Enable verbose logging.")
5355
showVersion = flag.Bool("version", false, "Print the version and exit.")
5456
// Set by LDFLAGS
@@ -323,11 +325,15 @@ func main() {
323325

324326
// Set up Signed TWAMP reflector.
325327
signedSigner := signed.NewEd25519Signer(ed25519.PrivateKey(keypair))
328+
var geoprobePubkeyBytes [32]byte
329+
copy(geoprobePubkeyBytes[:], geoProbePubkey[:])
326330
signedReflector, err := signed.NewReflector(
327331
fmt.Sprintf("0.0.0.0:%d", *signedTWAMPListenPort),
328332
defaultTWAMPReflectorTimeout,
329333
signedSigner,
334+
geoprobePubkeyBytes,
330335
allowedKeys,
336+
*verifyInterval,
331337
)
332338
if err != nil {
333339
log.Error("Failed to create Signed TWAMP reflector", "error", err)

controlplane/telemetry/cmd/geoprobe-target-sender/main.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import (
2222

2323
const (
2424
defaultProbePort = 8924
25-
defaultInterval = 60 * time.Second
25+
defaultInterval = 30 * time.Second
2626
defaultTimeout = 2 * time.Second
2727
)
2828

2929
var (
3030
probeIP = flag.String("probe-ip", "", "IP address of the GeoProbe to probe (required)")
3131
probePort = flag.Uint("probe-port", defaultProbePort, "TWAMP port on the probe")
32-
probePK = flag.String("probe-pk", "", "Base58 Ed25519 public key of the GeoProbe's Signing Authority (required)")
32+
probePK = flag.String("probe-pk", "", "Base58 Ed25519 public key of the GeoProbe's signing authority (required)")
3333
keypairPath = flag.String("keypair", "", "Path to this target's Ed25519 keypair file for signing outbound message (required)")
3434
interval = flag.Duration("interval", defaultInterval, "Interval between probes")
3535
count = flag.Uint("count", 0, "Number of probes to send (0 = infinite)")
@@ -207,16 +207,18 @@ func setupLogger(format string, debug bool) *slog.Logger {
207207
}
208208

209209
func logProbeResult(log *slog.Logger, seq uint32, rtt time.Duration, probeSigValid, replySigValid bool, reply *signed.ReplyPacket) {
210-
reflectorPK := solana.PublicKeyFromBytes(reply.ReflectorPubkey[:])
210+
authorityPK := solana.PublicKeyFromBytes(reply.AuthorityPubkey[:])
211+
geoprobePK := solana.PublicKeyFromBytes(reply.GeoprobePubkey[:])
211212

212213
if *logFormat == "json" {
213214
output := probeOutput{
214-
Timestamp: time.Now().UTC().Format(time.RFC3339),
215-
Seq: seq,
216-
RttMs: float64(rtt.Microseconds()) / 1000.0,
217-
ProbeSigValid: probeSigValid,
218-
ReflectorSigValid: replySigValid,
219-
ReflectorPubkey: reflectorPK.String(),
215+
Timestamp: time.Now().UTC().Format(time.RFC3339),
216+
Seq: seq,
217+
RttMs: float64(rtt.Microseconds()) / 1000.0,
218+
ProbeSigValid: probeSigValid,
219+
ReplySigValid: replySigValid,
220+
AuthorityPubkey: authorityPK.String(),
221+
GeoprobePubkey: geoprobePK.String(),
220222
}
221223
data, err := json.Marshal(output)
222224
if err != nil {
@@ -233,13 +235,14 @@ func logProbeResult(log *slog.Logger, seq uint32, rtt time.Duration, probeSigVal
233235
if !replySigValid {
234236
replySigStr = "INVALID"
235237
}
236-
fmt.Printf("[%s] seq=%d rtt=%s probe_sig=%s reflector_sig=%s probe=%s\n",
238+
fmt.Printf("[%s] seq=%d rtt=%s probe_sig=%s reflector_sig=%s authority=%s geoprobe=%s\n",
237239
time.Now().UTC().Format("2006-01-02 15:04:05 MST"),
238240
seq,
239241
formatRTT(rtt),
240242
probeSigStr,
241243
replySigStr,
242-
abbreviatePubkey(reflectorPK.String()),
244+
abbreviatePubkey(authorityPK.String()),
245+
abbreviatePubkey(geoprobePK.String()),
243246
)
244247
}
245248
}
@@ -273,13 +276,14 @@ func logProbeError(log *slog.Logger, seq uint32, probeErr error) {
273276
}
274277

275278
type probeOutput struct {
276-
Timestamp string `json:"timestamp"`
277-
Seq uint32 `json:"seq"`
278-
RttMs float64 `json:"rtt_ms"`
279-
ProbeSigValid bool `json:"probe_sig_valid,omitempty"`
280-
ReflectorSigValid bool `json:"reflector_sig_valid,omitempty"`
281-
ReflectorPubkey string `json:"reflector_pubkey,omitempty"`
282-
Error string `json:"error,omitempty"`
279+
Timestamp string `json:"timestamp"`
280+
Seq uint32 `json:"seq"`
281+
RttMs float64 `json:"rtt_ms"`
282+
ProbeSigValid bool `json:"probe_sig_valid,omitempty"`
283+
ReplySigValid bool `json:"reply_sig_valid,omitempty"`
284+
AuthorityPubkey string `json:"authority_pubkey,omitempty"`
285+
GeoprobePubkey string `json:"geoprobe_pubkey,omitempty"`
286+
Error string `json:"error,omitempty"`
283287
}
284288

285289
func formatRTT(d time.Duration) string {

controlplane/telemetry/cmd/geoprobe-target-sender/main_test.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,13 @@ func TestAbbreviatePubkey(t *testing.T) {
8989

9090
func TestProbeOutput_JSON(t *testing.T) {
9191
output := probeOutput{
92-
Timestamp: "2025-01-15T14:23:45Z",
93-
Seq: 1,
94-
RttMs: 12.534,
95-
ProbeSigValid: true,
96-
ReflectorSigValid: true,
97-
ReflectorPubkey: "FSM7abc123456zmQ",
92+
Timestamp: "2025-01-15T14:23:45Z",
93+
Seq: 1,
94+
RttMs: 12.534,
95+
ProbeSigValid: true,
96+
ReplySigValid: true,
97+
AuthorityPubkey: "FSM7abc123456zmQ",
98+
GeoprobePubkey: "ABCD1234xyz",
9899
}
99100

100101
data, err := json.Marshal(output)
@@ -113,8 +114,11 @@ func TestProbeOutput_JSON(t *testing.T) {
113114
if decoded.RttMs != 12.534 {
114115
t.Errorf("expected rtt_ms=12.534, got %f", decoded.RttMs)
115116
}
116-
if decoded.ReflectorPubkey != "FSM7abc123456zmQ" {
117-
t.Errorf("expected reflector_pubkey=FSM7abc123456zmQ, got %s", decoded.ReflectorPubkey)
117+
if decoded.AuthorityPubkey != "FSM7abc123456zmQ" {
118+
t.Errorf("expected authority_pubkey=FSM7abc123456zmQ, got %s", decoded.AuthorityPubkey)
119+
}
120+
if decoded.GeoprobePubkey != "ABCD1234xyz" {
121+
t.Errorf("expected geoprobe_pubkey=ABCD1234xyz, got %s", decoded.GeoprobePubkey)
118122
}
119123
}
120124

@@ -143,8 +147,11 @@ func TestProbeOutput_TimeoutJSON(t *testing.T) {
143147
t.Errorf("expected rtt_ms=-1, got %v", decoded["rtt_ms"])
144148
}
145149
// omitempty fields should not be present.
146-
if _, ok := decoded["reflector_pubkey"]; ok {
147-
t.Error("expected reflector_pubkey to be omitted for timeout")
150+
if _, ok := decoded["authority_pubkey"]; ok {
151+
t.Error("expected authority_pubkey to be omitted for timeout")
152+
}
153+
if _, ok := decoded["geoprobe_pubkey"]; ok {
154+
t.Error("expected geoprobe_pubkey to be omitted for timeout")
148155
}
149156
if _, ok := decoded["probe_sig_valid"]; ok {
150157
t.Error("expected probe_sig_valid to be omitted for timeout")
@@ -153,12 +160,13 @@ func TestProbeOutput_TimeoutJSON(t *testing.T) {
153160

154161
func TestProbeOutput_SuccessJSON_OmitsError(t *testing.T) {
155162
output := probeOutput{
156-
Timestamp: "2025-01-15T14:23:45Z",
157-
Seq: 1,
158-
RttMs: 5.0,
159-
ProbeSigValid: true,
160-
ReflectorSigValid: true,
161-
ReflectorPubkey: "test",
163+
Timestamp: "2025-01-15T14:23:45Z",
164+
Seq: 1,
165+
RttMs: 5.0,
166+
ProbeSigValid: true,
167+
ReplySigValid: true,
168+
AuthorityPubkey: "test",
169+
GeoprobePubkey: "test2",
162170
}
163171

164172
data, err := json.Marshal(output)

rfcs/rfc16-geolocation-verification.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,17 +351,20 @@ type SignedProbePacket struct {
351351

352352
The signature covers `[Seq, Sec, Frac, SenderPubkey]` (bytes 0–43)
353353

354-
**SignedReplyPacket (204 bytes)** — sent from Probe to Target:
354+
**SignedReplyPacket (236 bytes)** — sent from Probe to Target:
355355

356356
```go
357357
type SignedReplyPacket struct {
358358
Probe SignedProbePacket // Bytes 0-107: Complete original signed probe (echoed)
359-
ReflectorPubkey [32]byte // Bytes 108-139: Probe's Ed25519 Authority public key
360-
Signature [64]byte // Bytes 140-203: Ed25519 signature over bytes 0-139
359+
AuthorityPubkey [32]byte // Bytes 108-139: Signing authority's Ed25519 public key
360+
GeoprobePubkey [32]byte // Bytes 140-171: Geoprobe identity public key
361+
Signature [64]byte // Bytes 172-235: Ed25519 signature over bytes 0-171
361362
}
362363
```
363364

364-
The probe's signature covers `[Probe, ReflectorPubkey]` (bytes 0–139)
365+
`AuthorityPubkey` is the key used to sign and verify the reply. `GeoprobePubkey` identifies the specific geoprobe that produced the reply
366+
367+
The probe's signature covers `[Probe, AuthorityPubkey, GeoprobePubkey]` (bytes 0–171)
365368

366369
#### Interfaces
367370

tools/twamp/pkg/signed/packet.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import (
1010

1111
const (
1212
ProbePacketSize = 108
13-
ReplyPacketSize = 204
13+
ReplyPacketSize = 236
1414

1515
probePayloadSize = 44 // bytes 0-43: fields signed by sender
16-
replyPayloadSize = 140 // bytes 0-139: fields signed by reflector
16+
replyPayloadSize = 172 // bytes 0-171: fields signed by reflector
1717
)
1818

1919
var errInvalidPacket = errors.New("invalid packet format")
@@ -61,8 +61,9 @@ type ProbePacket struct {
6161
// ReplyPacket is sent from Probe to Target in the inbound probing flow.
6262
type ReplyPacket struct {
6363
Probe ProbePacket // Bytes 0-107: Complete original signed probe (echoed)
64-
ReflectorPubkey [32]byte // Bytes 108-139: Probe's Ed25519 public key
65-
Signature [64]byte // Bytes 140-203: Ed25519 signature over bytes 0-139
64+
AuthorityPubkey [32]byte // Bytes 108-139: Signing authority's Ed25519 public key
65+
GeoprobePubkey [32]byte // Bytes 140-171: Geoprobe identity public key
66+
Signature [64]byte // Bytes 172-235: Ed25519 signature over bytes 0-171
6667
}
6768

6869
func NewProbePacket(seq uint32, signer Signer) *ProbePacket {
@@ -134,8 +135,9 @@ func (r *ReplyPacket) Marshal(buf []byte) error {
134135
if err := r.Probe.Marshal(buf[0:108]); err != nil {
135136
return err
136137
}
137-
copy(buf[108:140], r.ReflectorPubkey[:])
138-
copy(buf[140:204], r.Signature[:])
138+
copy(buf[108:140], r.AuthorityPubkey[:])
139+
copy(buf[140:172], r.GeoprobePubkey[:])
140+
copy(buf[172:236], r.Signature[:])
139141
return nil
140142
}
141143

@@ -152,22 +154,25 @@ func UnmarshalReplyPacket(buf []byte) (*ReplyPacket, error) {
152154
r := &ReplyPacket{
153155
Probe: *probe,
154156
}
155-
copy(r.ReflectorPubkey[:], buf[108:140])
156-
copy(r.Signature[:], buf[140:204])
157+
copy(r.AuthorityPubkey[:], buf[108:140])
158+
copy(r.GeoprobePubkey[:], buf[140:172])
159+
copy(r.Signature[:], buf[172:236])
157160
return r, nil
158161
}
159162

160-
func NewReplyPacket(probe *ProbePacket, signer Signer) *ReplyPacket {
163+
func NewReplyPacket(probe *ProbePacket, signer Signer, geoprobePubkey [32]byte) *ReplyPacket {
161164
pub := signer.Public()
162165

163166
r := &ReplyPacket{
164-
Probe: *probe,
167+
Probe: *probe,
168+
GeoprobePubkey: geoprobePubkey,
165169
}
166-
copy(r.ReflectorPubkey[:], pub)
170+
copy(r.AuthorityPubkey[:], pub)
167171

168172
var payload [replyPayloadSize]byte
169173
_ = probe.Marshal(payload[0:108])
170-
copy(payload[108:140], r.ReflectorPubkey[:])
174+
copy(payload[108:140], r.AuthorityPubkey[:])
175+
copy(payload[140:172], r.GeoprobePubkey[:])
171176

172177
sig := signer.Sign(payload[:])
173178
copy(r.Signature[:], sig)
@@ -178,7 +183,8 @@ func NewReplyPacket(probe *ProbePacket, signer Signer) *ReplyPacket {
178183
func (r *ReplyPacket) Verify() bool {
179184
var payload [replyPayloadSize]byte
180185
_ = r.Probe.Marshal(payload[0:108])
181-
copy(payload[108:140], r.ReflectorPubkey[:])
186+
copy(payload[108:140], r.AuthorityPubkey[:])
187+
copy(payload[140:172], r.GeoprobePubkey[:])
182188

183-
return ed25519.Verify(ed25519.PublicKey(r.ReflectorPubkey[:]), payload[:], r.Signature[:])
189+
return ed25519.Verify(ed25519.PublicKey(r.AuthorityPubkey[:]), payload[:], r.Signature[:])
184190
}

0 commit comments

Comments
 (0)