Skip to content

feat(sam): Datagram2/Datagram3 session styles and offline signatures - #2

Open
gg582 wants to merge 5 commits into
mainfrom
feature/sam-datagram23
Open

feat(sam): Datagram2/Datagram3 session styles and offline signatures#2
gg582 wants to merge 5 commits into
mainfrom
feature/sam-datagram23

Conversation

@gg582

@gg582 gg582 commented Sep 5, 2026

Copy link
Copy Markdown
Member

Summary

Adds modern I2P datagram protocols and offline signature support to the SAM bridge.

  • SESSION CREATE accepts DATAGRAM2/DATAGRAM3 styles, mapped to protocols 19 (authenticated repliable datagrams) and 20 (unauthenticated raw-source datagrams)
  • New ModernDatagramEndpoint interface, implemented by the node/client endpoints via MarshalDatagramV2To/V3To
  • SAM private destination format supports the offline signature section: when the long-term signing private key is all zero, parses expires, transient key type/public key, the authorizing signature, and the transient private key
  • LocalDestination stores offline signature metadata and signs with the transient key; adds ImportLocalDestinationOffline
  • Inbound Datagram2 verifies the offline signature and destination hash binding; Datagram3 is unauthenticated per spec and uses the bare 32-byte hash as source

Testing

  • foundation offline signature unit tests (offline_signature_test.go)
  • SAM Datagram2/3 marshal/parse and offline key round-trip tests (datagram_modern_test.go, offline_test.go)
  • go build ./... and related package tests pass

Add DATAGRAM2/DATAGRAM3 session styles mapped to protocols 19 and 20,
extend private destination parsing with the offline signature section,
and carry the transient signing key through LocalDestination so datagram
signing works when the long-term key is kept offline.
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: a6222995-8616-4a4a-85f3-6feafcc16226


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

qodo-code-review Bot commented Sep 5, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add SAM Datagram2/3 and offline signature support

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add SAM DATAGRAM2 and DATAGRAM3 sessions using I2P protocols 19 and 20.
• Support offline-authorized transient signing across destinations, Datagram2, and LeaseSet2
 publication.
• Enforce expiry, target binding, unsupported combinations, and sensitive-key cleanup.
Diagram

graph TD
  client["SAM Client"] --> bridge["SAM Bridge"] --> session["Datagram Session"] --> endpoint["Destination Endpoint"] --> codec["Datagram Codec"] --> network["I2P Network"]
  bridge --> local["Local Destination"] --> ls2["LeaseSet2 Publisher"] --> network
Loading
High-Level Assessment

The approach is appropriate: an optional ModernDatagramEndpoint preserves compatibility with existing endpoint implementations while keeping private-key operations behind destination endpoints. Extending DestinationEndpoint directly would be unnecessarily breaking, while moving signing into SAM would leak key ownership across abstraction boundaries.

Files changed (21) +1535 / -124

Enhancement (13) +634 / -103
datagram.goRoute and verify all three datagram protocols +82/-18

Route and verify all three datagram protocols

• Dispatches outbound framing by the session protocol and supports Datagram2/3 endpoints. Inbound Datagram2 validates signatures, expiry, target binding, and source identity, while Datagram3 exposes its explicitly unauthenticated source hash. Frame overhead now accounts for each protocol and offline authorization sections.

client/internal/sam/datagram.go

private_destination.goParse and encode SAM offline private destinations +125/-35

Parse and encode SAM offline private destinations

• Recognizes an all-zero long-term signing key followed by the offline authorization and transient private key. Imports validated offline destinations, preserves the section during encoding, uses zero-copy parsing views, and clears temporary key material.

client/internal/sam/private_destination.go

protocol.goConfigure modern datagram sessions and offline-key policy +27/-11

Configure modern datagram sessions and offline-key policy

• Accepts DATAGRAM2 and DATAGRAM3 styles, maps them to their fixed protocols, and subscribes receivers accordingly. Rejects offline destinations with legacy Datagram1 or encrypted LeaseSet options and propagates offline metadata into sessions.

client/internal/sam/protocol.go

server.goInject the SAM offline-verification clock +6/-0

Inject the SAM offline-verification clock

• Adds an optional server clock used for deterministic offline signature expiry checks, defaulting to the system clock.

client/internal/sam/server.go

session.goModel modern datagram session state +28/-7

Model modern datagram session state

• Adds DATAGRAM2/3 styles, style-to-protocol helpers, offline authorization metadata, and an injected clock. Root and child sessions calculate protocol-specific framing overhead.

client/internal/sam/session.go

udp.goEnable Datagram2/3 over SAM UDP forwarding +4/-5

Enable Datagram2/3 over SAM UDP forwarding

• Accepts modern datagram session styles in UDP parsing, applies protocol-specific payload limits, and marshals UDP payloads with the selected datagram protocol.

client/internal/sam/udp.go

address_generator.goAdd transient signing to LocalDestination +95/-12

Add transient signing to LocalDestination

• Stores, clones, and securely releases offline signing state. Adds validated offline destination import and uses authorized transient keys for signing while rejecting expired authorizations and clearing expanded key copies.

foundation/address_generator.go

offline_signature.goIntroduce canonical offline signature support +182/-0

Introduce canonical offline signature support

• Defines offline authorization metadata, bounded signed-content serialization, transient key validation, metadata accessors, private-section serialization, expiry errors, and secure cleanup helpers.

foundation/offline_signature.go

destination_interface.goDefine the optional modern datagram endpoint interface +6/-0

Define the optional modern datagram endpoint interface

• Adds Datagram2 and Datagram3 marshalling methods without expanding the required DestinationEndpoint contract.

interfaces/destination/destination_interface.go

modern.goUnify Datagram2 offline authorization metadata +9/-11

Unify Datagram2 offline authorization metadata

• Aliases the datagram offline signature type to the foundation representation and exports the offline flag. Keeps parsed authorization content internal while retaining expiry, authorization, target-binding, and payload-signature verification.

networking/internal/datagram/modern.go

local_ls2.goPublish offline-authorized LeaseSet2 records +42/-4

Publish offline-authorized LeaseSet2 records

