Summary
During local testing of StayRTR, I found that a legitimate transition from a non-empty RTR dataset to an empty dataset is not represented coherently to RTR clients.
StayRTR successfully commits the empty generation and advances its internal serial, but both Reset Query and Serial Query then return Error Report: No Data (code 2) instead of serving the empty generation or the withdrawals required to transition from the previous non-empty state.
The observed sequence is:
G0: { VRP_A }
|
| normal update
v
G1: empty
|
| G1 committed
| serial advances from 0 to 1
v
Reset Query -> No Data
Serial Query -> No Data
No withdrawal for VRP_A
In a small stateful RTR client used as a local oracle, the previously learned VRP_A remained retained after the transition.
Tested revision
Commit: 4bad96343e87df9c8acbcb1d983d224c5dc114b6
Git describe: v0.6.4-1-g4bad963
All testing used valid RTR messages only. No StayRTR source modification was required.
Test scenario
The initial generation contained one VRP:
G0
ASN: AS64510
Prefix: 192.0.2.0/24
MaxLength: 24
The next generation was a valid empty RTR dataset:
G1
VRPs: 0
Router Keys: 0
ASPAs: 0
The transition was therefore:
G0 = { VRP_A }
|
v
G1 = {}
Expected behavior
A current generation containing zero RTR objects should remain distinguishable from the state where no current generation exists.
For a fresh client, an empty current generation should be represented coherently.
For a client previously synchronized to G0, the transition to G1 should allow the previously learned VRP_A to be removed, for example through the corresponding withdrawal semantics.
Conceptually:
No current generation
!=
Current generation exists, but contains zero objects
Actual behavior
1. True no-current-state control
Before any current generation was installed, a valid Reset Query returned:
Error Report
Error code: 2
Meaning: No Data
This established the baseline behavior for a genuine no-current-state condition.
2. Non-empty G0 works normally
After installing G0, a valid Reset Query returned:
Cache Response
IPv4 Prefix announcement:
AS64510 192.0.2.0/24 maxLength 24
End Of Data:
serial = 0
So the normal non-empty path worked correctly.
3. Empty G1 is committed
Without restarting StayRTR, I then performed a normal update from G0 to the empty G1.
The empty generation was committed successfully:
Object count: 0
Session: unchanged
Internal serial: 1
The serial had therefore advanced from 0 to 1, while the current dataset became empty.
At this point, StayRTR internally had a committed current generation:
Current generation: G1
Object count: 0
Serial: 1
Session: valid
4. Fresh Reset Query against G1 returns No Data
A fresh valid Reset Query against the committed empty G1 returned:
Error Report
Error code: 2
Meaning: No Data
No Cache Response was returned.
No End Of Data was returned.
Therefore, these two internally different states were externally treated the same way:
A. No current generation exists
B. A current generation exists,
has a valid session,
has an advanced serial,
but contains zero RTR objects
5. Serial Query from the previous G0 state also returns No Data
A client previously synchronized to G0 had:
Session: current StayRTR session
Serial: 0
State: { VRP_A }
After StayRTR committed empty G1 with internal serial 1, the client sent a valid Serial Query using the previous session and serial.
StayRTR returned:
Error Report
Error code: 2
Meaning: No Data
No withdrawal for VRP_A was emitted.
No coherent G0 -> G1 transition was provided.
Source-level cause
The issue appears to come from conflating current-state existence with current object count.
An empty generation can be committed into sdCurrent, and the internal serial can advance, but GetCurrentSerial() determines whether current data is valid using a condition equivalent to:
This means the implementation does not correctly distinguish:
State A:
No current generation exists
from:
State B:
Current generation exists
Valid session exists
Serial exists and has advanced
Current object count == 0
Once State B is reached:
G0 non-empty
|
v
G1 empty is accepted
|
v
G1 becomes current
|
v
serial advances from N to N+1
|
v
len(sdCurrent) == 0
|
v
GetCurrentSerial() reports no valid current data
|
+--> Reset Query -> No Data
|
+--> Serial Query -> No Data
The empty current generation therefore exists internally but cannot be represented coherently through the RTR query paths.
Stateful client observation
I also used a small local stateful RTR client as an oracle.
Before the transition, the client successfully synchronized to G0 and held:
StayRTR then transitioned normally to empty G1.
Because the subsequent Serial Query returned No Data and no withdrawal was emitted, the local stateful client still held:
To be precise:
The old-VRP retention was directly observed in the local stateful client used for this test. Production downstream behavior may vary by RTR client implementation, because different clients may react differently to an Error Report carrying No Data.
The server-side behavior does not depend on that client-specific consequence:
G1 is committed;
- the serial advances;
G1 contains zero objects;
- Reset Query returns
No Data;
- Serial Query returns
No Data;
- no withdrawal is emitted.
Minimal reproduction
-
Start StayRTR with a valid non-empty dataset containing one VRP.
-
Send a valid Reset Query and confirm:
Cache Response
-> VRP_A announcement
-> End Of Data(serial = N)
-
Record the current session and serial N.
-
Update StayRTR normally to a valid empty dataset.
-
Confirm that:
object count = 0
internal serial = N + 1
session remains valid
-
Send a fresh valid Reset Query.
-
Observe:
Error Report: No Data(code=2)
no Cache Response
no End Of Data
-
Send a valid Serial Query using the previous session and serial N.
-
Observe:
Error Report: No Data(code=2)
no withdrawal for VRP_A
Why this matters
The core issue is that these two states are semantically different:
and:
CURRENT_STATE_EXISTS_BUT_EMPTY
In the tested behavior, StayRTR has already committed the empty generation and advanced its serial, but valid RTR queries cannot retrieve a coherent representation of that generation.
For a client previously synchronized to a non-empty generation, the server also fails to provide the withdrawal transition needed to reach the empty state.
The directly demonstrated server-side impact is:
A legitimate non-empty-to-empty RTR state transition cannot be represented coherently after the empty generation has been committed.
The directly observed local downstream consequence is:
The previously learned VRP remained retained because the server returned No Data and emitted no withdrawal.
Production downstream behavior may vary by RTR client implementation.
Controls
| Case |
Current state |
Object count |
Serial |
Reset Query |
Serial Query |
Withdrawal |
| P0 |
No current generation |
N/A |
N/A |
No Data |
N/A |
N/A |
| P1 |
Non-empty G0 |
1 |
0 |
Normal full response |
Normal |
N/A |
| Target |
Committed empty G1 |
0 |
1 |
No Data |
No Data |
None |
The important differential is that the target behaves externally like the true no-current-state control even though G1 has been committed and the internal serial has advanced.
The directly demonstrated issue is the inability to represent a committed empty current generation coherently through Reset Query and Serial Query.
Possible fix direction
One possible direction would be to track current-generation existence independently from the number of RTR objects in sdCurrent.
A predicate such as:
does not distinguish:
No current generation exists
from:
A valid current generation exists and contains zero objects
A regression test could use:
and verify that:
G1 is committed.
- The serial advances.
- A fresh Reset Query can retrieve a coherent empty current generation.
- A Serial Query from
G0 can reach G1 coherently.
- The previous VRP is withdrawn.
- The empty generation is not reported as
No Data solely because its object count is zero.
Summary
During local testing of StayRTR, I found that a legitimate transition from a non-empty RTR dataset to an empty dataset is not represented coherently to RTR clients.
StayRTR successfully commits the empty generation and advances its internal serial, but both Reset Query and Serial Query then return
Error Report: No Data (code 2)instead of serving the empty generation or the withdrawals required to transition from the previous non-empty state.The observed sequence is:
In a small stateful RTR client used as a local oracle, the previously learned
VRP_Aremained retained after the transition.Tested revision
All testing used valid RTR messages only. No StayRTR source modification was required.
Test scenario
The initial generation contained one VRP:
The next generation was a valid empty RTR dataset:
The transition was therefore:
Expected behavior
A current generation containing zero RTR objects should remain distinguishable from the state where no current generation exists.
For a fresh client, an empty current generation should be represented coherently.
For a client previously synchronized to
G0, the transition toG1should allow the previously learnedVRP_Ato be removed, for example through the corresponding withdrawal semantics.Conceptually:
Actual behavior
1. True no-current-state control
Before any current generation was installed, a valid Reset Query returned:
This established the baseline behavior for a genuine no-current-state condition.
2. Non-empty G0 works normally
After installing
G0, a valid Reset Query returned:So the normal non-empty path worked correctly.
3. Empty G1 is committed
Without restarting StayRTR, I then performed a normal update from
G0to the emptyG1.The empty generation was committed successfully:
The serial had therefore advanced from
0to1, while the current dataset became empty.At this point, StayRTR internally had a committed current generation:
4. Fresh Reset Query against G1 returns No Data
A fresh valid Reset Query against the committed empty
G1returned:No
Cache Responsewas returned.No
End Of Datawas returned.Therefore, these two internally different states were externally treated the same way:
5. Serial Query from the previous G0 state also returns No Data
A client previously synchronized to
G0had:After StayRTR committed empty
G1with internal serial1, the client sent a valid Serial Query using the previous session and serial.StayRTR returned:
No withdrawal for
VRP_Awas emitted.No coherent
G0 -> G1transition was provided.Source-level cause
The issue appears to come from conflating current-state existence with current object count.
An empty generation can be committed into
sdCurrent, and the internal serial can advance, butGetCurrentSerial()determines whether current data is valid using a condition equivalent to:This means the implementation does not correctly distinguish:
from:
Once State B is reached:
The empty current generation therefore exists internally but cannot be represented coherently through the RTR query paths.
Stateful client observation
I also used a small local stateful RTR client as an oracle.
Before the transition, the client successfully synchronized to
G0and held:StayRTR then transitioned normally to empty
G1.Because the subsequent Serial Query returned
No Dataand no withdrawal was emitted, the local stateful client still held:To be precise:
The server-side behavior does not depend on that client-specific consequence:
G1is committed;G1contains zero objects;No Data;No Data;Minimal reproduction
Start StayRTR with a valid non-empty dataset containing one VRP.
Send a valid Reset Query and confirm:
Record the current session and serial
N.Update StayRTR normally to a valid empty dataset.
Confirm that:
Send a fresh valid Reset Query.
Observe:
Send a valid Serial Query using the previous session and serial
N.Observe:
Why this matters
The core issue is that these two states are semantically different:
and:
In the tested behavior, StayRTR has already committed the empty generation and advanced its serial, but valid RTR queries cannot retrieve a coherent representation of that generation.
For a client previously synchronized to a non-empty generation, the server also fails to provide the withdrawal transition needed to reach the empty state.
The directly demonstrated server-side impact is:
The directly observed local downstream consequence is:
Production downstream behavior may vary by RTR client implementation.
Controls
No DataNo DataNo DataThe important differential is that the target behaves externally like the true no-current-state control even though
G1has been committed and the internal serial has advanced.The directly demonstrated issue is the inability to represent a committed empty current generation coherently through Reset Query and Serial Query.
Possible fix direction
One possible direction would be to track current-generation existence independently from the number of RTR objects in
sdCurrent.A predicate such as:
does not distinguish:
from:
A regression test could use:
and verify that:
G1is committed.G0can reachG1coherently.No Datasolely because its object count is zero.