• Carries offline authorization metadata into LeaseSet2 serialization, sets the offline flag, and signs with the transient key type. Publication is rejected after authorization expiry or when metadata lengths are invalid.

networking/internal/netdb/local_ls2.go

networking_subsystem.goExport modern datagram APIs and constants +6/-0

Export modern datagram APIs and constants

• Exposes Datagram2/3 protocol identifiers, the offline flag and signature type, and modern marshal functions through the public networking facade.

networking/networking_subsystem.go

client_destination.goImplement modern datagram marshalling for node endpoints +22/-0

Implement modern datagram marshalling for node endpoints

• Serializes authenticated Datagram2 packets with target binding and optional offline authorization. Serializes unsigned Datagram3 packets using the local destination hash and validates endpoint lifecycle state.

node/internal/runtime/client_destination.go

Bug fix (1) +5 / -0
local_encrypted_ls2.goReject encrypted LeaseSets for offline destinations +5/-0

Reject encrypted LeaseSets for offline destinations

• Prevents blinding attempts that require the unavailable long-term signing private key.

networking/internal/netdb/local_encrypted_ls2.go

Tests (7) +896 / -21
datagram_modern_test.goTest modern SAM datagram sessions and validation +294/-0

Test modern SAM datagram sessions and validation

• Covers style parsing, protocol mapping, frame overhead, Datagram1/2/3 round trips, forged Datagram2 rejection, RAW restrictions, and endpoints lacking modern serialization support.

client/internal/sam/datagram_modern_test.go

offline_test.goTest SAM offline destination behavior +230/-0

Test SAM offline destination behavior

• Exercises offline Datagram2 round trips, expired and forged authorizations, private destination echoing, legacy Datagram rejection, Datagram3 allowance, and encrypted LeaseSet rejection.

client/internal/sam/offline_test.go

server_test.goExtend loop endpoints and stabilize asynchronous assertions +48/-16

Extend loop endpoints and stabilize asynchronous assertions

• Implements Datagram2/3 marshalling in the loopback endpoint. Updates live loopback tests to accept either valid ordering of status and received frames.

client/internal/sam/server_test.go

udp_test.goUpdate datagram capacity test for generalized overhead +1/-1

Update datagram capacity test for generalized overhead

• Uses the protocol-aware overhead calculator when testing exact Datagram1 payload capacity.

client/internal/sam/udp_test.go

offline_signature_test.goTest offline authorization lifecycle and serialization +201/-0

Test offline authorization lifecycle and serialization

• Verifies transient signing, cloning, release behavior, expiry enforcement, forgery rejection, key matching, absent long-term keys, and private-section encoding.

foundation/offline_signature_test.go

local_ls2_offline_test.goTest offline LeaseSet2 publication constraints +118/-0

Test offline LeaseSet2 publication constraints

• Verifies offline sections and signatures in parsed LeaseSet2 records, expired-publication rejection, and encrypted LeaseSet incompatibility.

networking/internal/netdb/local_ls2_offline_test.go

manager_allocation_test.goIncrease SSU2 allocation test timing tolerance +4/-4

Increase SSU2 allocation test timing tolerance

• Extends handshake and delivery waits to reduce failures in slower test environments without changing production behavior.

networking/internal/router/manager_allocation_test.go

@qodo-code-review

qodo-code-review Bot commented Sep 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📜 Skill insights (1)

Grey Divider


Action required

1. Looped SAM connections leak on failure 📜 Skill insight
Description
Each loop iteration creates a control connection, but control.Close() is reached only on the
success path; a failed readSAMLine or assertion exits via t.Fatalf and leaves that connection
open. This violates the requirement that tests clean up every resource they create.
Code

client/internal/sam/datagram_modern_test.go[R284-288]

+		_, _ = io.WriteString(control, "SESSION CREATE STYLE="+style+" ID="+id+" DESTINATION=TRANSIENT\n")
+		if line := readSAMLine(t, reader); !strings.Contains(line, "RESULT=OK") {
+			t.Fatalf("%s create = %q", style, line)
+		}
+		_, _ = io.WriteString(control, "DATAGRAM SEND ID="+id+" DESTINATION=peer.i2p SIZE=4\nDATA")
Evidence
The changed test code creates control inside a loop at line 284 and closes it at line 288, but
earlier failure paths in the same iteration call t.Fatalf before reaching the close. The checklist
requires all test-created connections and resources to be cleaned up before return.

client/internal/sam/datagram_modern_test.go[284-288]
Skill: just-good-practices

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The loop creates a SAM control connection but closes it only after all assertions succeed, so test failures can leak connections and associated session resources.

## Issue Context
Use per-iteration cleanup that runs when `t.Fatalf` aborts the current test iteration. Preserve the distinct session IDs introduced to avoid asynchronous teardown races.

## Fix Focus Areas
- client/internal/sam/datagram_modern_test.go[284-288]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Offline LeaseSets outlive authorization 🐞 Bug
Description
The offline expiry check rejects only a publication whose published time is already expired, then
allows leases whose end time extends past the transient-key authorization. Remote LS2 verification
and database retention do not enforce Offline.Expires, so that LeaseSet remains usable until its
normal lease expiry after the offline authorization has expired.
Code

networking/internal/netdb/local_ls2.go[R126-130]

+	if offline != nil {
+		// Peers verify the LS2 signature with the transient key authorized by
+		// this offline signature; publishing past its expiry is useless.
+		if published > uint64(offline.Expires) {
+			return 0, ErrLocalLeaseSet2
Evidence
The new serializer checks only the publication timestamp, then derives the LeaseSet expiry from the
latest lease without applying the offline expiry. The receiving verification and storage paths
validate signatures and ordinary lease ranges only, so they retain the entry through its normal
expiry.

networking/internal/netdb/local_ls2.go[123-155]
networking/internal/netdb/structures.go[542-556]
networking/internal/netdb/database.go[291-329]
networking/internal/netdb/database.go[437-455]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Offline-signed LeaseSet2 payloads can advertise lease validity beyond the offline authorization expiry. The local serializer checks only whether publication begins after expiry, while receivers retain a signature-valid LeaseSet based on ordinary lease dates.

## Issue Context
Ensure an offline signature limits the complete validity period of a LeaseSet signed with its transient key. Cap or reject local lease ranges extending beyond `offline.Expires`, and enforce the same bound during remote LS2 admission/validation so externally produced payloads cannot bypass it.

## Fix Focus Areas
- networking/internal/netdb/local_ls2.go[126-155]
- networking/internal/netdb/database.go[291-329]
- networking/internal/netdb/structures.go[542-556]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Offline primary permits Datagram1 child 🐞 Bug
Description
An offline STYLE=PRIMARY session can add a legacy STYLE=DATAGRAM child because addSubsession
reuses the offline root endpoint without applying the new Datagram1 restriction. Outbound packets
are then signed by the transient key, but Datagram1 cannot carry its authorization, so peers reject
every packet.
Code

client/internal/sam/protocol.go[275]

+	child := &samSession{server: s, root: root, id: id, style: style, endpoint: root.endpoint, control: connection, ctx: ctx, cancel: cancel, sourceIP: root.sourceIP, fromPort: fromPort, toPort: toPort, listenPort: listenPort, protocol: protocol, listenProtocol: listenProtocol, rawHeader: rawHeader, udpTarget: udpTarget, now: root.now, children: make(map[string]*samSession), attachments: make(map[net.Conn]struct{}), queueBytes: newByteBudget(s.config.MaxSessionQueueBytes), acceptRequests: make(chan acceptRequest, s.config.SessionQueue)}
Evidence
The new creation check rejects an offline destination only when the root style itself is legacy
DATAGRAM. Subsession creation still reuses root.endpoint and accepts a DATAGRAM child under
PRIMARY; DATAGRAM maps to protocol 18, whose outbound path calls MarshalDatagramV1To, while the
code explicitly documents that Datagram1 cannot carry the offline authorization section.

client/internal/sam/protocol.go[148-162]
client/internal/sam/protocol.go[259-285]
client/internal/sam/session.go[27-39]
client/internal/sam/datagram.go[123-132]
foundation/address_generator.go[305-318]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Offline PRIMARY sessions can add a legacy DATAGRAM subsession, bypassing the root-session validation. Reject such subsessions because Datagram1 cannot transmit the offline authorization needed to verify transient-key signatures.

## Issue Context
The child reuses the root endpoint and therefore signs with the offline transient key. DATAGRAM2 supports the authorization section and DATAGRAM3 is unsigned, so only legacy DATAGRAM must be rejected.

## Fix Focus Areas
- client/internal/sam/protocol.go[275-275]
- client/internal/sam/offline_test.go[168-230]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Sign reads wall clock ✓ Resolved 📜 Skill insight
Description
LocalDestination.Sign directly calls time.Now() to enforce offline-key expiry, making a
security-sensitive decision depend on an implicit clock and preventing deterministic boundary
testing. Inject a clock or explicit timestamp and use fixed test times.
Code

foundation/address_generator.go[R306-308]

+		if uint32(time.Now().Unix()) > d.offline.expires {
+			return nil, ErrOfflineSignatureExpired
+		}
Evidence
Rule 3091256 requires functions that depend on current time to accept a clock or timestamp and
requires tests to provide explicit times. LocalDestination.Sign instead obtains the current Unix
time from time.Now() while deciding whether the transient signing key is expired.

foundation/address_generator.go[305-308]
foundation/offline_signature_test.go[97-109]
Skill: testing-practices

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Offline-signature expiry validation reads the system wall clock directly, preventing deterministic testing of expiry boundaries.

## Issue Context
PR Compliance ID 3091256 requires current time to be supplied explicitly. The newly added expiry test also derives its input from `time.Now()` rather than a fixed instant.

## Fix Focus Areas
- foundation/address_generator.go[306-308]
- foundation/offline_signature_test.go[97-113]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Offline keys break Datagram1 ✓ Resolved 🐞 Bug
Description
STYLE=DATAGRAM remains accepted for an offline private destination, but Datagram1 signs using the
transient key and carries neither its public key nor its authorization; peers verify against the
long-term identity key and drop every packet. Only Datagram2 has the offline-signature wire format
required by this new signing behavior.
Code

foundation/address_generator.go[R305-312]

+	if d.offline != nil {
+		switch d.offline.keyType {
+		case SigningEdDSASHA512Ed25519:
+			return ed25519.Sign(ed25519.NewKeyFromSeed(d.offline.private), message), nil
+		case SigningRedDSASHA512Ed25519:
+			var private [32]byte
+			copy(private[:], d.offline.private)
+			return Red25519Sign(private, message)
Evidence
The PR imports the offline key and switches all Sign calls to its transient private key. Legacy
DATAGRAM still maps to Datagram1 and delegates to the endpoint's V1 marshaler, while Datagram1
verifies its signature using the identity included in the packet and has no offline-key section.

foundation/address_generator.go[296-325]
client/internal/sam/private_destination.go[123-135]
client/internal/sam/private_destination.go[171-190]
client/internal/sam/session.go[27-39]
client/internal/sam/datagram.go[123-132]
node/internal/runtime/client_destination.go[183-207]
networking/internal/datagram/datagram.go[72-151]
networking/internal/datagram/modern.go[73-143]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An imported offline destination has no long-term signing private key, so its `Sign` method correctly uses the transient key. However, legacy `STYLE=DATAGRAM` selects Datagram1, whose wire format cannot include the transient public key and long-term authorization; the receiver verifies the transient signature with the destination's long-term identity key and rejects it.

## Issue Context
DATAGRAM2 supports an offline section and is compatible with offline destinations. Reject offline private destinations for legacy DATAGRAM sessions (or otherwise prevent their DATAGRAM sends) with a clear invalid-key/style result; retain support for DATAGRAM2 and DATAGRAM3 as appropriate.

## Fix Focus Areas
- foundation/address_generator.go[305-315]
- client/internal/sam/private_destination.go[126-132]
- client/internal/sam/protocol.go[99-159]
- client/internal/sam/session.go[27-39]
- client/internal/sam/datagram.go[123-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Encrypted offline sessions cannot publish ✓ Resolved 🐞 Bug
Description
createSession accepts an offline destination together with encrypted LeaseSet options, although
its long-term private key is deliberately zero and EncryptedLeaseSetBlinding requires that key.
The runtime consequently cannot derive the daily blinded signing key, so the destination cannot
publish its encrypted LeaseSet or become usable.
Code

client/internal/sam/protocol.go[R148-151]

+	var offline *foundation.OfflineSignature
+	if meta, ok := local.OfflineSignature(); ok {
+		meta := meta
+		offline = &meta
Evidence
SAM policy parsing can mark an imported destination as encrypted, while offline import enforces a
zero long-term signing-private slot. The encrypted publisher calls EncryptedLeaseSetBlinding,
which still uses that zero private key; the official specification states that encrypted LeaseSets
with offline keys require separately pre-generated daily blinded keys and that no I2CP enhancement
exists to provide them.

client/internal/sam/protocol.go[340-411]
foundation/address_generator.go[348-368]
foundation/address_generator.go[535-543]
networking/internal/netdb/local_encrypted_ls2.go[92-109]
node/internal/runtime/destination_runtime.go[520-527]
🌐 The encrypted LeaseSet specification says offline deployments must generate blinded private keys offline for each day, and that no I2CP protocol enhancement is defined for delivering those keys.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
SAM accepts offline private destinations with encrypted LeaseSet policy, but the supplied private format contains no daily pre-generated blinded keys. Publication subsequently fails because blinding is attempted with the zeroed long-term private key.

## Issue Context
The encrypted LeaseSet specification requires blinded private keys to be generated offline for each day and defines no I2CP mechanism for supplying them. Reject this combination during `SESSION CREATE` unless a complete pre-generated blinded-key transport and publication implementation is added.

## Fix Focus Areas
- client/internal/sam/protocol.go[121-159]
- client/internal/sam/protocol.go[340-411]
- foundation/address_generator.go[348-368]
- foundation/address_generator.go[535-543]
- networking/internal/netdb/local_encrypted_ls2.go[92-109]
- node/internal/runtime/destination_runtime.go[520-527]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Datagram verification reads wall clock ✓ Resolved 📜 Skill insight
Description
parseReceivedDatagram invokes VerifyTarget, which reads time.Now() internally when enforcing
offline-signature expiry. This makes security-sensitive expiry behavior dependent on an implicit,
untestable global clock.
Code

client/internal/sam/datagram.go[178]

+		valid, err := packet.V2.VerifyTarget(s.endpoint.Hash())
Evidence
Rule 3091256 requires current time to be injected explicitly. The changed receive path calls
VerifyTarget, whose implementation directly evaluates time.Now().Unix(), even though
VerifyTargetAt supports explicit time.

client/internal/sam/datagram.go[165-182]
networking/internal/datagram/modern.go[116-124]
Skill: testing-practices

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Datagram2 offline-signature expiry verification reads the wall clock implicitly through `VerifyTarget`.

## Issue Context
The networking API already provides `VerifyTargetAt`, which accepts an explicit Unix timestamp. Supply the current time through an injected clock or timestamp dependency and use deterministic values in tests.

## Fix Focus Areas
- client/internal/sam/datagram.go[165-182]
- networking/internal/datagram/modern.go[116-124]
- client/internal/sam/offline_test.go[58-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Offline destinations cannot publish ✓ Resolved 🐞 Bug
Description
Offline destinations sign LeaseSet2 payloads with the transient key, but production LocalLeaseSet2
publication emits flags zero and no offline authorization section, so receivers verify against the
destination's long-term identity key and reject the LeaseSet. As a result, SAM sessions using
offline private destinations cannot publish usable inbound leases through the real runtime endpoint,
while loopback tests miss the failure by bypassing production LeaseSet construction and publication.
Code

foundation/address_generator.go[R305-308]

+	if d.offline != nil {
+		switch d.offline.keyType {
+		case SigningEdDSASHA512Ed25519:
+			return ed25519.Sign(ed25519.NewKeyFromSeed(d.offline.private), message), nil
Evidence
The PR changes LocalDestination.Sign to return a transient-key signature for offline destinations,
and the production destination runtime passes that signer into normal LocalLeaseSet2 publication.
Its serializer writes Flags=0 and includes no offline metadata, while the verifier switches to the
transient public key only when an offline authorization block is present; it therefore retains the
long-term identity key and cannot validate the signature. The added SAM test uses loopController,
so it bypasses the affected production LeaseSet construction and publication path.

foundation/address_generator.go[296-325]
node/internal/runtime/destination_runtime.go[506-529]
networking/internal/netdb/local_ls2.go[134-175]
networking/internal/netdb/structures.go[325-396]
networking/internal/netdb/structures.go[542-555]
node/internal/runtime/destination_runtime.go[506-517]
networking/internal/netdb/local_ls2.go[17-24]
networking/internal/netdb/local_ls2.go[94-175]
client/internal/sam/offline_test.go[58-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Offline `LocalDestination.Sign` uses the transient private key, but `LocalLeaseSet2` still serializes a normal LS2 header with flags zero and no offline authorization block. Remote peers consequently verify the published signature against the long-term destination identity key and reject the LeaseSet2.

## Issue Context
The production runtime constructs a `LocalLeaseSet2` and passes `destination.Sign` to the LeaseSet publisher. The existing LS2 parser and verifier already understand the offline flag and authorization section, so local LS2 construction, sizing, and serialization need to retain and emit the destination's offline metadata, set the offline flag, use the transient signature length and signing key only with that compatible format, and reject expired authorization before publication. Review encrypted LeaseSet publication as well because it uses the same destination and performs long-term-key blinding; the added SAM loopback test does not exercise this production construction and publication path.

## Fix Focus Areas
- foundation/address_generator.go[305-315]
- foundation/offline_signature.go[104-115]
- networking/internal/netdb/local_ls2.go[17-24]
- networking/internal/netdb/local_ls2.go[94-175]
- networking/internal/netdb/local_encrypted_ls2.go[247-266]
- networking/internal/netdb/structures.go[326-396]
- networking/internal/netdb/structures.go[542-555]
- node/internal/runtime/destination_runtime.go[506-529]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

9. Test leaves transient key unwiped ✓ Resolved 📘 Rule violation
Description
offlineTestDestination retains the expanded Ed25519 private key returned as transientFull
without clearing it, while only its derived seed is wiped. The private-key buffer remains in memory
until garbage collection.
Code

networking/internal/netdb/local_ls2_offline_test.go[32]

+	transientPublic, transientFull, err := ed25519.GenerateKey(rand.Reader)
Evidence
Compliance rule 3087786 requires complete private-key buffers to be explicitly zeroized before
release. The helper creates transientFull, derives and clears a separate seed, but never clears
the expanded private key itself.

Rule 3087786: Zeroize sensitive cryptographic material before releasing it
networking/internal/netdb/local_ls2_offline_test.go[32-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test helper leaves the expanded Ed25519 private key returned by `ed25519.GenerateKey` in memory after use.

## Issue Context
`transientFull.Seed()` creates a separate seed buffer, so clearing only `transientPrivate` does not wipe `transientFull`.

## Fix Focus Areas
- networking/internal/netdb/local_ls2_offline_test.go[32-37]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
10. Sign leaves expanded key ✓ Resolved 📘 Rule violation
Description
The offline Ed25519 signing path creates a 64-byte expanded private key with
ed25519.NewKeyFromSeed and releases it without clearing it. Repeated Datagram2 signing can
therefore leave transient private-key material in released memory.
Code

foundation/address_generator.go[308]

+			return ed25519.Sign(ed25519.NewKeyFromSeed(d.offline.private), message), nil
Evidence
Rule 3087786 requires private-key buffers to be overwritten before scope termination or release. The
new code constructs an expanded private key inline, leaving no reference through which it can be
wiped after signing.

Rule 3087786: Zeroize sensitive cryptographic material before releasing it
foundation/address_generator.go[305-315]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The offline signing path creates an expanded Ed25519 private key without zeroizing it after signing.

## Issue Context
Store the expanded key in a local variable, defer an explicit full-length wipe before invoking `ed25519.Sign`, and similarly wipe copied temporary private material in other transient-key branches.

## Fix Focus Areas
- foundation/address_generator.go[305-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Networking exposes duplicate signature type ✓ Resolved 📘 Rule violation
Description
The PR publicly aliases networking's separate datagram.OfflineSignature even though it also
introduces the canonical foundation.OfflineSignature. This preserves duplicate wire
representations and forces higher layers to manually translate identical signature metadata.
Code

networking/networking_subsystem.go[20]

+	DatagramOfflineSignature                           = datagram.OfflineSignature
Evidence
Rule 3087671 designates foundation as the sole location for shared signature wire structures. The PR
adds foundation.OfflineSignature but also exports the structurally duplicate networking-internal
type as DatagramOfflineSignature.

Rule 3087671: Define shared wire-structure types only in the foundation (L2) layer
foundation/offline_signature.go[11-19]
networking/internal/datagram/modern.go[23-30]
networking/networking_subsystem.go[19-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Networking exposes a second wire-level offline-signature representation instead of reusing the foundation type.

## Issue Context
Offline signature wire structures belong to foundation. Replace the networking-internal duplicate or adapt networking APIs to accept `foundation.OfflineSignature`, removing field-by-field translations in endpoint implementations.

## Fix Focus Areas
- networking/networking_subsystem.go[19-21]
- networking/internal/datagram/modern.go[23-30]
- foundation/offline_signature.go[11-19]
- node/internal/runtime/client_destination.go[201-207]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. SignedContent allocates destination buffer ✓ Resolved 📘 Rule violation
Description
OfflineSignature.SignedContent serializes wire fields into a newly allocated slice rather than
caller-provided fixed-capacity storage. Callers cannot detect insufficient capacity or reuse bounded
storage as required for serializers.
Code

foundation/offline_signature.go[R23-27]

+func (o OfflineSignature) SignedContent() []byte {
+	signed := make([]byte, 6+len(o.PublicKey))
+	binary.BigEndian.PutUint32(signed[:4], o.Expires)
+	binary.BigEndian.PutUint16(signed[4:6], uint16(o.Type))
+	copy(signed[6:], o.PublicKey)
Evidence
Rule 3087734 requires serializers to write only into caller-provided capacity. The newly added
method allocates signed itself and writes the serialized expiry, type, and public key into that
new buffer.

Rule 3087734: Serializers must not grow destination buffers beyond caller-provided capacity
foundation/offline_signature.go[21-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SignedContent` allocates its own serialization destination with `make`.

## Issue Context
Expose an encoded-length operation and a bounded `MarshalSignedContentTo(dst []byte)` method that returns an insufficient-capacity error without growing or replacing `dst`. Update signing and verification callers to supply fixed-capacity storage.

## Fix Focus Areas
- foundation/offline_signature.go[21-29]
- foundation/offline_signature.go[77-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Offline parser copies wire fields ✓ Resolved 📘 Rule violation
Description
parseOfflinePrivateKey heap-allocates copies of the public key, signature, and transient private
key instead of returning views into section. The caller retains the input buffer through parsing
and immediately imports the result, so these undocumented copies violate the zero-allocation parser
requirement.
Code

client/internal/sam/private_destination.go[R222-225]

+	public := append([]byte(nil), section[offset:offset+publicLength]...)
+	offset += publicLength
+	signature := append([]byte(nil), section[offset:offset+signatureLength]...)
+	offset += signatureLength
Evidence
Rule 3087711 requires wire parsers to expose subranges as views without heap allocation. Each
append([]byte(nil), section[...]...) in the new parser allocates and copies a field from the input
buffer.

Rule 3087711: Wire parsers must not heap-allocate while creating views over input buffers
client/internal/sam/private_destination.go[205-235]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The offline private-key wire parser copies parsed subranges into newly allocated slices.

## Issue Context
`decodePrivateDestination` keeps the decoded wire buffer alive until import completes, and the foundation import path performs any ownership copies it requires. Use slices over `section` in the parser unless an owning copy is explicitly required and documented.

## Fix Focus Areas
- client/internal/sam/private_destination.go[205-235]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 86 rules
✅ Skills: 9 invoked
  api-design
  code-health
  code-review
  cpp-practices
  java-practices
  just-good-practices
  shell-practices
  testing-practices
  zero-slop
Review mode: ⚖️ Balanced: The push changes runtime SAM session validation and offline LeaseSet expiry behavior, creating genuine protocol and signature-validity risk, but the logic is localized enough for one careful review pass.
ⓘ  1 issues published inline · 0 in summary

Grey Divider

Comment thread client/internal/sam/datagram.go Outdated
Comment thread client/internal/sam/private_destination.go Outdated
Comment thread networking/networking_subsystem.go
Comment thread foundation/address_generator.go Outdated
Comment thread foundation/offline_signature.go Outdated
Comment thread client/internal/sam/protocol.go
Comment thread foundation/address_generator.go Outdated
Comment thread foundation/address_generator.go
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for the first posted P1 finding and linked it to the corresponding review comment.
  • T-Rex produced a proof for the second posted P1 finding and linked it to the corresponding review comment.
  • T-Rex conducted general contract validation for the Offline LeaseSet2 regression by running the authored test, capturing baseline and after outputs, and collecting source evidence.
  • T-Rex prepared and organized artifacts including the test source, the test runner script, and the LeaseSet2 verification logs to support the proofs.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Offline LeaseSet2 publications omit transient-key authorization and fail peer verification

    • Bug
      • A LeaseSet2 made with an offline LocalDestination serializes with Flags=0 and no Offline section, while its final signature is made by the authorized transient key. On parsing, LeaseSet2.Verify treats it as a normal LS2 and verifies that signature with the destination long-term public key, returning false.
    • Cause
      • LocalDestination.Sign selects d.offline.private at foundation/address_generator.go:305-315, but LocalLeaseSet2.MarshalTo at networking/internal/netdb/local_ls2.go:134-145 has no offline metadata input, reserves no authorization bytes, and writes a literal zero flags word. Consequently the verifier’s offline branch in networking/internal/netdb/structures.go:547-553 is never reached.
    • Fix
      • Make LocalLeaseSet2 retain or receive offline authorization metadata, set leaseSetOfflineFlag, serialize expires/type/transient public key/long-term authorization signature before options, size the output and final signature according to the transient signing type, and continue signing the complete StoreLeaseSet2-prefixed payload with the transient private key.

    T-Rex Ran code and verified through T-Rex

Prompt To Fix All With AI
### Issue 1
foundation/address_generator.go:305-315
**Offline LeaseSets Are Rejected**

Offline destinations now sign LeaseSet2 payloads with their transient signing key, but LeaseSet2 publication still emits zero flags and no offline authorization. Receiving peers consequently verify the transient-key signature with the destination’s long-term public key and reject the LeaseSet2, leaving the offline SAM destination unreachable. Serialize the offline authorization, set the offline flag, and size and sign the LeaseSet2 using the transient signature type before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(sam): support Datagram2/Datagram3 a..." | Re-trigger Greptile

Comment on lines +305 to +315
if d.offline != nil {
switch d.offline.keyType {
case SigningEdDSASHA512Ed25519:
return ed25519.Sign(ed25519.NewKeyFromSeed(d.offline.private), message), nil
case SigningRedDSASHA512Ed25519:
var private [32]byte
copy(private[:], d.offline.private)
return Red25519Sign(private, message)
default:
return nil, ErrEncryptedSigningKey
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Offline LeaseSets Are Rejected

Offline destinations now sign LeaseSet2 payloads with their transient signing key, but LeaseSet2 publication still emits zero flags and no offline authorization. Receiving peers consequently verify the transient-key signature with the destination’s long-term public key and reject the LeaseSet2, leaving the offline SAM destination unreachable. Serialize the offline authorization, set the offline flag, and size and sign the LeaseSet2 using the transient signature type before merging.

Artifacts

Evidence from the check

  • Temporary test authored for this validation; it creates destinations, serializes and parses LeaseSet2 payloads, and asserts the online and offline verification outcomes, with the takeaway that the candidate path is directly exercised.

Evidence from the check

  • Temporary runner that copies the test into the internal package, runs baseline and offline cases, captures their command output, and removes the copied test, with the takeaway that repository production code was not modified.

Online LeaseSet2 baseline verification output

  • Executed online baseline command output showing flags zero with no offline section still verifies successfully, with the takeaway that the standard non-offline serialization remains valid.

Offline LeaseSet2 peer verification output

  • Executed offline destination command output showing flags zero and no offline section while peer verification returns false, with the takeaway that the offline-published LeaseSet2 is rejected.

LeaseSet2 signing and verification source evidence

  • Executed capture of the exact relevant source lines showing transient signing, literal LS2 flags zero, and the verifier’s conditional offline-key path, with the takeaway that the observed rejection matches the code path.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: foundation/address_generator.go
Line: 305-315

Comment:
**Offline LeaseSets Are Rejected**

Offline destinations now sign LeaseSet2 payloads with their transient signing key, but LeaseSet2 publication still emits zero flags and no offline authorization. Receiving peers consequently verify the transient-key signature with the destination’s long-term public key and reject the LeaseSet2, leaving the offline SAM destination unreachable. Serialize the offline authorization, set the offline flag, and size and sign the LeaseSet2 using the transient signature type before merging.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (6)

Grey Divider

🔗 Fix PR: #3

This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR.

Prompt for coding agent

This is an automated fix prepared on a separate branch (#3). It is NOT applied to this PR.
To use it: review Fix PR #3 (https://github.com/gosuda/IVNP/pull/3), evaluate each change critically against your local context, and cherry-pick the changes that are correct into this branch. Do not accept them blindly.
Process — 6 fixed
  • ☑ Fixed: Offline keys break Datagram1
  • ☑ Fixed: Encrypted offline sessions cannot publish
  • ☑ Fixed: Datagram verification reads wall clock
  • ☑ Fixed: Sign leaves expanded key
  • ☑ Fixed: SignedContent allocates destination buffer
  • ☑ Fixed: Offline parser copies wire fields
  • ⏭ Skipped (2)

LocalDestination.Sign now fails with ErrOfflineSignatureExpired once the
offline authorization has lapsed, so stale transient keys cannot keep
producing Datagram2 traffic. Also wipe the throwaway X25519 key generated
for ElGamal private destinations and document that Datagram3's FROM field
is attacker-controlled.
@gg582

gg582 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Follow-up security audit fixes (830e457):

  • Send path now enforces offline signature expiry: LocalDestination.Sign returns ErrOfflineSignatureExpired for stale transient keys (same rule as the receiver-side VerifyTargetAt)
  • Throwaway X25519 key generated for ElGamal private destinations is wiped after use
  • Code comments now state explicitly that Datagram3's FROM field is sender-controlled; use DATAGRAM/DATAGRAM2 when source authentication matters

Comment thread foundation/address_generator.go Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 830e457

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviews (2): Last reviewed commit: "fix(foundation): reject expired offline ..." | Re-trigger Greptile

- Inject clock into SAM server and foundation offline signing instead of
  reading the wall clock in verification and expiry paths
- Parse SAM offline private key sections as zero-copy views over the
  caller-owned wire buffer
- Unify datagram offline signature type with foundation.OfflineSignature
- Wipe expanded Ed25519 keys and Red25519 key copies after signing
- Add bounded MarshalSignedContentTo serializer with explicit capacity
  errors
- Reject SESSION CREATE for offline destinations combined with legacy
  DATAGRAM style or encrypted LeaseSet options; Datagram1 cannot carry
  the offline authorization and blinding requires the long-term key
- Publish LeaseSet2 with the offline signature section and flag so peers
  can verify transient-key signatures, refusing expired authorizations
- Deflake loopback test: RECEIVED lines may precede the STATUS reply on
  the shared control connection
@gg582

gg582 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

CI failure and review findings addressed in f612c45.

CI: TestEmbeddedServerLiveLoopbackStylesAndRecovery was racing — inbound forwarding may write DATAGRAM RECEIVED on the control connection before the STATUS reply. The test now accepts either ordering (same fix applied to the RAW section).

Review findings, all fixed:

  1. VerifyTarget wall-clock read: ServerConfig.Now is injected, SAM calls VerifyTargetAt with it
  2. parseOfflinePrivateKey now returns zero-copy views over the wire buffer; the caller wipes wire on all exits
  3. datagram.OfflineSignature is now an alias of foundation.OfflineSignature; field-by-field translations removed
  4. Expanded Ed25519 key and Red25519 key copies are wiped after signing
  5. SignedContent replaced with SignedContentLen + MarshalSignedContentTo (fixed-capacity, ErrDestinationSmall)
  6. SESSION CREATE rejects offline destination + encrypted LeaseSet options (blinding needs the long-term key)
  7. LocalLeaseSet2 now emits the offline signature section and flag, sizes with the transient key's signature length, and refuses expired authorization; encrypted LS2 constructor rejects offline destinations
  8. STYLE=DATAGRAM with an offline destination is rejected with INVALID_KEY (Datagram1 cannot carry the authorization); DATAGRAM2/3 unaffected
  9. foundation offline expiry uses an injectable clock; tests use fixed instants

Verified: go build ./..., full go test -count=1 ./... (the only intermittent failure is the unrelated pre-existing TestSSU2LiveVectorReadAuthDispatchWriteAllocations timing test), importformatter, gojgp lint.

Comment thread networking/internal/netdb/local_ls2_offline_test.go
ctx, cancel := context.WithCancel(root.ctx)
child := &samSession{server: s, root: root, id: id, style: style, endpoint: root.endpoint, control: connection, ctx: ctx, cancel: cancel, sourceIP: root.sourceIP, fromPort: fromPort, toPort: toPort, listenPort: listenPort, protocol: protocol, listenProtocol: listenProtocol, rawHeader: rawHeader, udpTarget: udpTarget, children: make(map[string]*samSession), attachments: make(map[net.Conn]struct{}), queueBytes: newByteBudget(s.config.MaxSessionQueueBytes), acceptRequests: make(chan acceptRequest, s.config.SessionQueue)}
child.datagramOverhead = root.datagramOverhead
child := &samSession{server: s, root: root, id: id, style: style, endpoint: root.endpoint, control: connection, ctx: ctx, cancel: cancel, sourceIP: root.sourceIP, fromPort: fromPort, toPort: toPort, listenPort: listenPort, protocol: protocol, listenProtocol: listenProtocol, rawHeader: rawHeader, udpTarget: udpTarget, now: root.now, children: make(map[string]*samSession), attachments: make(map[net.Conn]struct{}), queueBytes: newByteBudget(s.config.MaxSessionQueueBytes), acceptRequests: make(chan acceptRequest, s.config.SessionQueue)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Offline primary permits datagram1 child 🐞 Bug ≡ Correctness

An offline STYLE=PRIMARY session can add a legacy STYLE=DATAGRAM child because addSubsession
reuses the offline root endpoint without applying the new Datagram1 restriction. Outbound packets
are then signed by the transient key, but Datagram1 cannot carry its authorization, so peers reject
every packet.
Agent Prompt
## Issue description
Offline PRIMARY sessions can add a legacy DATAGRAM subsession, bypassing the root-session validation. Reject such subsessions because Datagram1 cannot transmit the offline authorization needed to verify transient-key signatures.

## Issue Context
The child reuses the root endpoint and therefore signs with the offline transient key. DATAGRAM2 supports the authorization section and DATAGRAM3 is unsigned, so only legacy DATAGRAM must be rejected.

## Fix Focus Areas
- client/internal/sam/protocol.go[275-275]
- client/internal/sam/offline_test.go[168-230]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +126 to +130
if offline != nil {
// Peers verify the LS2 signature with the transient key authorized by
// this offline signature; publishing past its expiry is useless.
if published > uint64(offline.Expires) {
return 0, ErrLocalLeaseSet2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Offline leasesets outlive authorization 🐞 Bug ⛨ Security

The offline expiry check rejects only a publication whose published time is already expired, then
allows leases whose end time extends past the transient-key authorization. Remote LS2 verification
and database retention do not enforce Offline.Expires, so that LeaseSet remains usable until its
normal lease expiry after the offline authorization has expired.
Agent Prompt
## Issue description
Offline-signed LeaseSet2 payloads can advertise lease validity beyond the offline authorization expiry. The local serializer checks only whether publication begins after expiry, while receivers retain a signature-valid LeaseSet based on ordinary lease dates.

## Issue Context
Ensure an offline signature limits the complete validity period of a LeaseSet signed with its transient key. Cap or reject local lease ranges extending beyond `offline.Expires`, and enforce the same bound during remote LS2 admission/validation so externally produced payloads cannot bypass it.

## Fix Focus Areas
- networking/internal/netdb/local_ls2.go[126-155]
- networking/internal/netdb/database.go[291-329]
- networking/internal/netdb/structures.go[542-556]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f612c45

TestSSU2LiveVectorReadAuthDispatchWriteAllocations timed out on loaded CI
runners: the 500ms handshake timeout and 5s warmup/delivery waits left no
room for scheduling delays. Raise the handshake timeout to 2s and the
waits to 30s; the allocation assertions are unchanged.

TestDatagramModernSendWithoutEndpointSupport reused the session ID dg
across styles, but session teardown after connection close is
asynchronous, so the second SESSION CREATE raced with removal and could
fail with DUPLICATED_ID. Use a distinct ID per style.
@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

T-Rex T-Rex Logs

What T-Rex did

  • Authored the test trex-artifacts/offline-primary-datagram-probe-test.go to probe the offline primary datagram behavior.
  • Verified the bad behavior existed before changes by examining offline-primary-datagram-01-before.log.
  • Verified the bad behavior persists after changes by examining offline-primary-datagram-02-after.log.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (3): Last reviewed commit: "fix: address review findings on offline ..." | Re-trigger Greptile

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f260e77

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviews (4): Last reviewed commit: "test: deflake SSU2 allocation and SAM en..." | Re-trigger Greptile

@gg582
gg582 marked this pull request as ready for review September 5, 2026 04:23
- SESSION ADD under an offline PRIMARY root now rejects STYLE=DATAGRAM,
  matching the createSession restriction; Datagram1 cannot carry the
  offline authorization
- LocalLeaseSet2.MarshalTo caps lease end times at the offline
  authorization expiry so no published lease outlives the transient key
- Wipe the expanded transient Ed25519 key in the netdb offline test
  helper
Comment on lines +284 to +288
_, _ = io.WriteString(control, "SESSION CREATE STYLE="+style+" ID="+id+" DESTINATION=TRANSIENT\n")
if line := readSAMLine(t, reader); !strings.Contains(line, "RESULT=OK") {
t.Fatalf("%s create = %q", style, line)
}
_, _ = io.WriteString(control, "DATAGRAM SEND ID="+id+" DESTINATION=peer.i2p SIZE=4\nDATA")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Looped sam connections leak on failure 📜 Skill insight ▣ Testability

Each loop iteration creates a control connection, but control.Close() is reached only on the
success path; a failed readSAMLine or assertion exits via t.Fatalf and leaves that connection
open. This violates the requirement that tests clean up every resource they create.
Agent Prompt
## Issue description
The loop creates a SAM control connection but closes it only after all assertions succeed, so test failures can leak connections and associated session resources.

## Issue Context
Use per-iteration cleanup that runs when `t.Fatalf` aborts the current test iteration. Preserve the distinct session IDs introduced to avoid asynchronous teardown races.

## Fix Focus Areas
- client/internal/sam/datagram_modern_test.go[284-288]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit f260e77

@qodo-code-review

Copy link
Copy Markdown

Qodo Fixer

🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (4)

Grey Divider

🔗 Fix PR: #4

This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR.

Prompt for coding agent

This is an automated fix prepared on a separate branch (#4). It is NOT applied to this PR.
To use it: review Fix PR #4 (https://github.com/gosuda/IVNP/pull/4), evaluate each change critically against your local context, and cherry-pick the changes that are correct into this branch. Do not accept them blindly.
Process — 4 fixed
  • ☑ Fixed: Looped SAM connections leak on failure
  • ☑ Fixed: Offline LeaseSets outlive authorization
  • ☑ Fixed: Offline primary permits Datagram1 child
  • ☑ Fixed: Test leaves transient key unwiped

@gg582

gg582 commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Latest review round addressed in 50ab427:

  • Offline PRIMARY sessions can no longer add a legacy STYLE=DATAGRAM child (addSubsession applied the root endpoint without the Datagram1 restriction); DATAGRAM2 children still work
  • LocalLeaseSet2.MarshalTo now caps lease end times at the offline authorization expiry, so published leases cannot outlive the transient key
  • The netdb offline test helper wipes the expanded transient Ed25519 key

Added regression tests for both behaviors (TestSessionAddOfflinePrimaryDatagram1Rejected, TestLocalLeaseSet2OfflineCapsLeaseExpiry). Full suite, importformatter, and gojgp lint all pass.

Comment on lines +146 to +149
if offline != nil && leases[i].EndDate > offline.Expires {
// Remote verifiers stop trusting the transient key at the offline
// authorization expiry, so no lease may outlive it.
leases[i].EndDate = offline.Expires

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Publisher reuses expired capped ls2 🐞 Bug ☼ Reliability

LocalLeaseSet2.MarshalTo caps only its serialized lease copy, while LeaseSetPublisher schedules
and caches that payload using the original later inbound expiry. After the offline authorization
expires, unchanged inbound leases therefore leave the expired cached LS2 eligible for forced or
scheduled republication, producing rejected network advertisements.
Agent Prompt
## Issue description
Offline LS2 serialization caps lease end dates at the offline authorization expiry, but `LeaseSetPublisher` continues tracking the original inbound lease expiry. This allows its cached LS2 payload to be republished after the serialized lease and authorization have expired.

## Issue Context
The effective expiry used for publication scheduling and cached-payload validity must match the expiry actually serialized into the LS2. Expired cached payloads must not be sent, including forced publication paths.

## Fix Focus Areas
- networking/internal/netdb/local_ls2.go[145-155]
- networking/internal/netdb/publication.go[269-355]
- networking/internal/netdb/publication.go[402-405]
- networking/internal/netdb/local_ls2_offline_test.go[120-157]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 50ab427

@greptile-apps

greptile-apps Bot commented Sep 5, 2026

Copy link
Copy Markdown

Reviews (5): Last reviewed commit: "fix: close remaining offline-signature g..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant